58 lines
2.2 KiB
Markdown
58 lines
2.2 KiB
Markdown
# Messages Tab
|
|
|
|
**Nav id**: `messages` (default tab on load)
|
|
**Icon**: ✦
|
|
|
|
---
|
|
|
|
## What it shows
|
|
|
|
A full-screen chat thread between Quinn and the client. Quinn's messages appear on the right (gold-tinted bubbles), client messages on the left. Newest messages scroll into view automatically.
|
|
|
|
---
|
|
|
|
## Data flow
|
|
|
|
### Initial load
|
|
|
|
`fetchMessages(token)` — `GET /vip/messages/:token` — fetches the full message history on mount and on window focus.
|
|
|
|
After fetch, `markRead(token)` fires — `PUT /vip/messages/:token/read?side=client` — clears the unread badge server-side.
|
|
|
|
### Real-time updates
|
|
|
|
`useMessageStream` (`src/hooks/useMessageStream.ts`) opens a Server-Sent Events connection to `GET /vip/messages/:token/stream`. Incoming events are merged into local state, deduped by `msg.id`.
|
|
|
|
If SSE is unavailable or disconnects, the hook falls back to polling via `fetchMessages` on a timer.
|
|
|
|
### Sending
|
|
|
|
`sendMessage(token, text)` — `POST /vip/messages/:token` — appends the new message optimistically to local state on success.
|
|
|
|
Keyboard shortcut: Enter submits, Shift+Enter inserts a newline.
|
|
|
|
---
|
|
|
|
## Push notifications
|
|
|
|
When `Notification.permission === 'default'`, a soft prompt appears above the composer. On approval, `registerPushSubscription(token)` sends the browser's `PushSubscription` to `PUT /vip/push/:token/push-subscription`. The server stores it and sends a Web Push on new inbound Quinn messages.
|
|
|
|
Push state machine: `idle → requesting → granted | denied`. Once `granted` or `denied`, the prompt is hidden.
|
|
|
|
---
|
|
|
|
## Error states
|
|
|
|
| Condition | Behavior |
|
|
|-----------|---------|
|
|
| `invalid_token` on fetch | Sets `invalidToken = true` → portal renders "link no longer valid" |
|
|
| `invalid_token` on send | Same |
|
|
| Network error on fetch | Error card with "Try again" button |
|
|
| Network error on send | `error` state shown inline |
|
|
|
|
---
|
|
|
|
## Non-obvious details
|
|
|
|
- `bottomRef` div at the end of the message list is scrolled into view whenever messages change **and** the messages tab is active — prevents unwanted scroll when on other tabs.
|
|
- Messages tab is the only tab with a `<form>` composer and a push prompt — other tabs just render their content in the `flex: 1` area between header and nav.
|