import { useEffect, useMemo, useState } from 'react'; import { useConversations } from '../hooks/use-conversations'; import { useAdapter } from '../provider'; import { ConversationList } from './conversation-list'; import { ChannelBrowser } from './channel-browser'; import { NewConversation } from './new-conversation'; import { Thread } from './thread'; import { ThreadPane } from './thread-pane'; /** * The drop-in messenger: sectioned conversation list (Channels / Direct messages) + open thread, * with a channel browser when the adapter supports channels. Owns only selection + browse state; * all data flows through the injected adapter via the hooks. */ export function Messenger({ focusThreadId }: { focusThreadId?: string | null } = {}) { const { conversations, loading, error, refetch } = useConversations(); const adapter = useAdapter(); const channelsSupported = typeof adapter.browseChannels === 'function'; const directorySupported = typeof adapter.directory === 'function'; const [selected, setSelected] = useState(null); const [browsing, setBrowsing] = useState(false); const [composing, setComposing] = useState(false); // "New message" people picker open const [activeRoot, setActiveRoot] = useState(null); // open thread pane's root message useEffect(() => { if (browsing || composing) return; if (selected && conversations.some((c) => c.threadId === selected)) return; setSelected(conversations[0]?.threadId ?? null); }, [conversations, selected, browsing, composing]); // Switching conversations (or into browse/compose) closes any open thread pane. useEffect(() => setActiveRoot(null), [selected, browsing, composing]); // Deep link (e.g. from global search): focus the requested conversation. useEffect(() => { if (!focusThreadId) return; setBrowsing(false); setComposing(false); 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]); function pick(threadId: string): void { setBrowsing(false); setComposing(false); setSelected(threadId); } function openBrowse(): void { setComposing(false); setBrowsing(true); } function openCompose(): void { setBrowsing(false); setComposing(true); } return (
{browsing ? ( { refetch(); pick(threadId); }} /> ) : composing ? ( { refetch(); pick(threadId); }} /> ) : ( { refetch(); setSelected(null); }} /> )}
{!browsing && !composing && selected && activeRoot ? ( setActiveRoot(null)} /> ) : null}
); }