use-slack-item-avatars.ts3.3 KBView on GitHub import { useMemo } from 'react';
import { useQueries } from '@tanstack/react-query';
import type { InboxItem } from '@/modules/inbox/types';
import { useTRPC } from '@/providers/query-provider';
type SlackPrincipal = { displayName?: string; avatarUrl?: string } | null;
/** Matches a Slack user mention token `<@U123>` / `<@W123|fallback>` in message text. */
const MENTION_RE = /<@([UW][A-Z0-9]+)(?:\|[^>]*)?>/g;
export interface SlackItemPrincipals {
/** `(workspaceId, userId) => avatarUrl` — for the row avatar (last sender). */
resolveAvatar: (ws?: string, uid?: string) => string | undefined;
/** `(workspaceId, userId) => displayName` — for decoding `<@U…>` mentions in snippets. */
resolveName: (ws?: string, uid?: string) => string | undefined;
}
/**
* Resolve Slack principals (avatar + display name) for the visible Slack inbox
* rows — batched one `integrations.slack.resolveSlackPrincipals` query per
* workspace (same source the thread view uses). Collects both the last sender's
* `(workspaceId, slackUserId)` (for the avatar) AND every `<@U…>` mention id
* found in each row's snippet (so mentions can be decoded to names). Slack data
* is live (Slack CDN / API), so this is best-effort.
*/
export function useSlackItemPrincipals(items: InboxItem[]): SlackItemPrincipals {
const trpc = useTRPC();
const entries = useMemo(() => {
const byWorkspace = new Map<string, Set<string>>();
const add = (ws: string, uid: string) => {
if (!byWorkspace.has(ws)) byWorkspace.set(ws, new Set());
byWorkspace.get(ws)!.add(uid);
};
for (const it of items) {
if (it.channel !== 'slack' || it.ref.kind !== 'slack') continue;
const ws = it.ref.workspaceId;
if (!ws) continue;
if (it.ref.slackUserId) add(ws, it.ref.slackUserId);
// Pull mention ids out of the snippet so `<@U…>` decodes to a real name.
for (const m of it.snippet.matchAll(MENTION_RE)) add(ws, m[1]);
}
return Array.from(byWorkspace.entries()).map(([workspaceId, ids]) => ({
workspaceId,
userIds: Array.from(ids).sort(),
}));
}, [items]);
const queries = useQueries({
queries: entries.map(({ workspaceId, userIds }) =>
trpc.integrations.slack.resolveSlackPrincipals.queryOptions(
{ workspaceId, userIds },
{ enabled: userIds.length > 0, staleTime: 60 * 60 * 1000, gcTime: 60 * 60 * 1000 },
),
),
});
const { avatarByKey, nameByKey } = useMemo(() => {
const avatarByKey = new Map<string, string>();
const nameByKey = new Map<string, string>();
entries.forEach((entry, idx) => {
const data = queries[idx]?.data as { byUserId?: Record<string, SlackPrincipal> } | undefined;
if (!data?.byUserId) return;
for (const [uid, principal] of Object.entries(data.byUserId)) {
if (principal?.avatarUrl) avatarByKey.set(`${entry.workspaceId}:${uid}`, principal.avatarUrl);
if (principal?.displayName) nameByKey.set(`${entry.workspaceId}:${uid}`, principal.displayName);
}
});
return { avatarByKey, nameByKey };
}, [entries, queries]);
return useMemo(
() => ({
resolveAvatar: (ws, uid) => (ws && uid ? avatarByKey.get(`${ws}:${uid}`) : undefined),
resolveName: (ws, uid) => (ws && uid ? nameByKey.get(`${ws}:${uid}`) : undefined),
}),
[avatarByKey, nameByKey],
);
}