From 745a23823a41c8f28f0a028d12e863c9c48d6580 Mon Sep 17 00:00:00 2001 From: maaz519 Date: Thu, 23 Jul 2026 15:26:07 +0530 Subject: [PATCH] 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 --- packages/iios-messaging-ui/package.json | 2 +- packages/iios-messaging-ui/src/adapter.ts | 9 ++++++++ .../src/components/messenger.tsx | 23 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/iios-messaging-ui/package.json b/packages/iios-messaging-ui/package.json index 3274af2..0b7dcf3 100644 --- a/packages/iios-messaging-ui/package.json +++ b/packages/iios-messaging-ui/package.json @@ -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", diff --git a/packages/iios-messaging-ui/src/adapter.ts b/packages/iios-messaging-ui/src/adapter.ts index 3c72ace..0f7ca27 100644 --- a/packages/iios-messaging-ui/src/adapter.ts +++ b/packages/iios-messaging-ui/src/adapter.ts @@ -39,6 +39,15 @@ export interface MessagingAdapter { markRead(threadId: string, messageId: string): Promise; + /** 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. * diff --git a/packages/iios-messaging-ui/src/components/messenger.tsx b/packages/iios-messaging-ui/src/components/messenger.tsx index dd2effc..e9e0501 100644 --- a/packages/iios-messaging-ui/src/components/messenger.tsx +++ b/packages/iios-messaging-ui/src/components/messenger.tsx @@ -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]);