* feat: client-side knock auto-accept via KnockTracker Replace server-side AutoAcceptInviteIfKnocked (removed in synapse-pangea-chat PR #21) with client-side KnockTracker. - Record knocked room IDs in Matrix account data (org.pangea.knocked_rooms) - Auto-join when invite arrives for a previously knocked room - Migrate storage from GetStorage to Matrix account data for cross-device sync and reinstall persistence - Add joining-courses.instructions.md design doc * formatting * centralizes calls to knock-storage related functions --------- Co-authored-by: ggurdin <ggurdin@gmail.com>
6.8 KiB
| applyTo |
|---|
| lib/pangea/join_codes/**,lib/pangea/chat_list/**,lib/pangea/course_creation/**,lib/pangea/spaces/**,lib/pages/chat_list/**,lib/utils/url_launcher.dart,lib/config/routes.dart |
Joining Courses — Client Design
How users join courses (Matrix spaces) through three routes: class link, class code, and knock-accept.
- Synapse module cleanup: synapse-pangea-chat PR #21
- Course plans: course-plans.instructions.md
Join Routes Overview
| Route | Entry Point | Mechanism | User Interaction |
|---|---|---|---|
| Class link | Deep link URL | Extracts class code from URL → knock_with_code → join | None after click (auto-join) |
| Class code | Manual text input | knock_with_code → join | Enter code only |
| Knock-accept | "Ask to Join" button | Matrix knock → admin approves → invite → client auto-joins | Knock button; then wait for admin |
All three routes converge on the user becoming a Membership.join member of the course space. Child rooms (announcements, introductions, activity chats) are joined separately afterward.
Route 1 — Class Link
URL format: https://pangea.chat/#/join_with_link?classcode=XYZ
- User clicks link → GoRouter navigates to
/join_with_link - Class code is saved to local storage (
SpaceCodeRepo) - Redirect to
/home - On home load,
joinCachedSpaceCode()fires the knock_with_code + join flow (same as Route 2)
Pre-login persistence: If not logged in, the code persists on disk. After account creation, joinCachedSpaceCode() auto-joins.
matrix.to links: Standard Matrix links (https://matrix.to/#/...) go through a separate path. For knock-only rooms, this shows the public room bottom sheet with a code field and "Ask to Join" button.
Route 2 — Class Code
Three UI entry points (onboarding, in-app code page, public room bottom sheet) all converge on SpaceCodeController.joinSpaceWithCode():
- Save code as
recentCode(used for invite dedup in Route 3) POST /_synapse/client/pangea/v1/knock_with_code— Pangea-custom Synapse endpoint that validates the access code and invites the user- Response contains
{ roomIds, alreadyJoined, rateLimited } joinRoomById(spaceId)→ wait for sync → navigate to space
This is NOT a standard Matrix knock — the Synapse module handles the invite directly.
Route 3 — Knock-Accept
User knocks
User finds the course in one of three places:
- Public course preview — linked from
matrix.toor direct room ID navigation. Shows a "Join" button that becomes a knock if the room's join rule isknock. - Public room bottom sheet — search results for public rooms. For knock rooms, shows a code field and an "Ask to Join" button side by side. TODO: change to "Knock" for consistency.
- Public room dialog — a simpler variant of the bottom sheet used in some navigation paths. Button label changes to "Knock" for knock-rule rooms.
User taps the knock button → standard Matrix knockRoom() call → confirmation dialog ("You have knocked") → wait for admin.
Admin approves
Admin notices the knock in one of three places:
- Notification badge on the space icon in the navigation bar. TODO: need to add this.
- Chat list for that space — the knocking user appears as a pending entry. TODO: make this a bit more attention-grabbing.
- Member list (room participants page) — knocking users are sorted below joined members, labeled "Knocking."
Admin taps the knocking user → popup menu shows "Approve" (only visible for Membership.knock) → room.invite(userId).
Analytics room knocks follow the same mechanism — admins request access to a student's analytics room by knocking, and the student's client auto-accepts when the invite arrives.
Invite Handling
When an invite arrives via /sync, the sync listener routes it by room type:
| Room type | Action |
|---|---|
| Space | Evaluate priority rules (below) |
| Analytics room | Auto-join immediately |
| Previously knocked room | Auto-join immediately |
| Other | No sync-time action; handled when user taps it |
Space invite priority
When a space invite arrives, the client evaluates in order:
- Child of joined parent → auto-join (no prompt)
- Code just inputted → skip (Route 2 is handling it)
- Previously knocked → auto-join (no prompt)
- Otherwise → show accept/decline dialog
KnockTracker
The server-side auto_accept_invite module was removed because it crashed Synapse (PR #21). Auto-accept now lives client-side via KnockTracker.
Matrix /sync invites use StrippedStateEvent, which lacks unsigned / prev_content — so the client can't tell from the invite alone whether it previously knocked. KnockTracker solves this by recording each knocked room ID in Matrix account data (org.pangea.knocked_rooms). When an invite arrives for a tracked room, the client auto-joins and clears the record.
Account data is used so the state survives reinstall, logout, and syncs across devices. Only the user can initiate a knock, so an invite for a tracked room is always a legitimate approval. Applies to both spaces and non-space rooms.
All auto-join cases
Every case where room.join() is called without explicit user confirmation:
| Condition | Trigger |
|---|---|
| Navigating to an invited room's chat view | Building the widget |
| Invited space is a child of a joined parent | Space invite via sync |
| Tapping a left space in the list | User tap |
| Analytics room invite | Always auto-joined |
| Default chats in a course (announcements, introductions) | Viewing the course |
| knock_with_code succeeded | Code entry flow |
| User previously knocked on the room | Invite received for a prior knock |