feat: Phase 8 — Typing Indicators & Phase 9 — Private Rooms #7

Merged
longjacksonle merged 21 commits from long-branch into main 2026-04-13 14:59:27 +02:00

Summary

Phase 8 — Typing Indicators

  • WebSocket hub tracks active typers per room; broadcasts UserTyping /
    UserStoppedTyping events
  • New Typing client control message; WsHandle.send_typing() helper throttles to
    once per second per room
  • Room view and DM view both show live typing banners ("X is typing…", "X and Y
    are typing…", "Several people are typing…")
  • Typer is cleared from the list when their message arrives, avoiding stale
    indicators

Phase 9 — Private / Invite-Only Rooms

  • DB migration adds invite_code TEXT column to rooms with a unique index
  • list_rooms now filters: non-admins see only public rooms plus private rooms
    they're members of; admins see all
  • send_message and list_messages enforce room access — private rooms reject
    non-members with Access denied
    get_room_by_invite, regenerate_invite_code
  • New server functions: join_room_by_invite, leave_room, invite_user_to_room
    (mod+), regenerate_invite_code (admin)
  • /invite/:code route — visiting the link auto-joins and redirects to the room
  • Admin Rooms panel shows type badges, invite link copy, regenerate button, and
    direct invite-by-username field
  • Room creator is auto-added as a member on creation; RoomMemberAdded WS event
    fires so their sidebar refreshes immediately
  • Sidebar reacts to RoomMemberAdded / RoomMemberRemoved events to refresh room
    list without a page reload
  • 7 integration tests: member visibility, access control, invite lookup,
    idempotent add, code regeneration

Bug fixes (shipped in this branch)

  • RefCell already borrowed WASM panic — all oninput and onclick signal mutations
    now run inside spawn(async move { ... }), deferring them past the event handler's
    RefCell borrow
  • Form GET submissions — login, register, room composer, DM composer converted
    from to
    + type="button" + onkeydown Enter handling;
    credentials no longer appear in the URL
  • Room switching without reload — room_id prop is synced into a reactive Signal
    each render so use_server_future re-runs when navigating between rooms
  • Hook ordering — use_navigator() and use_context_provider moved to unconditional
    call sites in AuthLayout
  • DB persistence — named Docker volume lets_chat_data:/data added to
    compose.dev.yml so SQLite databases survive container rebuilds

Test plan

  • Register a new account — fields stay populated, no URL leaking, redirects to
    home on success
  • Log out and log back in — login form works consistently
  • Click between #general and #random — room content switches without a page reload
  • Send messages in a room and DM — Enter key and Send button both work; no GET
    redirect
  • Create a private room in Admin → Rooms — room appears in creator's sidebar
    immediately
  • Copy invite link and open in another browser session — user joins and room
    appears in their sidebar
  • Invite a user by username from the admin panel — they see the room without using
    an invite link
  • Confirm non-members get Access denied when accessing a private room directly
  • Verify typing indicator appears and clears correctly in both rooms and DMs
  • Run cargo test — all 45 tests pass
Summary Phase 8 — Typing Indicators - WebSocket hub tracks active typers per room; broadcasts UserTyping / UserStoppedTyping events - New Typing client control message; WsHandle.send_typing() helper throttles to once per second per room - Room view and DM view both show live typing banners ("X is typing…", "X and Y are typing…", "Several people are typing…") - Typer is cleared from the list when their message arrives, avoiding stale indicators Phase 9 — Private / Invite-Only Rooms - DB migration adds invite_code TEXT column to rooms with a unique index - list_rooms now filters: non-admins see only public rooms plus private rooms they're members of; admins see all - send_message and list_messages enforce room access — private rooms reject non-members with Access denied get_room_by_invite, regenerate_invite_code - New server functions: join_room_by_invite, leave_room, invite_user_to_room (mod+), regenerate_invite_code (admin) - /invite/:code route — visiting the link auto-joins and redirects to the room - Admin Rooms panel shows type badges, invite link copy, regenerate button, and direct invite-by-username field - Room creator is auto-added as a member on creation; RoomMemberAdded WS event fires so their sidebar refreshes immediately - Sidebar reacts to RoomMemberAdded / RoomMemberRemoved events to refresh room list without a page reload - 7 integration tests: member visibility, access control, invite lookup, idempotent add, code regeneration Bug fixes (shipped in this branch) - RefCell already borrowed WASM panic — all oninput and onclick signal mutations now run inside spawn(async move { ... }), deferring them past the event handler's RefCell borrow - Form GET submissions — login, register, room composer, DM composer converted from <form onsubmit> to <div> + type="button" + onkeydown Enter handling; credentials no longer appear in the URL - Room switching without reload — room_id prop is synced into a reactive Signal each render so use_server_future re-runs when navigating between rooms - Hook ordering — use_navigator() and use_context_provider moved to unconditional call sites in AuthLayout - DB persistence — named Docker volume lets_chat_data:/data added to compose.dev.yml so SQLite databases survive container rebuilds Test plan - Register a new account — fields stay populated, no URL leaking, redirects to home on success - Log out and log back in — login form works consistently - Click between #general and #random — room content switches without a page reload - Send messages in a room and DM — Enter key and Send button both work; no GET redirect - Create a private room in Admin → Rooms — room appears in creator's sidebar immediately - Copy invite link and open in another browser session — user joins and room appears in their sidebar - Invite a user by username from the admin panel — they see the room without using an invite link - Confirm non-members get Access denied when accessing a private room directly - Verify typing indicator appears and clears correctly in both rooms and DMs - Run cargo test — all 45 tests pass
Extend `list_rooms` to filter by membership for non-admin users, add
`is_room_member`, `add_room_member`, `remove_room_member`,
`get_room_by_invite`, and `regenerate_invite_code` helpers. Update
`create_room` to accept `room_type` and `invite_code`. Deduplicate row
mapping via a shared `map_room` helper.
- Update all components to use Signal<Option<User>> instead of Signal<User>
- Replace form onsubmit handlers with spawn-based do_login/do_register closures that support Enter key submission
- Add rooms_version signal to sidebar for reactive refresh on room membership changes
- Rename use_invite_code to redeem_invite_code in test
- Add join_room_by_invite and leave_room server functions
- Fix invite_code uniqueness: move UNIQUE constraint to index instead of column definition
- Rename use_invite_code to redeem_invite_code for clarity
- Refactor AuthLayout to hoist hooks unconditionally and use Signal<Option<User>>
- Check all phase 9 checklist items as done in plan and TODO
- Mount tests/ and lets_chat_data volume in compose.dev.yml
- Expand explicit test list in justfile test recipe
- Add cargo check, dx check, cargo test to allowed Claude tools
Replace parsed_id with a reactive Signal in RoomViewPage so switching
rooms re-runs server futures and WS subscriptions. Also auto-join the
creator as a room member in create_room and broadcast RoomMemberAdded.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!7
No description provided.