feat(messaging-ui): focusThreadId on Messenger + Inbox for search deep-links (0.1.6)

A host can pass focusThreadId to select/open a specific conversation — used by the
CRM's global search to jump to the exact thread. Mail switches to Open + selects the
matching item; messenger selects the thread. (Source for the already-published 0.1.6.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 14:35:22 +05:30
parent f5c89159f4
commit b3eb027071
3 changed files with 25 additions and 4 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@insignia/iios-messaging-ui", "name": "@insignia/iios-messaging-ui",
"version": "0.1.5", "version": "0.1.6",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.js", "module": "dist/index.js",
@@ -12,7 +12,7 @@ import { ThreadPane } from './thread-pane';
* with a channel browser when the adapter supports channels. Owns only selection + browse state; * with a channel browser when the adapter supports channels. Owns only selection + browse state;
* all data flows through the injected adapter via the hooks. * all data flows through the injected adapter via the hooks.
*/ */
export function Messenger() { export function Messenger({ focusThreadId }: { focusThreadId?: string | null } = {}) {
const { conversations, loading, error, refetch } = useConversations(); const { conversations, loading, error, refetch } = useConversations();
const adapter = useAdapter(); const adapter = useAdapter();
const channelsSupported = typeof adapter.browseChannels === 'function'; const channelsSupported = typeof adapter.browseChannels === 'function';
@@ -32,6 +32,14 @@ export function Messenger() {
// Switching conversations (or into browse/compose) closes any open thread pane. // Switching conversations (or into browse/compose) closes any open thread pane.
useEffect(() => setActiveRoot(null), [selected, browsing, composing]); 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]);
const channels = useMemo(() => conversations.filter((c) => c.membership === 'channel'), [conversations]); const channels = useMemo(() => conversations.filter((c) => c.membership === 'channel'), [conversations]);
const dms = 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]); const selectedConversation = useMemo(() => conversations.find((c) => c.threadId === selected) ?? null, [conversations, selected]);
+15 -2
View File
@@ -32,17 +32,30 @@ const timeOf = (iso?: string): string => {
}; };
/** The unified inbox: work items + mail in one list; click a threaded row to read + reply. */ /** The unified inbox: work items + mail in one list; click a threaded row to read + reply. */
export function Inbox() { export function Inbox({ focusThreadId }: { focusThreadId?: string | null } = {}) {
const [filter, setFilter] = useState<InboxState>('OPEN'); const [filter, setFilter] = useState<InboxState>('OPEN');
const { items, loading, error, transition, refetch } = useInbox(filter); const { items, loading, error, transition, refetch } = useInbox(filter);
const compose = useCompose(); const compose = useCompose();
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const [composing, setComposing] = useState(false); const [composing, setComposing] = useState(false);
// Deep link (e.g. from global search): mail lives in the Open view, so switch there and let the
// selection effect pick the matching thread once the list loads.
useEffect(() => { useEffect(() => {
if (focusThreadId) setFilter('OPEN');
}, [focusThreadId]);
useEffect(() => {
if (focusThreadId) {
const hit = items.find((i) => i.threadId === focusThreadId);
if (hit) {
setSelectedId(hit.id);
return;
}
}
if (selectedId && items.some((i) => i.id === selectedId)) return; if (selectedId && items.some((i) => i.id === selectedId)) return;
setSelectedId(items[0]?.id ?? null); setSelectedId(items[0]?.id ?? null);
}, [items, selectedId]); }, [items, selectedId, focusThreadId]);
const selected = items.find((i) => i.id === selectedId) ?? null; const selected = items.find((i) => i.id === selectedId) ?? null;