import { CHAT_PERMISSIONS } from "@hivegrid/chat"; import { buildChatMessageDeepLink } from "@hivegrid/chat/deep-links"; import { listChatMessageDiscovery, type ChatMessageDiscoveryRecord } from "@hivegrid/chat/message-discovery.server"; import { buildChatMessageDiscoveryVM, type ChatMessageDiscoveryMode, type ChatMessageDiscoveryVM } from "@hivegrid/view-models"; import { requireChatPermission, type ChatRouteContext } from "../routes/chatAccess.server"; import { chatIndexPath, chatMentionsPath, chatSearchPath } from "../routes/chatRoutePaths"; export type ChatMessageDiscoveryLabels = Readonly<{ directMessage: string; unnamedGroup: string; unnamedProject: string; unknownUser: string; }>; export async function loadChatMessageDiscovery(input: { readonly context: ChatRouteContext; readonly tenantSlug: string; readonly mode: ChatMessageDiscoveryMode; readonly query?: string; readonly beforeCursor?: string; readonly limit?: number; readonly labels: ChatMessageDiscoveryLabels; }): Promise { const context = requireChatPermission(input.context, CHAT_PERMISSIONS.readOwnChannels); const result = await listChatMessageDiscovery({ tenantId: context.tenantId, userId: context.userId, permissions: context.permissions, mode: input.mode, ...(input.query !== undefined ? { query: input.query } : {}), ...(input.beforeCursor ? { beforeCursor: input.beforeCursor } : {}), ...(input.limit !== undefined ? { limit: input.limit } : {}), }); return buildChatMessageDiscoveryVM({ mode: input.mode, query: input.query, nextCursor: result.nextCursor, hrefs: { chat: chatIndexPath(input.tenantSlug), search: chatSearchPath(input.tenantSlug), mentions: chatMentionsPath(input.tenantSlug), }, rows: result.messages.map((message) => toRow(message, context.userId, input.tenantSlug, input.labels)), }); } function toRow(message: ChatMessageDiscoveryRecord, currentUserId: string, tenantSlug: string, labels: ChatMessageDiscoveryLabels) { const author = message.participants.find((participant) => participant.userId === message.authorUserId); const authorLabel = author?.displayName ?? labels.unknownUser; const channelTitle = resolveChannelTitle(message, currentUserId, labels); return { id: message.id, chatId: message.chatId, channelKind: message.channelKind, channelTitle, authorLabel, authorAvatar: { imageUrl: author?.avatarUrl ?? null, fallbackLabel: initials(authorLabel), }, body: message.body, createdAtIso: message.createdAtIso, detailHref: buildChatMessageDeepLink({ tenantSlug, chatId: message.chatId, messageId: message.id, parentMessageId: message.parentMessageId, }), isThreadReply: message.parentMessageId !== null, } as const; } function resolveChannelTitle(message: ChatMessageDiscoveryRecord, currentUserId: string, labels: ChatMessageDiscoveryLabels): string { if (message.channelKind === "dm") { return message.participants.find((participant) => participant.userId !== currentUserId)?.displayName ?? labels.directMessage; } return message.channelName ?? (message.channelKind === "project" ? labels.unnamedProject : labels.unnamedGroup); } function initials(label: string): string { const normalized = label.trim(); const parts = normalized.split(/\s+/).filter(Boolean); return (parts.length > 1 ? `${parts[0]?.[0] ?? ""}${parts.at(-1)?.[0] ?? ""}` : normalized.slice(0, 2)).toUpperCase() || "?"; }