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