feat/user-status #34

Merged
nrupard merged 5 commits from feat/user-status into main 2026-05-06 02:14:13 +02:00
Owner
No description provided.
- Adds three mutually-exclusive availability states (active default, idle, dnd) plus an independent free-text custom status (max 50 chars). Migration 0004_user_status.sql adds status, custom_status, and last_active_at columns to the users table; create_user backfills last_active_at because SQLite ALTER TABLE ADD COLUMN forbids non-constant defaults.
- Status circle overlay on every avatar via a new shared partials/avatar.html include. Sidebar (own + DM peers), DM header, and message author avatars all use it; settings page profile preview renders the circle inline so it can keep the asset_version cache-buster on the upload preview. Custom status text shows on hover via a title attribute (decision: not under every avatar to keep dense rows clean) and inline on the settings page.
- HTMX inline picker pattern (mirrors the emoji reaction picker in routes/reactions.rs) anchored to the sidebar own-avatar button. GET /status/picker returns the form, POST /status persists + broadcasts UserStatusChanged + returns the picker fragment plus an OOB swap of the own-avatar wrapper so the dot updates in the same response. GET /status/cancel collapses the popover.
- Idle auto-flip: tokio task spawned from main.rs ticks every 60s and runs db::auth::mark_idle_users with a 30 minute threshold, broadcasting UserStatusChanged for each row that flips. db::auth::touch_user_activity is the inverse - called from the WS connect, message-send, and typing paths; bumps last_active_at, atomically promotes idle back to active, and leaves DND alone (DND stays sticky and is never auto-changed).
- DND notification hook (routes/mod.rs::should_notify) returns false for DND users. The current broadcast path does not consult it because v1 has no push/toast/sound layer; it is a documented seam for the next contributor adding that delivery path. Unread badges and OOB DOM updates still deliver to DND users.
- New tests/db_status.rs covers default status, set_user_status validation (invalid enum, >50 char custom), touch_user_activity flip semantics, mark_idle_users threshold + DND-immunity, and is_user_dnd. All other test pools that touch the auth DB now include the 0004 migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Adds Hub::is_user_connected for view loaders to check WebSocket presence. Adds routes::effective_status that overrides a persisted active/idle/dnd value with "offline" when the user has no live connection.
- avatar.html renders bg-slate-400 for offline. The status enum stored in users.status still only carries explicit user choice plus the idle auto-flip, so the picker continues to expose three options and never persists "offline".
- Override applied in load_sidebar peer construction, load_author_meta for message authors, and the DM peer header. Self is exempt because the viewer is loading the page even before their WebSocket finishes opening, so trusting the persisted status avoids a brief gray flash on first paint. Sidebar own avatar and settings preview also keep persisted status (always self).
- Live trade-off unchanged from prior commit: WS disconnect does not broadcast UserStatusChanged, so other viewers still see the previous color until next page nav. Initial verify with a second account that never opened a WS confirms gray dot in DM header and sidebar peer row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Hub::connect now returns is_first_for_user and Hub::disconnect returns the user_id when the last connection closes, so the WS handler can emit UserStatusChanged on online/offline transitions without giving the hub awareness of the persisted status enum.
- routes/ws.rs broadcasts UserStatusChanged on first connection (re-fetched from DB so the wire value is fresh), then runs the existing touch_user_and_maybe_broadcast which handles idle->active flips. On disconnect of the last conn it broadcasts UserStatusChanged status="offline" so peers see the gray dot in real time.
- New ws/user_status_update.html fragment renders a tiny inline script that runs on every recipient via HTMX's default script handling. It selects every [data-user-status="X"] dot in the rendered DOM (sidebar peers, message authors, DM header, settings preview, picker trigger), swaps the bg-* class, and updates the parent span's title attribute for the custom-status tooltip. Per-user-id, status, and custom_status fields are pre-encoded as JSON literals to defend against quote/script injection in the user-controlled custom status text.
- Hand-tested with two sessions: alice held an open WS while bob (a) POSTed status=dnd custom="in meeting", (b) opened his own WS, (c) closed his WS. Alice's socket received four scripts in order with the correct status and tooltip values.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The htmx-ext-ws extension iterates top-level children of each incoming WS frame and only swaps elements that have hx-swap-oob set or that match a target by id (oobSwap defaults to "true" otherwise, requiring an id match). A bare <script> child has neither, so the previous fragment was silently dropped and never executed in the browser even though the wire format looked correct in headless tests.

Wrap the script in a <div id="status-events-slot" hx-swap-oob="innerHTML"> and add a matching empty slot inside the layout's hx-ext="ws" container. HTMX's allowScriptTags (true by default in 2.0) then evaluates the script after each swap, updating every avatar dot for the affected user across the rendered DOM.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous design only exposed custom_status through the parent span's title attribute, which is invisible until you hover the avatar dot - in practice no one notices it. Add a visible text slot beside each avatar wherever the custom text has room to breathe and wire the WS script to update it live.

- Sidebar own-header now shows the caller's custom status under their @username, italicized at text-xs.
- Each DM peer row shows the peer's custom status under the peer's display name in the same style. Peer name and custom text now share a flex column so the unread badge stays right-aligned.
- DM page header shows the peer's custom status under @username.
- ws/user_status_update.html script also walks every [data-user-custom="X"] element and replaces its textContent (using textContent, not innerHTML, so no markup escapes the JSON-encoded string).
- Message author rows still skip the inline text to keep dense chat history readable, matching the original design call. Avatar hover tooltip stays in place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
nrupard deleted branch feat/user-status 2026-05-06 02:14:14 +02:00
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!34
No description provided.