feat(messaging-ui): presence + live-unread adapter seams

Add optional MessagingAdapter.setFocus(threadId) and subscribeActivity(cb)
so hosts can report the foregrounded thread (backend suppresses push for it)
and drive live conversation-list refresh on any thread's activity. Messenger
wires focus/blur presence + activity-driven refetch. Published as 0.1.7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 15:26:07 +05:30
parent d1d4b56201
commit 745a23823a
3 changed files with 33 additions and 1 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@insignia/iios-messaging-ui",
"version": "0.1.6",
"version": "0.1.7",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
@@ -39,6 +39,15 @@ export interface MessagingAdapter {
markRead(threadId: string, messageId: string): Promise<void>;
/** Report which thread is in the foreground (null when none/blurred) so the backend can suppress
* push notifications for a thread you're actively viewing. Absent => presence isn't tracked. */
setFocus?(threadId: string | null): void;
/** Subscribe to activity across ALL of the caller's threads (not just the open one) — fires for
* every incoming message. Drives live unread + in-app notifications. Absent => no cross-thread
* awareness. Returns an unsubscribe fn. */
subscribeActivity?(cb: (e: { threadId: string; message: Message }) => void): Unsubscribe;
/**
* The current user's actor id, or null if not yet known.
*
@@ -40,6 +40,29 @@ export function Messenger({ focusThreadId }: { focusThreadId?: string | null } =
setSelected(focusThreadId);
}, [focusThreadId]);
// Presence: tell the backend which thread is foregrounded so it suppresses push for it. Null while
// browsing/composing, when the window is blurred, and on unmount (leaving the messenger).
useEffect(() => {
const active = browsing || composing ? null : selected;
adapter.setFocus?.(active);
const onBlur = (): void => adapter.setFocus?.(null);
const onFocus = (): void => adapter.setFocus?.(active);
window.addEventListener('blur', onBlur);
window.addEventListener('focus', onFocus);
return () => {
adapter.setFocus?.(null);
window.removeEventListener('blur', onBlur);
window.removeEventListener('focus', onFocus);
};
}, [adapter, selected, browsing, composing]);
// Live unread + ordering: refresh the conversation list when any of the caller's threads gets a
// message (not just the open one).
useEffect(() => {
if (!adapter.subscribeActivity) return;
return adapter.subscribeActivity(() => refetch());
}, [adapter, refetch]);
const channels = useMemo(() => conversations.filter((c) => c.membership === 'channel'), [conversations]);
const dms = useMemo(() => conversations.filter((c) => c.membership !== 'channel'), [conversations]);
const selectedConversation = useMemo(() => conversations.find((c) => c.threadId === selected) ?? null, [conversations, selected]);