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]);