From 0a2374629824941591a231ebbea71c4ffcbd6a44 Mon Sep 17 00:00:00 2001
From: Tajim
Date: Thu, 27 Aug 2026 21:44:15 +0600
Subject: [PATCH 01/15] refactor: remove unused chatSettings from
DashboardController
---
app/Http/Controllers/Admin/DashboardController.php | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php
index a941f73..0d19664 100644
--- a/app/Http/Controllers/Admin/DashboardController.php
+++ b/app/Http/Controllers/Admin/DashboardController.php
@@ -3,7 +3,6 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
-use App\Models\AppSetting;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
@@ -14,12 +13,7 @@ class DashboardController extends Controller
{
public function index()
{
- return Inertia::render('admin/Dashboard', [
- 'chatSettings' => [
- 'enabled' => AppSetting::get('global_chat_enabled', true),
- 'audience' => AppSetting::get('global_chat_audience', 'verified_members'),
- ],
- ]);
+ return Inertia::render('admin/Dashboard');
}
public function analytics(Request $request)
From 3eea086b728ee5930c99d82aea156f15481b74a5 Mon Sep 17 00:00:00 2001
From: Tajim
Date: Thu, 27 Aug 2026 22:09:10 +0600
Subject: [PATCH 02/15] feat(chat): add chat ban support with moderation
shortcuts and timestamps
---
app/Http/Controllers/ChatController.php | 12 ++
app/Http/Requests/User/UpdateUserRequest.php | 1 +
app/Models/User.php | 7 +
...7_add_chat_banned_until_to_users_table.php | 28 ++++
resources/js/components/admin/UserRow.vue | 18 ++-
resources/js/pages/Chat/Index.vue | 16 ++-
.../js/pages/admin/users/CreateOrEdit.vue | 123 +++++++++++++++++-
7 files changed, 200 insertions(+), 5 deletions(-)
create mode 100644 database/migrations/2026_08_27_160307_add_chat_banned_until_to_users_table.php
diff --git a/app/Http/Controllers/ChatController.php b/app/Http/Controllers/ChatController.php
index 58f8829..e85262e 100644
--- a/app/Http/Controllers/ChatController.php
+++ b/app/Http/Controllers/ChatController.php
@@ -29,6 +29,9 @@ public function index(Request $request)
$reason = 'Global chat is currently disabled for maintenance.';
} elseif (! $user) {
$reason = 'Please sign in to join the conversation.';
+ } elseif ($user->isChatBanned()) {
+ $bannedUntilFormatted = $user->chat_banned_until->diffForHumans();
+ $reason = "You are temporarily banned from chat until {$user->chat_banned_until->toDateTimeString()} ({$bannedUntilFormatted}).";
} elseif ($audience === 'verified_members') {
if ($user->is_verified || $user->can('view admin')) {
$canPost = true;
@@ -104,6 +107,15 @@ public function store(Request $request)
return response()->json(['message' => 'Global chat is currently disabled.'], 403);
}
+ // Check if user is chat banned
+ if ($user->isChatBanned()) {
+ $bannedUntilFormatted = $user->chat_banned_until->diffForHumans();
+
+ return response()->json([
+ 'message' => "You are banned from sending messages until {$user->chat_banned_until->toDateTimeString()} ({$bannedUntilFormatted}).",
+ ], 403);
+ }
+
// Check audience permission
if ($audience === 'verified_members' && ! $user->is_verified && ! $user->can('view admin')) {
return response()->json(['message' => 'Global chat is currently restricted to verified members.'], 403);
diff --git a/app/Http/Requests/User/UpdateUserRequest.php b/app/Http/Requests/User/UpdateUserRequest.php
index 84638ae..71ff13d 100644
--- a/app/Http/Requests/User/UpdateUserRequest.php
+++ b/app/Http/Requests/User/UpdateUserRequest.php
@@ -37,6 +37,7 @@ public function rules(): array
'role' => ['sometimes', 'string'],
'permissions' => ['sometimes', 'array'],
'permissions.*' => ['string', 'exists:permissions,name'],
+ 'chat_banned_until' => ['sometimes', 'nullable', 'date'],
];
return $rules;
diff --git a/app/Models/User.php b/app/Models/User.php
index c86e494..03f0191 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -43,6 +43,7 @@ class User extends Authenticatable
'receive_emails',
'google_id',
'email_verified_at',
+ 'chat_banned_until',
'image_path',
'about',
'title',
@@ -74,6 +75,11 @@ public function getIsVerifiedAttribute(): bool
: $this->roles()->exists();
}
+ public function isChatBanned(): bool
+ {
+ return $this->chat_banned_until !== null && $this->chat_banned_until->isFuture();
+ }
+
public function getImageUrlAttribute()
{
if (! $this->image_path) {
@@ -87,6 +93,7 @@ protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
+ 'chat_banned_until' => 'datetime',
'password' => 'hashed',
'receive_emails' => 'boolean',
];
diff --git a/database/migrations/2026_08_27_160307_add_chat_banned_until_to_users_table.php b/database/migrations/2026_08_27_160307_add_chat_banned_until_to_users_table.php
new file mode 100644
index 0000000..90df143
--- /dev/null
+++ b/database/migrations/2026_08_27_160307_add_chat_banned_until_to_users_table.php
@@ -0,0 +1,28 @@
+timestamp('chat_banned_until')->nullable()->after('receive_emails');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('users', function (Blueprint $table) {
+ $table->dropColumn('chat_banned_until');
+ });
+ }
+};
diff --git a/resources/js/components/admin/UserRow.vue b/resources/js/components/admin/UserRow.vue
index 26175ca..8ac1d8c 100644
--- a/resources/js/components/admin/UserRow.vue
+++ b/resources/js/components/admin/UserRow.vue
@@ -1,17 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reported Chat Messages
+
+
+ Review community reports, snapshot of reported messages, and moderate disruptive accounts.
+
+
+
+
+
+
+
+
+
+
+ Chat Configuration
+
+
+
+
+ Reported Messages
+
+ {{ pendingCount }}
+
+
+
+
+
+
+
+
+
+ Pending Review
+
+
+
+
+ {{ pendingCount }}
+
+
Requires moderation action
+
+
+
+
+
+ Resolved
+
+
+
+
+ {{ reviewedCount }}
+
+
Reviewed & resolved reports
+
+
+
+
+
+ Dismissed
+
+
+
+
+ {{ dismissedCount }}
+
+
Ignored or invalid reports
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No {{ currentFilter !== 'all' ? currentFilter : '' }} reports found
+
+
+ Everything looks clean for this filter criteria.
+
+
+
+
+
+
+
+
+
+
+ {{ report.status }}
+
+
+
+ Reported {{ formatDate(report.created_at) }}
+
+
+
+ by {{ report.reporter.name }}
+ (@{{ report.reporter.username }})
+
+
+
+
+ Reason:
+
+ {{ report.reason || 'Not specified' }}
+
+
+
+
+
+
+
+
+
+
Edit / Ban Author
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Message Author: {{ report.reported_user_name || report.reported_user?.name || 'Unknown' }}
+
+
+ (@{{ report.reported_user_username || report.reported_user?.username }})
+
+
+ Currently Banned
+
+
+
+
+ Sent {{ formatDate(report.message_sent_at) }}
+
+
+
+
+ "{{ report.message_content }}"
+
+
+
+
+
+
+
diff --git a/routes/admin.php b/routes/admin.php
index ab9d605..813ce34 100644
--- a/routes/admin.php
+++ b/routes/admin.php
@@ -115,6 +115,9 @@
// Chat Management & Settings
Route::middleware('permission:manage chat')->group(function () {
Route::get('/chat', [ChatSettingsController::class, 'edit'])->name('chat.edit');
+ Route::get('/chat/reports', [ChatSettingsController::class, 'reports'])->name('chat.reports.index');
Route::post('/chat/settings', [ChatSettingsController::class, 'update'])->name('chat.settings.update');
Route::post('/chat/clear', [ChatSettingsController::class, 'clearMessages'])->name('chat.clear');
+ Route::patch('/chat/reports/{report}/status', [ChatSettingsController::class, 'updateReportStatus'])->name('chat.reports.update-status');
+ Route::delete('/chat/reports/{report}', [ChatSettingsController::class, 'deleteReport'])->name('chat.reports.destroy');
});
diff --git a/routes/web.php b/routes/web.php
index 81af179..eeec2f7 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -38,6 +38,7 @@
// Global Chat Actions
Route::post('/api/chat/messages', [ChatController::class, 'store'])->name('chat.messages.store');
Route::delete('/api/chat/messages/{message}', [ChatController::class, 'destroy'])->name('chat.messages.destroy');
+ Route::post('/api/chat/reports', [ChatController::class, 'report'])->name('chat.reports.store');
});
// Global Chat Messages List (Public Read)
From fc5592b295cbe9063e0889d0e0ead8dddc3c2c3d Mon Sep 17 00:00:00 2001
From: Tajim
Date: Thu, 27 Aug 2026 22:39:19 +0600
Subject: [PATCH 04/15] feat(chat): add message reply support with ellipsized
snapshot and interactive jump
---
app/Events/ChatMessageSent.php | 2 +
app/Http/Controllers/ChatController.php | 19 +++
app/Models/ChatMessage.php | 2 +
...dd_reply_fields_to_chat_messages_table.php | 29 +++++
resources/js/pages/Chat/Index.vue | 108 ++++++++++++++++--
5 files changed, 150 insertions(+), 10 deletions(-)
create mode 100644 database/migrations/2026_08_27_163500_add_reply_fields_to_chat_messages_table.php
diff --git a/app/Events/ChatMessageSent.php b/app/Events/ChatMessageSent.php
index ad7f18c..1d85bd6 100644
--- a/app/Events/ChatMessageSent.php
+++ b/app/Events/ChatMessageSent.php
@@ -25,6 +25,8 @@ public function __construct(ChatMessage $chatMessage)
$this->message = [
'id' => $chatMessage->id,
'content' => $chatMessage->content,
+ 'reply_to_id' => $chatMessage->reply_to_id,
+ 'reply_to_content' => $chatMessage->reply_to_content,
'created_at' => $chatMessage->created_at->toIso8601String(),
'user' => [
'id' => $chatMessage->user->id,
diff --git a/app/Http/Controllers/ChatController.php b/app/Http/Controllers/ChatController.php
index cca5d22..932e0f3 100644
--- a/app/Http/Controllers/ChatController.php
+++ b/app/Http/Controllers/ChatController.php
@@ -8,6 +8,7 @@
use App\Models\ChatMessage;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
+use Illuminate\Support\Str;
use Inertia\Inertia;
class ChatController extends Controller
@@ -52,6 +53,8 @@ public function index(Request $request)
->map(fn (ChatMessage $msg) => [
'id' => $msg->id,
'content' => $msg->content,
+ 'reply_to_id' => $msg->reply_to_id,
+ 'reply_to_content' => $msg->reply_to_content,
'created_at' => $msg->created_at->toIso8601String(),
'user' => [
'id' => $msg->user->id,
@@ -134,13 +137,27 @@ public function store(Request $request)
$validated = $request->validate([
'content' => ['required', 'string', 'max:280'],
+ 'reply_to_id' => ['nullable', 'integer'],
]);
$content = trim($validated['content']);
+ $replyToId = $validated['reply_to_id'] ?? null;
+ $replyToContent = null;
+
+ if ($replyToId) {
+ $parentMessage = ChatMessage::find($replyToId);
+ if ($parentMessage) {
+ $replyToContent = Str::limit($parentMessage->content, 97, '...');
+ } else {
+ $replyToId = null;
+ }
+ }
$message = ChatMessage::create([
'user_id' => $user->id,
'content' => $content,
+ 'reply_to_id' => $replyToId,
+ 'reply_to_content' => $replyToContent,
]);
// Record rate limit for configured cooldown seconds
@@ -163,6 +180,8 @@ public function store(Request $request)
return response()->json([
'id' => $message->id,
'content' => $message->content,
+ 'reply_to_id' => $message->reply_to_id,
+ 'reply_to_content' => $message->reply_to_content,
'created_at' => $message->created_at->toIso8601String(),
'user' => [
'id' => $message->user->id,
diff --git a/app/Models/ChatMessage.php b/app/Models/ChatMessage.php
index 9f2f71f..9f87c1d 100644
--- a/app/Models/ChatMessage.php
+++ b/app/Models/ChatMessage.php
@@ -10,6 +10,8 @@ class ChatMessage extends Model
protected $fillable = [
'user_id',
'content',
+ 'reply_to_id',
+ 'reply_to_content',
];
public function user(): BelongsTo
diff --git a/database/migrations/2026_08_27_163500_add_reply_fields_to_chat_messages_table.php b/database/migrations/2026_08_27_163500_add_reply_fields_to_chat_messages_table.php
new file mode 100644
index 0000000..43a6481
--- /dev/null
+++ b/database/migrations/2026_08_27_163500_add_reply_fields_to_chat_messages_table.php
@@ -0,0 +1,29 @@
+unsignedBigInteger('reply_to_id')->nullable()->after('user_id');
+ $table->string('reply_to_content', 100)->nullable()->after('reply_to_id');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('chat_messages', function (Blueprint $table) {
+ $table->dropColumn(['reply_to_id', 'reply_to_content']);
+ });
+ }
+};
diff --git a/resources/js/pages/Chat/Index.vue b/resources/js/pages/Chat/Index.vue
index bdfdaa2..b44f674 100644
--- a/resources/js/pages/Chat/Index.vue
+++ b/resources/js/pages/Chat/Index.vue
@@ -1,6 +1,6 @@
@@ -154,7 +166,7 @@ const cooldownPresets = [
{{ totalMessages }}
Auto-pruning maintains max 200Auto-pruning maintains max {{ form.max_messages }}
@@ -411,6 +423,65 @@ const cooldownPresets = [
+
+
+
+
+
+ Specify how many recent messages to keep in storage.
+ Older messages beyond this limit are automatically pruned.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ messages
+
+
+
+
+
diff --git a/resources/js/pages/admin/ChatSettings.vue b/resources/js/pages/admin/ChatSettings.vue
index 3e1eed5..222d2a5 100644
--- a/resources/js/pages/admin/ChatSettings.vue
+++ b/resources/js/pages/admin/ChatSettings.vue
@@ -20,6 +20,7 @@ interface ChatSettingsProps {
audience: string;
cooldown_seconds: number;
max_messages: number;
+ max_length: number;
};
totalMessages: number;
recentMessagesCount: number;
@@ -33,6 +34,7 @@ const form = useForm({
audience: props.settings.audience,
cooldown_seconds: props.settings.cooldown_seconds,
max_messages: props.settings.max_messages ?? 200,
+ max_length: props.settings.max_length ?? 280,
});
const submitSettings = () => {
@@ -67,6 +69,13 @@ const messageLimitPresets = [
{ label: '500 Messages', value: 500 },
{ label: '1000 Messages', value: 1000 },
];
+
+const lengthPresets = [
+ { label: '140 Characters', value: 140 },
+ { label: '280 Characters (Default)', value: 280 },
+ { label: '500 Characters', value: 500 },
+ { label: '1000 Characters', value: 1000 },
+];
@@ -482,6 +491,64 @@ const messageLimitPresets = [
+
+
+
+
+
+ Control the maximum length allowed for any single student message.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ chars
+
+
+
+
+
+
+
+
+
+
+
+ Automatically detects and blocks vulgar, abusive, and inappropriate words before they can be sent.
+
+
+
+
+
+
+
+
+
+
+
+
+ The filter collapses repeated letters (e.g. "fuuuuck" → "fuck") and replaces common leet-speak (e.g. @ → a, $ → s, 0 → o).
+
+
+
+