Skip to main content
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)

  1. Separate entity, shared slug. ClassroomController is untouched; the apps call POST /chatrooms after 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} with slug), and an open conversation follows the rename live.
  2. 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) over web_socket_channel, the same code on web, Android and iOS (asked and chosen on 20/10/2026).
  3. Polling fallback. When GET /api/realtime/config says enabled: false (driver log), or the socket is down, an open conversation polls GET /chatrooms/{slug}/messages?after= every 5 s. After every (re)subscription it catches up, so a reconnect never loses messages.
  4. Uploads before sending. POST /chatrooms/{slug}/attachments takes one image (≤ 20 MB per request, so the server limit stays per file); the message then carries attachment_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).
  5. Notification collapse. chatroom_members.last_notification_id: while that notification is unread, new staff messages add nothing, so 10 quick messages = 1 notification.
  6. 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}).
  7. Content managers may create / sync a chatroom (they create classrooms) and see its details, but cannot read the messages or moderate.
  8. 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}.csv instead 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, edit ChatroomService::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 test on 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 is test/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

The migration only creates tables: nothing existing changes, rollback = 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 is LMS_Backend/CHATROOMS.md (env block, migrate, caches, supervisor / systemd, nginx, PHP limits, cron, checks, troubleshooting). Summary:
  1. Keys in the backend .env (one Reverb app per environment; the key is public, the secret is not):
    Then php artisan config:clear (or optimize).
  2. Keep Reverb running: a supervisor program (or systemd unit) running php artisan reverb:start in 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).
  3. 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 two location blocks 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-For passes the browser’s address on, and proxy_send_timeout gives sends the same 120 s as reads. 8080 is REVERB_SERVER_PORT. (/apps/ only matters when Laravel posts through the public host; with REVERB_HOST=127.0.0.1 it doesn’t.) Check from any machine:
  4. Uploads up to 20 MB: PHP upload_max_filesize = 25M, post_max_size = 25M (Plesk → PHP settings) and nginx client_max_body_size 25m;.
  5. Scheduler (for chatrooms:prune-attachments and the existing notifications:delete-expired): a cron * * * * * cd <backend> && php artisan schedule:run >> /dev/null 2>&1.
  6. Check: curl -H "Authorization: Bearer <token>" https://backend-test.theteam.com.tn/api/realtime/config answers enabled: true with 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.removed event 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.