Code DONE ✅ (20/10/2026): backend, Flutter web + phones and API docs. Mirror of
LMS_Project/CHATROOMS.md; the endpoint reference is Chatrooms API Endpoints.Server setup still to do (section 7). Until Laravel Reverb runs on a server, the chat works by polling every 5 seconds.
0. What you get
Chat features: paperclip for images only (JPG, PNG, GIF, WEBP, 20 MB each, 4 per message) with a
callout under the composer, clickable links (http/https/www, opened in a new tab), day separators,
grouped bubbles with name + role badge (Instructor / Admin), older pages loaded on scroll, full-size
image viewer. Moderators (instructor + admins): Learners can write switch (off = only staff can
post, the learner composer becomes a notice, live), Wipe history (confirmation dialog, deletes
messages and images for everyone). A staff message sends each learner an in-app notification
“New message in chatroom X” (never e-mail; at most one unread per learner at a time).
1. Decisions taken (and why)
- Separate entity, shared slug.
ClassroomControlleris untouched; the apps callPOST /chatroomsafter the classroom exists. Members are sent by e-mail (unique in the LMS) and stored as user ids. A chatroom can later get its own UI without a classroom. Since 20/10/2026 (Update 131) the slug itself is generated by the backend from the classroom title on creation (like training and quiz slugs) and editable on the classroom’s edit page; the save then renames the chatroom too (PUT /chatrooms/{old slug}withslug), and an open conversation follows the rename live. - Reverb + Pusher protocol, no Flutter SDK. Reverb 1.12 supports Laravel
^10.47(we are on 10.49), so no framework upgrade. The Flutter side is a ~300-line Pusher-protocol client (lib/core/services/realtime/realtime_service.dart) overweb_socket_channel, the same code on web, Android and iOS (asked and chosen on 20/10/2026). - Polling fallback. When
GET /api/realtime/configsaysenabled: false(driverlog), or the socket is down, an open conversation pollsGET /chatrooms/{slug}/messages?after=every 5 s. After every (re)subscription it catches up, so a reconnect never loses messages. - Uploads before sending.
POST /chatrooms/{slug}/attachmentstakes one image (≤ 20 MB per request, so the server limit stays per file); the message then carriesattachment_ids. Only the sender’s own, unsent uploads of the same chatroom are accepted (the ownership gap of the profElite doc is closed). Unsent uploads are deleted daily (chatrooms:prune-attachments). - Notification collapse.
chatroom_members.last_notification_id: while that notification is unread, new staff messages add nothing, so 10 quick messages = 1 notification. - Stepper order Basic info → Chatroom → Content (chosen on 20/10/2026). On Edit, Next walks the steps
and the final Save also syncs the chatroom’s title and members (
PUT /chatrooms/{slug}). - Content managers may create / sync a chatroom (they create classrooms) and see its details, but cannot read the messages or moderate.
- Admin backups (added 20/10/2026, after the first delivery). CSV, UTF-8 with a BOM (Excel shows
Arabic), columns
message_id, sent_at, sender_id, sender_name, sender_email, sender_role, content, attachments; cells starting with= + - @get a'(no spreadsheet formula injection). The name is{slug}-{Ymd-His}-{random 8}.csvinstead of the asked{slug}-{timestamp}.csv: a slug + time alone would be easy to guess, and the file holds private messages and e-mails. To drop the suffix, editChatroomService::backup().
2. Data model (2026_10_20_000000_create_chatrooms_tables)
Every column, the neighbour tables and an image’s life cycle: Chatrooms API §2.4.
Roles in a chatroom: instructor (
chatrooms.instructor_id), learner (member row),
admin (company admin or super-admin, not a member). Instructor + admins post at any time and
moderate; learners post while learners_can_post.
3. API (all in the auth + not-blocked + company-policy group)
Events on
private-chatroom.{id}: message.sent ({message}, or {message_id} when over 8 KB),
chatroom.updated ({chatroom: {id, title, learners_can_post}}), chatroom.cleared.
Full request / response examples: LMS_Documentation/api-reference/chatrooms.mdx.
4. Files
Backend:app/Http/Controllers/ChatroomController.php, app/Services/ChatroomService.php,
app/Models/Chatroom*.php (4), app/Events/Chatroom{MessageSent,Updated,Cleared}.php,
app/Console/Commands/PruneChatroomAttachments.php (+ daily in app/Console/Kernel.php),
routes/api.php, routes/channels.php, app/Providers/BroadcastServiceProvider.php (enabled in
config/app.php, routes under /api with Sanctum), config/broadcasting.php (reverb connection,
5 s / 3 s timeouts, client block), config/reverb.php (published, max_request_size 100 000),
.env.example, resources/lang/{en,fr,ar}/messages.php, tests/Feature/ChatroomTest.php (20 tests),
composer.json / composer.lock.
Flutter: lib/core/services/realtime/realtime_service.dart, lib/core/data/remote/chatroom.dart,
lib/core/data/response_type/chatrooms_response.dart, lib/core/providers/chatroom_provider.dart,
lib/core/providers/chatroom_conversation_controller.dart, lib/ui/widgets/chatroom/*
(conversation, links, actions), lib/ui/pages/admin_pages/chatrooms/* (table + conversation page,
shared by the instructor), classroom_chatroom_step.dart, classroom_chatroom_create_button.dart,
lib/ui/pages/student_pages/classroom/student_{classrooms_hub,chatrooms_view,chatroom_mobile_screen}.dart,
routes / shells / side menus, assets/icons/appbar_icons/chat.svg, 59 keys × 3 languages,
test/chatroom_conversation_test.dart (6) + test/chatroom_realtime_test.dart (8) +
test/chatroom_backup_test.dart (3). The UI-only
classroom_chat_room.dart and its test are gone. Details: LMS_Frontend/major_code_updates.md
Updates 129 and 130 (backup).
5. How it was verified (20/10/2026)
- Backend:
php artisan teston SQLite, 147 passed (20 new, the backup ones included). Pint on the new files. - Flutter:
flutter analyze, no new issue;flutter test, 864 passed. The only failure istest/widget_test.dart, commented out since the initial commit. - End to end on a real Reverb server (local, SQLite backend): a learner subscribed with the Flutter
RealtimeService; the instructor’s message reached the socket 26 ms after the POST, the learner got the in-app notification, the sender’s own socket was skipped (X-Socket-ID), closing the chat arrived live and made the learner’s POST a 403, an outsider’s channel auth was a 403, the wipe arrived live. - Screens rendered to PNG from widget tests (student web + phone, admin table + conversation, stepper step 2); a centered moderation bar and a status pill overflow were fixed from them.
- Not tried yet: the real web app / phones against a deployed backend (needs §6 and §7).
6. Deploying the code
php artisan migrate:rollback --step=1.
Classrooms created before today have no chatroom: open the classroom (Chat room tab → Create
chatroom) or Edit classroom → step 2.
7. Server setup for real time (Plesk) — TO DO
Without this the chat still works (5-second polling). With it, messages are instant. The step-by-step guide with every command isLMS_Backend/CHATROOMS.md (env block, migrate,
caches, supervisor / systemd, nginx, PHP limits, cron, checks, troubleshooting). Summary:
- Keys in the backend
.env(one Reverb app per environment; the key is public, the secret is not):Thenphp artisan config:clear(oroptimize). - Keep Reverb running: a supervisor program (or systemd unit) running
php artisan reverb:startin the backend folder as the vhost’s user,autorestart=true. After each deploy:php artisan reverb:restart. Plesk’s “Scheduled tasks” can’t keep a process alive, so this needs SSH / root (apt install supervisor). - nginx proxy (Plesk → the backend domain → Apache & nginx Settings → Additional nginx
directives), so
wss://goes through the existing certificate. That box is one field for the whole vhost: it also holds the upload limit of point 4 and the/storage/CORS block of the Flutter web app. The twolocationblocks do not conflict. The full box reads:^~stops Plesk’s pattern-based locations (such as “Serve static files directly by nginx”) from taking the request first.X-Forwarded-Forpasses the browser’s address on, andproxy_send_timeoutgives sends the same 120 s as reads.8080isREVERB_SERVER_PORT. (/apps/only matters when Laravel posts through the public host; withREVERB_HOST=127.0.0.1it doesn’t.) Check from any machine: - Uploads up to 20 MB: PHP
upload_max_filesize = 25M,post_max_size = 25M(Plesk → PHP settings) and nginxclient_max_body_size 25m;. - Scheduler (for
chatrooms:prune-attachmentsand the existingnotifications:delete-expired): a cron* * * * * cd <backend> && php artisan schedule:run >> /dev/null 2>&1. - Check:
curl -H "Authorization: Bearer <token>" https://backend-test.theteam.com.tn/api/realtime/configanswersenabled: truewith the public host; open a chatroom on two browsers and send a message.
8. Known limits / later
- A member removed from a chatroom keeps their open socket until they reconnect (REST already answers
403). Fix later with a
member.removedevent or by closing their Reverb connection. - The notification bell isn’t pushed live (it refreshes as before). The socket exists now, so a
private-App.Models.User.{id}event could update the badge later. - No read receipts / unread counters / typing indicator (not asked).
- Archiving a classroom keeps its chatroom (separate entity). A permanent delete of the classroom deletes its chatroom too since 20/10/2026; there is still no stand-alone chatroom delete endpoint.