feat(sdk): P2.6 @insignia/iios-message-web (React hooks) + allowlist boundary
MessageProvider/useThread/useMessages over the kernel-client socket (live append, typing, dedupe). Reconnect test (re-opens active thread). Rewrote import-boundary as an allowlist so SDK<->service sibling imports are forbidden both ways. 28 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export { MessageProvider, useThread, useMessages } from './react';
|
||||
export type { Message } from '@insignia/iios-kernel-client';
|
||||
@@ -0,0 +1,101 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { MessageSocket, type Message } from '@insignia/iios-kernel-client';
|
||||
|
||||
interface MessageContextValue {
|
||||
socket: MessageSocket;
|
||||
}
|
||||
|
||||
const MessageContext = createContext<MessageContextValue | null>(null);
|
||||
|
||||
/** Wraps a MessageSocket and provides it to the message hooks. */
|
||||
export function MessageProvider({
|
||||
serviceUrl,
|
||||
token,
|
||||
children,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
token: string;
|
||||
children: React.ReactNode;
|
||||
}): React.ReactElement {
|
||||
const socket = useMemo(() => new MessageSocket({ serviceUrl, token }), [serviceUrl, token]);
|
||||
useEffect(() => () => socket.disconnect(), [socket]);
|
||||
return <MessageContext.Provider value={{ socket }}>{children}</MessageContext.Provider>;
|
||||
}
|
||||
|
||||
function useSocket(): MessageSocket {
|
||||
const ctx = useContext(MessageContext);
|
||||
if (!ctx) throw new Error('useThread/useMessages must be used within <MessageProvider>');
|
||||
return ctx.socket;
|
||||
}
|
||||
|
||||
/** Open or create a thread; returns the resolved thread id + status. */
|
||||
export function useThread(): {
|
||||
threadId: string | null;
|
||||
status: string;
|
||||
open: (id?: string) => Promise<string>;
|
||||
} {
|
||||
const socket = useSocket();
|
||||
const [threadId, setThreadId] = useState<string | null>(null);
|
||||
const [status, setStatus] = useState('OPEN');
|
||||
|
||||
const open = async (id?: string): Promise<string> => {
|
||||
const r = await socket.openThread(id);
|
||||
setThreadId(r.threadId);
|
||||
setStatus(r.status);
|
||||
return r.threadId;
|
||||
};
|
||||
|
||||
return { threadId, status, open };
|
||||
}
|
||||
|
||||
/** Live messages for a thread + send/read/typing actions. */
|
||||
export function useMessages(threadId: string | null): {
|
||||
messages: Message[];
|
||||
send: (content: string, opts?: { contentRef?: string }) => Promise<void>;
|
||||
markRead: (interactionId: string) => Promise<void>;
|
||||
typing: () => void;
|
||||
typingUsers: string[];
|
||||
} {
|
||||
const socket = useSocket();
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [typingUsers, setTypingUsers] = useState<string[]>([]);
|
||||
const seen = useRef<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
if (!threadId) return;
|
||||
seen.current = new Set();
|
||||
|
||||
void socket.openThread(threadId).then((r) => {
|
||||
seen.current = new Set(r.history.map((m) => m.id));
|
||||
setMessages(r.history);
|
||||
});
|
||||
|
||||
const offMessage = socket.on('message', (m) => {
|
||||
if (m.threadId !== threadId || seen.current.has(m.id)) return;
|
||||
seen.current.add(m.id);
|
||||
setMessages((prev) => [...prev, m]);
|
||||
});
|
||||
const offTyping = socket.on('typing', (e) => {
|
||||
if (e.threadId !== threadId) return;
|
||||
setTypingUsers((u) => (u.includes(e.userId) ? u : [...u, e.userId]));
|
||||
setTimeout(() => setTypingUsers((u) => u.filter((x) => x !== e.userId)), 2500);
|
||||
});
|
||||
|
||||
return () => {
|
||||
offMessage();
|
||||
offTyping();
|
||||
};
|
||||
}, [socket, threadId]);
|
||||
|
||||
const send = async (content: string, opts?: { contentRef?: string }): Promise<void> => {
|
||||
if (threadId) await socket.sendMessage(threadId, content, opts);
|
||||
};
|
||||
const markRead = async (interactionId: string): Promise<void> => {
|
||||
if (threadId) await socket.markRead(threadId, interactionId);
|
||||
};
|
||||
const typing = (): void => {
|
||||
if (threadId) socket.typing(threadId);
|
||||
};
|
||||
|
||||
return { messages, send, markRead, typing, typingUsers };
|
||||
}
|
||||
Reference in New Issue
Block a user