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:
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user