* 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>
125 lines
6.8 KiB
Markdown
125 lines
6.8 KiB
Markdown
---
|
|
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](https://github.com/pangeachat/synapse-pangea-chat/pull/21)
|
|
- **Course plans**: [course-plans.instructions.md](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`
|
|
|
|
1. User clicks link → GoRouter navigates to `/join_with_link`
|
|
2. Class code is saved to local storage (`SpaceCodeRepo`)
|
|
3. Redirect to `/home`
|
|
4. 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()`:
|
|
|
|
1. Save code as `recentCode` (used for invite dedup in Route 3)
|
|
2. `POST /_synapse/client/pangea/v1/knock_with_code` — Pangea-custom Synapse endpoint that validates the access code and invites the user
|
|
3. Response contains `{ roomIds, alreadyJoined, rateLimited }`
|
|
4. `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.to` or direct room ID navigation. Shows a "Join" button that becomes a knock if the room's join rule is `knock`.
|
|
- **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:
|
|
|
|
1. **Child of joined parent** → auto-join (no prompt)
|
|
2. **Code just inputted** → skip (Route 2 is handling it)
|
|
3. **Previously knocked** → auto-join (no prompt)
|
|
4. **Otherwise** → show accept/decline dialog
|
|
|
|
### KnockTracker
|
|
|
|
The server-side `auto_accept_invite` module was removed because it crashed Synapse ([PR #21](https://github.com/pangeachat/synapse-pangea-chat/pull/21)). Auto-accept now lives client-side via [`KnockTracker`](../../lib/pangea/join_codes/knock_tracker.dart).
|
|
|
|
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 |
|
|
|
|
---
|
|
|
|
## Future Work
|
|
|