diff --git a/app/Events/ChatMessageDeleted.php b/app/Events/ChatMessageDeleted.php index f7ba4b8..c511060 100644 --- a/app/Events/ChatMessageDeleted.php +++ b/app/Events/ChatMessageDeleted.php @@ -14,12 +14,15 @@ class ChatMessageDeleted implements ShouldBroadcastNow public int $messageId; + public string $deletedAt; + /** * Create a new event instance. */ - public function __construct(int $messageId) + public function __construct(int $messageId, ?string $deletedAt = null) { $this->messageId = $messageId; + $this->deletedAt = $deletedAt ?? now()->toIso8601String(); } /** @@ -38,4 +41,17 @@ public function broadcastAs(): string { return 'message.deleted'; } + + /** + * Data to broadcast with the event. + * + * @return array + */ + public function broadcastWith(): array + { + return [ + 'messageId' => $this->messageId, + 'deleted_at' => $this->deletedAt, + ]; + } } diff --git a/app/Events/ChatMessageReacted.php b/app/Events/ChatMessageReacted.php new file mode 100644 index 0000000..8f6262e --- /dev/null +++ b/app/Events/ChatMessageReacted.php @@ -0,0 +1,57 @@ +messageId = $messageId; + $this->reactions = $reactions; + } + + /** + * Get the channels the event should broadcast on. + * + * @return array + */ + public function broadcastOn(): array + { + return [ + new Channel('global-chat'), + ]; + } + + public function broadcastAs(): string + { + return 'message.reacted'; + } + + /** + * Data to broadcast with the event. + * + * @return array + */ + public function broadcastWith(): array + { + return [ + 'messageId' => $this->messageId, + 'reactions' => $this->reactions, + ]; + } +} diff --git a/app/Events/ChatMessageSent.php b/app/Events/ChatMessageSent.php index 1d85bd6..de7c362 100644 --- a/app/Events/ChatMessageSent.php +++ b/app/Events/ChatMessageSent.php @@ -24,9 +24,12 @@ public function __construct(ChatMessage $chatMessage) $this->message = [ 'id' => $chatMessage->id, - 'content' => $chatMessage->content, + 'content' => $chatMessage->deleted_at ? 'This message was deleted by a moderator.' : $chatMessage->content, + 'is_deleted' => $chatMessage->deleted_at !== null, + 'deleted_at' => $chatMessage->deleted_at?->toIso8601String(), 'reply_to_id' => $chatMessage->reply_to_id, 'reply_to_content' => $chatMessage->reply_to_content, + 'reactions' => $chatMessage->getFormattedReactions(null), 'created_at' => $chatMessage->created_at->toIso8601String(), 'user' => [ 'id' => $chatMessage->user->id, diff --git a/app/Events/ChatSettingsUpdated.php b/app/Events/ChatSettingsUpdated.php index df3bea4..4f917fb 100644 --- a/app/Events/ChatSettingsUpdated.php +++ b/app/Events/ChatSettingsUpdated.php @@ -27,6 +27,7 @@ public function __construct() 'cooldown_seconds' => (int) AppSetting::get('global_chat_cooldown_seconds', 30), 'max_messages' => (int) AppSetting::get('global_chat_max_messages', 200), 'max_length' => (int) AppSetting::get('global_chat_max_length', 280), + 'allowed_emojis' => (array) AppSetting::get('global_chat_allowed_emojis', ['👍', 'â¤ī¸', 'đŸ”Ĩ', '😂', '🎉', '😮', 'đŸ˜ĸ', '👏']), ]; } diff --git a/app/Http/Controllers/Admin/ChatSettingsController.php b/app/Http/Controllers/Admin/ChatSettingsController.php index 421e1bb..e13663c 100644 --- a/app/Http/Controllers/Admin/ChatSettingsController.php +++ b/app/Http/Controllers/Admin/ChatSettingsController.php @@ -2,10 +2,12 @@ namespace App\Http\Controllers\Admin; +use App\Events\ChatSettingsUpdated; use App\Http\Controllers\Controller; use App\Models\AppSetting; use App\Models\ChatMessage; use App\Models\ChatReport; +use App\Models\User; use Illuminate\Http\Request; use Inertia\Inertia; @@ -15,6 +17,7 @@ public function edit() { $bannedWords = AppSetting::get('global_chat_banned_words', ''); $bannedWordsText = is_array($bannedWords) ? implode(', ', $bannedWords) : (string) $bannedWords; + $allowedEmojis = AppSetting::get('global_chat_allowed_emojis', ['👍', 'â¤ī¸', 'đŸ”Ĩ', '😂', '🎉', '😮', 'đŸ˜ĸ', '👏']); return Inertia::render('admin/ChatSettings', [ 'settings' => [ @@ -26,6 +29,8 @@ public function edit() 'max_length' => (int) AppSetting::get('global_chat_max_length', 280), 'profanity_filter_enabled' => (bool) AppSetting::get('global_chat_profanity_filter_enabled', true), 'banned_words' => $bannedWordsText, + 'allowed_emojis' => (array) $allowedEmojis, + 'bot_username' => (string) AppSetting::get('global_chat_bot_username', 'hscstack'), ], 'totalMessages' => ChatMessage::count(), 'recentMessagesCount' => ChatMessage::where('created_at', '>=', now()->subHours(24))->count(), @@ -83,6 +88,9 @@ public function update(Request $request) 'max_length' => ['required', 'integer', 'min:50', 'max:1000'], 'profanity_filter_enabled' => ['required', 'boolean'], 'banned_words' => ['nullable', 'string'], + 'allowed_emojis' => ['nullable', 'array'], + 'allowed_emojis.*' => ['required', 'string', 'max:32'], + 'bot_username' => ['nullable', 'string', 'max:50', 'exists:users,username'], ]); $isEnabled = $validated['audience'] !== 'disabled'; @@ -95,12 +103,21 @@ public function update(Request $request) AppSetting::set('global_chat_profanity_filter_enabled', $validated['profanity_filter_enabled'], 'boolean'); AppSetting::set('global_chat_banned_words', $validated['banned_words'] ?? '', 'string'); + $emojis = ! empty($validated['allowed_emojis']) + ? array_values(array_unique(array_filter($validated['allowed_emojis']))) + : ['👍', 'â¤ī¸', 'đŸ”Ĩ', '😂', '🎉', '😮', 'đŸ˜ĸ', '👏']; + AppSetting::set('global_chat_allowed_emojis', $emojis, 'json'); + + if (isset($validated['bot_username']) && ! empty($validated['bot_username'])) { + AppSetting::set('global_chat_bot_username', $validated['bot_username'], 'string'); + } + // Immediately prune if current count exceeds new limit ChatMessage::pruneOldMessages($validated['max_messages']); // Broadcast settings update to global-chat channel try { - broadcast(new \App\Events\ChatSettingsUpdated()); + broadcast(new ChatSettingsUpdated); } catch (\Throwable $e) { // Log without failing response } @@ -132,4 +149,32 @@ public function deleteReport(ChatReport $report) return back()->with('success', 'Report deleted successfully.'); } + + public function updateUserBan(Request $request, User $user) + { + if ($user->can('view admin')) { + return back()->with('error', 'Staff and Admin accounts cannot be banned from chat.'); + } + + $validated = $request->validate([ + 'chat_banned_until' => ['nullable', 'date'], + ]); + + $user->update([ + 'chat_banned_until' => $validated['chat_banned_until'], + ]); + + $moderator = $request->user(); + + if ($user->isChatBanned()) { + $durationText = $user->chat_banned_until->diffForHumans(); + ChatMessage::sendBotMessage("Moderator @{$moderator->username} banned @{$user->username} from chat until {$user->chat_banned_until->toDayDateTimeString()} ({$durationText})."); + $message = "User @{$user->username} has been banned from chat until {$user->chat_banned_until->toDateTimeString()}."; + } else { + ChatMessage::sendBotMessage("Moderator @{$moderator->username} unbanned @{$user->username} from chat."); + $message = "User @{$user->username} has been unbanned from chat."; + } + + return back()->with('success', $message); + } } diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index d087b0c..c39cab6 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -41,14 +41,20 @@ public function store(StoreUserRequest $request) } $user = User::create($validated); - $role = $validated['role']; - $user->syncRoles([$role]); + $role = $validated['role'] ?? null; - if ($role === 'editor') { - $user->syncPermissions( - $validated['permissions'] ?? [] - ); + if (! empty($role)) { + $user->syncRoles([$role]); + + if ($role === 'editor') { + $user->syncPermissions( + $validated['permissions'] ?? [] + ); + } else { + $user->syncPermissions([]); + } } else { + $user->syncRoles([]); $user->syncPermissions([]); } @@ -86,16 +92,21 @@ public function update(UpdateUserRequest $request, User $user) $user->update($validated); - if (isset($validated['role'])) { + if (array_key_exists('role', $validated)) { $role = $validated['role']; - $user->syncRoles([$role]); + if (! empty($role)) { + $user->syncRoles([$role]); - if ($role === 'editor') { - $user->syncPermissions( - $validated['permissions'] ?? [] - ); + if ($role === 'editor') { + $user->syncPermissions( + $validated['permissions'] ?? [] + ); + } else { + $user->syncPermissions([]); + } } else { + $user->syncRoles([]); $user->syncPermissions([]); } } diff --git a/app/Http/Controllers/ChatController.php b/app/Http/Controllers/ChatController.php index cfc14ea..cf50ac3 100644 --- a/app/Http/Controllers/ChatController.php +++ b/app/Http/Controllers/ChatController.php @@ -3,9 +3,12 @@ namespace App\Http\Controllers; use App\Events\ChatMessageDeleted; +use App\Events\ChatMessageReacted; use App\Events\ChatMessageSent; use App\Models\AppSetting; use App\Models\ChatMessage; +use App\Models\ChatReport; +use App\Models\User; use App\Services\ChatProfanityFilter; use Illuminate\Http\Request; use Illuminate\Support\Facades\RateLimiter; @@ -48,7 +51,7 @@ public function index(Request $request) } // Fetch last X messages in chronological order - $messages = ChatMessage::with(['user:id,name,username,image_path,institution', 'user.roles:id,name']) + $messages = ChatMessage::with(['user:id,name,username,image_path,institution', 'user.roles:id,name', 'reactions.user:id,name,username']) ->latest('id') ->take($maxMessages) ->get() @@ -56,9 +59,12 @@ public function index(Request $request) ->values() ->map(fn (ChatMessage $msg) => [ 'id' => $msg->id, - 'content' => $msg->content, + 'content' => $msg->deleted_at ? 'This message was deleted by a moderator.' : $msg->content, + 'is_deleted' => $msg->deleted_at !== null, + 'deleted_at' => $msg->deleted_at?->toIso8601String(), 'reply_to_id' => $msg->reply_to_id, 'reply_to_content' => $msg->reply_to_content, + 'reactions' => $msg->getFormattedReactions($user?->id), 'created_at' => $msg->created_at->toIso8601String(), 'user' => [ 'id' => $msg->user->id, @@ -71,6 +77,8 @@ public function index(Request $request) ], ]); + $allowedEmojis = (array) AppSetting::get('global_chat_allowed_emojis', ['👍', 'â¤ī¸', 'đŸ”Ĩ', '😂', '🎉', '😮', 'đŸ˜ĸ', '👏']); + if (! $request->wantsJson() && ! $request->is('api/*')) { return Inertia::render('Chat/Index', [ 'chatState' => [ @@ -82,6 +90,7 @@ public function index(Request $request) 'can_post' => $canPost, 'reason' => $reason, 'can_delete' => (bool) $user?->can('manage chat'), + 'reaction_emojis' => $allowedEmojis, 'messages' => $messages, 'pusher_key' => config('broadcasting.connections.pusher.key'), 'pusher_cluster' => config('broadcasting.connections.pusher.options.cluster', 'ap2'), @@ -98,6 +107,7 @@ public function index(Request $request) 'can_post' => $canPost, 'reason' => $reason, 'can_delete' => (bool) $user?->can('manage chat'), + 'reaction_emojis' => $allowedEmojis, 'messages' => $messages, 'pusher_key' => config('broadcasting.connections.pusher.key'), 'pusher_cluster' => config('broadcasting.connections.pusher.options.cluster', 'ap2'), @@ -179,7 +189,9 @@ public function store(Request $request) if ($replyToId) { $parentMessage = ChatMessage::find($replyToId); if ($parentMessage) { - $replyToContent = Str::limit($parentMessage->content, 97, '...'); + $replyToContent = $parentMessage->deleted_at + ? 'This message was deleted by a moderator.' + : Str::limit($parentMessage->content, 97, '...'); } else { $replyToId = null; } @@ -211,9 +223,12 @@ public function store(Request $request) return response()->json([ 'id' => $message->id, - 'content' => $message->content, + 'content' => $message->deleted_at ? 'This message was deleted by a moderator.' : $message->content, + 'is_deleted' => $message->deleted_at !== null, + 'deleted_at' => $message->deleted_at?->toIso8601String(), 'reply_to_id' => $message->reply_to_id, 'reply_to_content' => $message->reply_to_content, + 'reactions' => [], 'created_at' => $message->created_at->toIso8601String(), 'user' => [ 'id' => $message->user->id, @@ -237,16 +252,19 @@ public function destroy(Request $request, ChatMessage $message) abort(403, 'Unauthorized'); } - $messageId = $message->id; - $message->delete(); + $deletedAt = now(); + $message->update(['deleted_at' => $deletedAt]); try { - broadcast(new ChatMessageDeleted($messageId))->toOthers(); + broadcast(new ChatMessageDeleted($message->id, $deletedAt->toIso8601String()))->toOthers(); } catch (\Throwable $e) { // Ignore } - return response()->json(['success' => true]); + return response()->json([ + 'success' => true, + 'deleted_at' => $deletedAt->toIso8601String(), + ]); } public function report(Request $request) @@ -264,7 +282,7 @@ public function report(Request $request) ]); // Prevent duplicate reporting of the exact same message content by the same user - $alreadyReported = \App\Models\ChatReport::where('reporter_id', $user->id) + $alreadyReported = ChatReport::where('reporter_id', $user->id) ->where('message_content', $validated['message_content']) ->where('reported_user_id', $validated['reported_user_id'] ?? null) ->exists(); @@ -275,7 +293,7 @@ public function report(Request $request) ], 422); } - $report = \App\Models\ChatReport::create([ + $report = ChatReport::create([ 'reporter_id' => $user->id, 'reported_user_id' => $validated['reported_user_id'] ?? null, 'reported_user_name' => $validated['reported_user_name'] ?? null, @@ -288,16 +306,21 @@ public function report(Request $request) // Auto-ban logic: If the reported user has 5 or more reports on this message/content, auto-ban for 1 day if (! empty($validated['reported_user_id'])) { - $reportedUser = \App\Models\User::find($validated['reported_user_id']); + $reportedUser = User::find($validated['reported_user_id']); if ($reportedUser && ! $reportedUser->can('view admin')) { - $totalReportsForMessage = \App\Models\ChatReport::where('reported_user_id', $reportedUser->id) + $totalReportsForMessage = ChatReport::where('reported_user_id', $reportedUser->id) ->where('message_content', $validated['message_content']) ->count(); if ($totalReportsForMessage >= 5) { + $wasAlreadyBanned = $reportedUser->isChatBanned(); $reportedUser->update([ 'chat_banned_until' => now()->addDay(), ]); + + if (! $wasAlreadyBanned) { + ChatMessage::sendBotMessage("System automatically suspended @{$reportedUser->username} from chat for 24 hours following community reports."); + } } } } @@ -307,4 +330,64 @@ public function report(Request $request) 'report_id' => $report->id, ], 201); } + + public function toggleReaction(Request $request, ChatMessage $message) + { + $user = $request->user(); + abort_unless($user, 401, 'Unauthenticated'); + + if ($user->isChatBanned()) { + return response()->json([ + 'message' => 'You cannot react to messages while chat banned.', + ], 403); + } + + if ($message->isDeleted()) { + return response()->json([ + 'message' => 'Cannot react to deleted messages.', + ], 422); + } + + $validated = $request->validate([ + 'emoji' => ['required', 'string', 'max:32'], + ]); + + $emoji = $validated['emoji']; + + // Allowed reaction emojis from settings + $allowedEmojis = (array) AppSetting::get('global_chat_allowed_emojis', ['👍', 'â¤ī¸', 'đŸ”Ĩ', '😂', '🎉', '😮', 'đŸ˜ĸ', '👏']); + if (! in_array($emoji, $allowedEmojis, true)) { + return response()->json([ + 'message' => 'This reaction emoji is not enabled.', + ], 422); + } + + $existing = $message->reactions() + ->where('user_id', $user->id) + ->where('emoji', $emoji) + ->first(); + + if ($existing) { + $existing->delete(); + } else { + $message->reactions()->create([ + 'user_id' => $user->id, + 'emoji' => $emoji, + ]); + } + + $formattedReactions = $message->getFormattedReactions($user->id); + $publicReactions = $message->getFormattedReactions(null); + + try { + broadcast(new ChatMessageReacted($message->id, $publicReactions))->toOthers(); + } catch (\Throwable $e) { + // Ignore + } + + return response()->json([ + 'success' => true, + 'reactions' => $formattedReactions, + ]); + } } diff --git a/app/Http/Requests/User/StoreUserRequest.php b/app/Http/Requests/User/StoreUserRequest.php index 00cd66a..6c40ad0 100644 --- a/app/Http/Requests/User/StoreUserRequest.php +++ b/app/Http/Requests/User/StoreUserRequest.php @@ -26,8 +26,8 @@ public function rules(): array 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'unique:users,email'], - 'role' => ['required', 'string', 'exists:roles,name'], - 'permissions' => ['required', 'array'], + 'role' => ['nullable', 'string'], + 'permissions' => ['nullable', 'array'], 'permissions.*' => ['string', 'exists:permissions,name'], 'file' => ['nullable', 'image', 'max:2048'], 'about' => ['nullable', 'string'], diff --git a/app/Http/Requests/User/UpdateUserRequest.php b/app/Http/Requests/User/UpdateUserRequest.php index 71ff13d..2c6a2ab 100644 --- a/app/Http/Requests/User/UpdateUserRequest.php +++ b/app/Http/Requests/User/UpdateUserRequest.php @@ -34,8 +34,8 @@ public function rules(): array 'facebook' => ['sometimes', 'nullable', 'string', 'max:255'], 'instagram' => ['sometimes', 'nullable', 'string', 'max:255'], 'github' => ['sometimes', 'nullable', 'string', 'max:255'], - 'role' => ['sometimes', 'string'], - 'permissions' => ['sometimes', 'array'], + 'role' => ['sometimes', 'nullable', 'string'], + 'permissions' => ['sometimes', 'nullable', 'array'], 'permissions.*' => ['string', 'exists:permissions,name'], 'chat_banned_until' => ['sometimes', 'nullable', 'date'], ]; diff --git a/app/Models/ChatMessage.php b/app/Models/ChatMessage.php index 9f87c1d..5858cb6 100644 --- a/app/Models/ChatMessage.php +++ b/app/Models/ChatMessage.php @@ -2,8 +2,10 @@ namespace App\Models; +use App\Events\ChatMessageSent; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; class ChatMessage extends Model { @@ -12,6 +14,11 @@ class ChatMessage extends Model 'content', 'reply_to_id', 'reply_to_content', + 'deleted_at', + ]; + + protected $casts = [ + 'deleted_at' => 'datetime', ]; public function user(): BelongsTo @@ -19,6 +26,39 @@ public function user(): BelongsTo return $this->belongsTo(User::class); } + public function reactions(): HasMany + { + return $this->hasMany(ChatMessageReaction::class); + } + + public function isDeleted(): bool + { + return $this->deleted_at !== null; + } + + /** + * Format reactions as an aggregated array. + */ + public function getFormattedReactions(?int $currentUserId = null): array + { + $reactions = $this->relationLoaded('reactions') + ? $this->reactions + : $this->reactions()->with('user:id,name,username')->get(); + + return $reactions + ->groupBy('emoji') + ->map(function ($group, $emoji) use ($currentUserId) { + return [ + 'emoji' => $emoji, + 'count' => $group->count(), + 'reacted' => $currentUserId ? $group->contains('user_id', $currentUserId) : false, + 'users' => $group->map(fn ($r) => $r->user?->username ?? $r->user?->name)->filter()->values()->toArray(), + ]; + }) + ->values() + ->toArray(); + } + /** * Keep only the latest N messages in the database. */ @@ -29,4 +69,31 @@ public static function pruneOldMessages(int $keepCount = 200): void static::where('id', '<=', $cutoffId)->delete(); } } + + /** + * Post an automated announcement message from the configured system bot. + */ + public static function sendBotMessage(string $content): ?self + { + $bot = User::getSystemBot(); + if (! $bot) { + return null; + } + + $message = static::create([ + 'user_id' => $bot->id, + 'content' => $content, + ]); + + $maxMessages = (int) AppSetting::get('global_chat_max_messages', 200); + static::pruneOldMessages($maxMessages); + + try { + broadcast(new ChatMessageSent($message)); + } catch (\Throwable $e) { + // Ignore broadcast failures in testing or if pusher is offline + } + + return $message; + } } diff --git a/app/Models/ChatMessageReaction.php b/app/Models/ChatMessageReaction.php new file mode 100644 index 0000000..4fa1a89 --- /dev/null +++ b/app/Models/ChatMessageReaction.php @@ -0,0 +1,25 @@ +belongsTo(ChatMessage::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 03f0191..b2b29ac 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -80,6 +80,13 @@ public function isChatBanned(): bool return $this->chat_banned_until !== null && $this->chat_banned_until->isFuture(); } + public static function getSystemBot(): ?self + { + $botUsername = AppSetting::get('global_chat_bot_username'); + + return $botUsername ? static::where('username', $botUsername)->first() : null; + } + public function getImageUrlAttribute() { if (! $this->image_path) { diff --git a/database/migrations/2026_08_28_111000_add_deleted_at_to_chat_messages_table.php b/database/migrations/2026_08_28_111000_add_deleted_at_to_chat_messages_table.php new file mode 100644 index 0000000..ccb7438 --- /dev/null +++ b/database/migrations/2026_08_28_111000_add_deleted_at_to_chat_messages_table.php @@ -0,0 +1,28 @@ +timestamp('deleted_at')->nullable()->after('reply_to_content'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('chat_messages', function (Blueprint $table) { + $table->dropColumn('deleted_at'); + }); + } +}; diff --git a/database/migrations/2026_08_28_111600_create_chat_message_reactions_table.php b/database/migrations/2026_08_28_111600_create_chat_message_reactions_table.php new file mode 100644 index 0000000..52e38cb --- /dev/null +++ b/database/migrations/2026_08_28_111600_create_chat_message_reactions_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('chat_message_id')->constrained('chat_messages')->cascadeOnDelete(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->string('emoji', 32); + $table->timestamps(); + + $table->unique(['chat_message_id', 'user_id', 'emoji'], 'chat_reaction_unique'); + $table->index(['chat_message_id', 'emoji'], 'chat_reaction_index'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('chat_message_reactions'); + } +}; diff --git a/resources/js/components/ChatBanModal.vue b/resources/js/components/ChatBanModal.vue new file mode 100644 index 0000000..7f296d6 --- /dev/null +++ b/resources/js/components/ChatBanModal.vue @@ -0,0 +1,288 @@ + + + diff --git a/resources/js/components/admin/UserRow.vue b/resources/js/components/admin/UserRow.vue index 8ac1d8c..114087d 100644 --- a/resources/js/components/admin/UserRow.vue +++ b/resources/js/components/admin/UserRow.vue @@ -5,7 +5,7 @@ import { usePermissions } from '@/lib/usePermissions'; const { can } = usePermissions(); -const props = defineProps({ +defineProps({ user: Object, }); @@ -13,7 +13,10 @@ const page = usePage(); const userId = page.props.auth.user.id; const isChatBanned = (user: any) => { - if (!user?.chat_banned_until) return false; + if (!user?.chat_banned_until) { + return false; + } + return new Date(user.chat_banned_until).getTime() > Date.now(); }; @@ -84,7 +87,7 @@ const deleteUser = (id: number) => { class="inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-bold uppercase ring-1 ring-inset" :class="getRoleBadgeStyles(user.roles?.[0]?.name)" > - {{ user.roles?.[0]?.name ?? 'no role' }} + {{ user.roles?.[0]?.name ?? 'student' }} -