feat(demo): P2.7 Vite React two-pane realtime demo + dev token endpoint (P2 complete)

apps/message-demo: Alice creates a thread, Bob joins; live two-way chat, typing,
read receipts (useMessages now exposes reads[]). Dev-only /v1/dev/token endpoint
(gated by IIOS_DEV_TOKENS) so the browser can auth. .env autoloaded (dotenv).
Realtime smoke script passes end-to-end (message + unread + receipt).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 01:36:14 +05:30
parent eff3c615d5
commit 4d05f7c1b1
14 changed files with 960 additions and 3 deletions
+7 -1
View File
@@ -55,10 +55,12 @@ export function useMessages(threadId: string | null): {
markRead: (interactionId: string) => Promise<void>;
typing: () => void;
typingUsers: string[];
reads: string[];
} {
const socket = useSocket();
const [messages, setMessages] = useState<Message[]>([]);
const [typingUsers, setTypingUsers] = useState<string[]>([]);
const [reads, setReads] = useState<string[]>([]);
const seen = useRef<Set<string>>(new Set());
useEffect(() => {
@@ -80,10 +82,14 @@ export function useMessages(threadId: string | null): {
setTypingUsers((u) => (u.includes(e.userId) ? u : [...u, e.userId]));
setTimeout(() => setTypingUsers((u) => u.filter((x) => x !== e.userId)), 2500);
});
const offReceipt = socket.on('receipt', (e) => {
if (e.kind === 'READ') setReads((r) => (r.includes(e.interactionId) ? r : [...r, e.interactionId]));
});
return () => {
offMessage();
offTyping();
offReceipt();
};
}, [socket, threadId]);
@@ -97,5 +103,5 @@ export function useMessages(threadId: string | null): {
if (threadId) socket.typing(threadId);
};
return { messages, send, markRead, typing, typingUsers };
return { messages, send, markRead, typing, typingUsers, reads };
}