@burdenoff/microfe-conversations 2026.830.2 → 2026.928.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conversations/hooks/useConversationQueries.js +66 -40
- package/dist/conversations/hooks/useConversationQueries.js.map +1 -1
- package/dist/conversations/pages/ConversationDetailPage.js +316 -276
- package/dist/conversations/pages/ConversationDetailPage.js.map +1 -1
- package/dist/conversations/utils/mappers.js +21 -12
- package/dist/conversations/utils/mappers.js.map +1 -1
- package/dist/conversations/utils/readReceiptCache.js +45 -0
- package/dist/conversations/utils/readReceiptCache.js.map +1 -0
- package/dist/generated/wspace-operations.js +90 -63
- package/dist/generated/wspace-operations.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConversationDetailPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationDetailPage.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport { isQuotaExhaustedError } from \"@burdenoff/fe-libs/shared/utils\";\nimport { useNavigate, useParams } from \"react-router\";\nimport {\n Archive,\n ArrowLeft,\n BellOff,\n Bell,\n ExternalLink,\n Hash,\n LogOut,\n Loader2,\n MoreVertical,\n Pin,\n PinOff,\n SendHorizontal,\n Users,\n X,\n} from \"lucide-react\";\nimport {\n Button,\n IllustratedEmptyState,\n NextSteps,\n type NextStep,\n} from \"@burdenoff/fe-libs/ui\";\nimport { MessageBubble } from \"../components/MessageBubble\";\nimport {\n useGetConversation,\n useConversationActions,\n} from \"../hooks/useConversationQueries\";\nimport {\n useGetMessages,\n useSendMessage,\n useEditMessage,\n useMessageActions,\n} from \"../hooks/useMessageQueries\";\nimport { useMessageStreamForConversation } from \"../hooks/useMessageSubscriptions\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport { cn } from \"../utils/cn\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { QuotaExhaustedDialog } from \"../components/QuotaExhaustedDialog\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { nativeImpact, nativeNotify } from \"../../utils/nativeBridge\";\nimport { useTr } from \"../utils/i18n\";\nimport type { Message } from \"../types\";\n\nconst MESSAGES_PAGE_SIZE = 100;\nconst MESSAGE_REFRESH_FALLBACK_MS = 30_000;\n\nexport function ConversationDetailPage() {\n const { conversationId } = useParams<{ conversationId: string }>();\n const navigate = useNavigate();\n const { basePath, currentUser } = useConversations();\n const bus = useEventBus();\n const tr = useTr();\n const {\n clearMessageDraft,\n isParticipantsPanelOpen,\n messageDrafts,\n setMessageDraft,\n toggleParticipantsPanel,\n } = useConversationsStore();\n\n const [messageInput, setMessageInput] = useState(\n messageDrafts[conversationId ?? \"\"] ?? \"\",\n );\n const [showMessageQuotaExhausted, setShowMessageQuotaExhausted] =\n useState(false);\n const [locallySentMessages, setLocallySentMessages] = useState<Message[]>([]);\n const messagesEndRef = useRef<HTMLDivElement>(null);\n const composerRef = useRef<HTMLTextAreaElement>(null);\n\n const focusComposer = useCallback(() => {\n composerRef.current?.focus();\n composerRef.current?.scrollIntoView({ behavior: \"smooth\" });\n }, []);\n\n const {\n conversation,\n members: conversationMembers,\n membersTotalCount,\n loading: conversationLoading,\n error: conversationError,\n } = useGetConversation(conversationId ?? \"\");\n const {\n messages,\n loading: messagesLoading,\n error: messagesError,\n refetch: refetchMessages,\n } = useGetMessages(conversationId ?? \"\", {\n first: MESSAGES_PAGE_SIZE,\n pollInterval: MESSAGE_REFRESH_FALLBACK_MS,\n });\n // Subscribe to live message events; the query has a light polling fallback for\n // local/federated transports that keep the stream open but miss events.\n useMessageStreamForConversation(conversationId, MESSAGES_PAGE_SIZE);\n const { sendMessage, loading: sendingMessage } = useSendMessage();\n const { editMessage, loading: editingMessage } = useEditMessage();\n const {\n deleteMessage,\n addReaction,\n removeReaction,\n pinMessage,\n unpinMessage,\n } = useMessageActions();\n const {\n archiveConversation,\n pinConversation,\n unpinConversation,\n muteConversation,\n unmuteConversation,\n leaveConversation,\n } = useConversationActions();\n\n const [moreMenuOpen, setMoreMenuOpen] = useState(false);\n const [editingMessageId, setEditingMessageId] = useState<string | null>(null);\n const [editingContent, setEditingContent] = useState(\"\");\n const [replyingTo, setReplyingTo] = useState<{\n id: string;\n content: string;\n sender: string;\n } | null>(null);\n const moreMenuRef = useRef<HTMLDivElement>(null);\n\n const currentUserId = currentUser?.id ?? \"\";\n const sourceContext = conversation?.sourceContext;\n\n const displayedMessages = useMemo(() => {\n const byId = new Map<string, Message>();\n for (const message of locallySentMessages) {\n if (message.conversationId !== conversationId) continue;\n byId.set(message.id, message);\n }\n for (const message of messages) {\n byId.set(message.id, message);\n }\n return Array.from(byId.values()).sort(\n (a, b) =>\n new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),\n );\n }, [conversationId, locallySentMessages, messages]);\n\n useEffect(() => {\n messagesEndRef.current?.scrollIntoView({ behavior: \"smooth\" });\n }, [displayedMessages.length]);\n\n useEffect(() => {\n setLocallySentMessages([]);\n }, [conversationId]);\n\n useEffect(() => {\n if (locallySentMessages.length === 0 || messages.length === 0) return;\n const serverIds = new Set(messages.map((message) => message.id));\n setLocallySentMessages((current) => {\n const pending = current.filter((message) => !serverIds.has(message.id));\n return pending.length === current.length ? current : pending;\n });\n }, [locallySentMessages.length, messages, setLocallySentMessages]);\n\n useEffect(() => {\n if (!conversationId) return;\n if (messageInput.trim()) {\n setMessageDraft(conversationId, messageInput);\n return;\n }\n clearMessageDraft(conversationId);\n }, [messageInput, conversationId, setMessageDraft, clearMessageDraft]);\n\n const messageAuthorNames = useMemo(() => {\n const byAuthor = new Map<string, string>();\n for (const message of displayedMessages) {\n if (!message.authorId || byAuthor.has(message.authorId)) continue;\n if (!message.sender.name) continue;\n byAuthor.set(message.authorId, message.sender.name);\n }\n return byAuthor;\n }, [displayedMessages]);\n\n const participants = useMemo(() => {\n if (conversationMembers.length > 0) {\n return conversationMembers\n .filter((member) => member.status === \"ACTIVE\" && !member.leftAt)\n .map((member) => ({\n id: member.userId,\n name:\n messageAuthorNames.get(member.userId) ||\n member.customTitle ||\n tr(\"conversations.member\", \"Member\"),\n role: member.role,\n }));\n }\n\n return Array.from(messageAuthorNames.entries()).map(([id, name]) => ({\n id,\n name,\n role: tr(\"conversations.member\", \"Member\"),\n }));\n }, [conversationMembers, messageAuthorNames, tr]);\n\n const hiddenParticipantsCount = conversation?.memberCount\n ? Math.max(conversation.memberCount - participants.length, 0)\n : 0;\n\n const handleSendMessage = useCallback(async () => {\n if (!conversationId || !messageInput.trim() || sendingMessage) return;\n let message: Message | null;\n try {\n message = await sendMessage({\n conversationId,\n content: messageInput.trim(),\n type: \"TEXT\",\n parentMessageId: replyingTo?.id,\n refetchFirst: MESSAGES_PAGE_SIZE,\n });\n } catch (error) {\n if (isQuotaExhaustedError(error)) {\n setShowMessageQuotaExhausted(true);\n return;\n }\n nativeNotify(\"error\");\n throw error;\n }\n if (!message) return;\n setLocallySentMessages((current) => [...current, message]);\n bus.emit(\"conversations.message.sent\", {\n messageId: message.id,\n conversationId,\n });\n setMessageInput(\"\");\n setReplyingTo(null);\n clearMessageDraft(conversationId);\n }, [\n bus,\n conversationId,\n messageInput,\n sendingMessage,\n sendMessage,\n clearMessageDraft,\n replyingTo,\n ]);\n\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (event.key !== \"Enter\" || event.shiftKey) return;\n event.preventDefault();\n void handleSendMessage();\n },\n [handleSendMessage],\n );\n\n // Emit opened event so sinks (analytics/audit) can record the view\n useEffect(() => {\n if (!conversationId) return;\n bus.emit(\"conversations.conversation.opened\", { conversationId });\n }, [bus, conversationId]);\n\n // Close more-menu on outside click\n useEffect(() => {\n if (!moreMenuOpen) return;\n const handler = (e: MouseEvent) => {\n if (\n moreMenuRef.current &&\n !moreMenuRef.current.contains(e.target as Node)\n ) {\n setMoreMenuOpen(false);\n }\n };\n document.addEventListener(\"mousedown\", handler);\n return () => document.removeEventListener(\"mousedown\", handler);\n }, [moreMenuOpen]);\n\n const handleReply = useCallback(\n (message: { id: string; content: string; sender: { name: string } }) => {\n setReplyingTo({\n id: message.id,\n content: message.content,\n sender: message.sender.name,\n });\n },\n [],\n );\n\n const handleReact = useCallback(\n async (messageId: string, emoji: string) => {\n try {\n await addReaction(messageId, emoji);\n } catch {\n // If already reacted, toggle off\n await removeReaction(messageId, emoji).catch(() => {});\n }\n },\n [addReaction, removeReaction],\n );\n\n const handleEdit = useCallback((message: { id: string; content: string }) => {\n setEditingMessageId(message.id);\n setEditingContent(message.content);\n }, []);\n\n const handleEditSubmit = useCallback(async () => {\n if (!editingMessageId || !editingContent.trim() || editingMessage) return;\n await editMessage(editingMessageId, editingContent.trim());\n setEditingMessageId(null);\n setEditingContent(\"\");\n }, [editingMessageId, editingContent, editingMessage, editMessage]);\n\n const handleEditCancel = useCallback(() => {\n setEditingMessageId(null);\n setEditingContent(\"\");\n }, []);\n\n const handleDelete = useCallback(\n async (messageId: string) => {\n const confirmed = window.confirm(\n tr(\n \"conversations.confirmDeleteMessage\",\n \"Delete this message? This action cannot be undone.\",\n ),\n );\n if (!confirmed) return;\n nativeImpact(\"medium\");\n try {\n await deleteMessage(messageId);\n void refetchMessages();\n nativeNotify(\"success\");\n } catch (error) {\n console.error(\"[microfe-conversations] message delete failed\", error);\n nativeNotify(\"error\");\n }\n },\n [deleteMessage, refetchMessages, tr],\n );\n\n const handleTogglePinMessage = useCallback(\n async (messageId: string, isPinned: boolean) => {\n if (isPinned) {\n await unpinMessage(messageId);\n return;\n }\n await pinMessage(messageId);\n },\n [pinMessage, unpinMessage],\n );\n\n const handlePin = useCallback(async () => {\n if (!conversationId) return;\n if (conversation?.isPinned) {\n await unpinConversation(conversationId);\n } else {\n await pinConversation(conversationId);\n }\n setMoreMenuOpen(false);\n }, [\n conversationId,\n conversation?.isPinned,\n pinConversation,\n unpinConversation,\n ]);\n\n const handleMute = useCallback(async () => {\n if (!conversationId) return;\n if (conversation?.isMuted) {\n await unmuteConversation(conversationId);\n } else {\n await muteConversation(conversationId);\n }\n setMoreMenuOpen(false);\n }, [\n conversationId,\n conversation?.isMuted,\n muteConversation,\n unmuteConversation,\n ]);\n\n const handleArchive = useCallback(async () => {\n if (!conversationId) return;\n try {\n await archiveConversation(conversationId);\n bus.emit(\"conversations.conversation.archived\", { conversationId });\n setMoreMenuOpen(false);\n navigate(basePath || \"/conversations\");\n } catch (error) {\n console.error(\n \"[microfe-conversations] conversation archive failed\",\n error,\n );\n nativeNotify(\"error\");\n }\n }, [archiveConversation, basePath, bus, conversationId, navigate]);\n\n const handleLeave = useCallback(async () => {\n if (!conversationId) return;\n const confirmed = window.confirm(\n tr(\n \"conversations.confirmLeaveConversation\",\n \"Leave this conversation? You will lose access unless someone adds you again.\",\n ),\n );\n if (!confirmed) return;\n try {\n await leaveConversation(conversationId);\n if (currentUserId) {\n bus.emit(\"conversations.member.removed\", {\n conversationId,\n userId: currentUserId,\n });\n }\n setMoreMenuOpen(false);\n navigate(basePath || \"/conversations\");\n } catch (error) {\n console.error(\"[microfe-conversations] conversation leave failed\", error);\n nativeNotify(\"error\");\n }\n }, [\n basePath,\n bus,\n conversationId,\n currentUserId,\n leaveConversation,\n navigate,\n tr,\n ]);\n\n const handleOpenContext = useCallback(() => {\n if (!sourceContext?.contextHref) return;\n try {\n const url = new URL(sourceContext.contextHref);\n if (url.origin === window.location.origin) {\n navigate(`${url.pathname}${url.search}${url.hash}`);\n return;\n }\n } catch {\n // Fall through to full navigation for relative or invalid URLs.\n }\n\n window.location.assign(sourceContext.contextHref);\n }, [navigate, sourceContext?.contextHref]);\n\n // Concrete next actions surfaced on the (complex) detail page so a new user\n // isn't left staring at an unfamiliar surface. Each step is wired to a real\n // handler already present on this page; steps that don't apply to the\n // current conversation type/state are filtered out.\n const detailNextSteps = useMemo<NextStep[]>(() => {\n const steps: NextStep[] = [\n {\n id: \"send-message\",\n label: tr(\n \"conversations.nextSteps.send.label\",\n \"Send your first reply\",\n ),\n description: tr(\n \"conversations.nextSteps.send.description\",\n \"Jump to the composer and start the conversation.\",\n ),\n icon: <SendHorizontal size={14} aria-hidden=\"true\" />,\n onClick: focusComposer,\n done: displayedMessages.length > 0,\n },\n ];\n if (conversation?.isGroup) {\n steps.push({\n id: \"view-participants\",\n label: tr(\n \"conversations.nextSteps.participants.label\",\n \"Review participants\",\n ),\n description: tr(\n \"conversations.nextSteps.participants.description\",\n \"See who can read and post in this conversation.\",\n ),\n icon: <Users size={14} aria-hidden=\"true\" />,\n onClick: () => {\n if (!isParticipantsPanelOpen) toggleParticipantsPanel();\n },\n });\n }\n if (sourceContext?.contextHref) {\n steps.push({\n id: \"open-context\",\n label: tr(\n \"conversations.nextSteps.context.label\",\n \"Open the related item\",\n ),\n description: tr(\n \"conversations.nextSteps.context.description\",\n \"Jump to the page or record this conversation is about.\",\n ),\n icon: <ExternalLink size={14} aria-hidden=\"true\" />,\n onClick: handleOpenContext,\n });\n }\n steps.push({\n id: \"pin-conversation\",\n label: conversation?.isPinned\n ? tr(\"conversations.nextSteps.unpin.label\", \"Unpin this conversation\")\n : tr(\"conversations.nextSteps.pin.label\", \"Pin for quick access\"),\n description: tr(\n \"conversations.nextSteps.pin.description\",\n \"Keep important conversations at the top of your inbox.\",\n ),\n icon: <Pin size={14} aria-hidden=\"true\" />,\n onClick: () => void handlePin(),\n done: conversation?.isPinned,\n });\n return steps;\n }, [\n conversation?.isGroup,\n conversation?.isPinned,\n displayedMessages.length,\n focusComposer,\n handleOpenContext,\n handlePin,\n isParticipantsPanelOpen,\n sourceContext?.contextHref,\n toggleParticipantsPanel,\n tr,\n ]);\n\n if (!conversation && conversationLoading) {\n return (\n <div className=\"flex items-center justify-center h-[calc(100vh-64px)] text-sm text-text-secondary\">\n {tr(\"conversations.loading\", \"Loading conversation...\")}\n </div>\n );\n }\n\n if (!conversation || conversationError) {\n return (\n <div className=\"flex h-[calc(100vh-64px)] items-center justify-center p-8\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={\n conversationError\n ? tr(\"conversations.unavailableTitle\", \"Conversation unavailable\")\n : tr(\"conversations.notFound\", \"Conversation not found\")\n }\n description={\n conversationError\n ? tr(\n \"conversations.unavailable\",\n \"We couldn't load this conversation. Please try again.\",\n )\n : tr(\n \"conversations.notFoundDescription\",\n \"This conversation may have been deleted, archived, or you no longer have access to it.\",\n )\n }\n action={\n <Button\n type=\"button\"\n onClick={() => navigate(basePath || \"/conversations\")}\n >\n {tr(\"conversations.back\", \"Back to conversations\")}\n </Button>\n }\n />\n </div>\n );\n }\n\n return (\n <div className=\"relative flex h-[calc(100vh-64px)] bg-bg-surface\">\n <div className=\"flex-1 flex flex-col min-w-0\">\n <header\n data-tour=\"conversation-detail-header\"\n className=\"flex items-center justify-between px-4 py-3 border-b border-border-seam bg-bg-surface/95 backdrop-blur-sm\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <button\n type=\"button\"\n onClick={() => navigate(basePath || \"/conversations\")}\n className=\"flex min-h-10 min-w-10 items-center justify-center rounded-lg text-text-secondary transition-colors hover:bg-bg-sunken/50 hover:text-text-primary\"\n >\n <ArrowLeft size={18} />\n </button>\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-1.5\">\n {conversation.type === \"CHANNEL\" && (\n <Hash\n size={14}\n className=\"text-text-secondary flex-shrink-0\"\n />\n )}\n <h2 className=\"text-sm font-semibold text-text-primary truncate\">\n {conversation.title}\n </h2>\n </div>\n <p className=\"text-xs text-text-secondary\">\n {conversation.type === \"DIRECT\"\n ? tr(\n \"conversations.directConversation\",\n \"Direct conversation\",\n )\n : conversation.type === \"CHANNEL\"\n ? `${tr(\"conversations.channel\", \"Channel\")} · ${conversation.memberCount} ${tr(\"conversations.membersCountLabel\", \"members\")}`\n : conversation.type === \"ANNOUNCEMENT\"\n ? `${tr(\"conversations.announcement\", \"Announcement\")} · ${conversation.memberCount} ${tr(\"conversations.membersCountLabel\", \"members\")}`\n : `${conversation.memberCount} ${tr(\"conversations.membersCountLabel\", \"members\")}`}\n </p>\n </div>\n </div>\n\n <div className=\"flex shrink-0 items-center gap-1\">\n {sourceContext?.contextHref && (\n <HeaderButton\n dataTour=\"conversation-open-context\"\n icon={<ExternalLink size={16} />}\n label={tr(\"conversations.openContext\", \"Open context\")}\n onClick={handleOpenContext}\n />\n )}\n {conversation.isGroup && (\n <HeaderButton\n dataTour=\"conversation-participants-button\"\n icon={<Users size={16} />}\n label={tr(\"conversations.participants\", \"Participants\")}\n active={isParticipantsPanelOpen}\n onClick={toggleParticipantsPanel}\n />\n )}\n <div className=\"relative\" ref={moreMenuRef}>\n <HeaderButton\n icon={<MoreVertical size={16} />}\n label={tr(\"conversations.moreOptions\", \"More options\")}\n active={moreMenuOpen}\n onClick={() => setMoreMenuOpen((prev) => !prev)}\n />\n {moreMenuOpen && (\n <div className=\"absolute right-0 top-full mt-1 w-48 bg-popover border border-border-subtle rounded-lg shadow-lg z-50 py-1 animate-in fade-in-0 zoom-in-95 duration-100\">\n <DropdownItem\n icon={\n conversation.isPinned ? (\n <PinOff size={14} />\n ) : (\n <Pin size={14} />\n )\n }\n label={\n conversation.isPinned\n ? tr(\n \"conversations.unpinConversation\",\n \"Unpin conversation\",\n )\n : tr(\n \"conversations.pinConversation\",\n \"Pin conversation\",\n )\n }\n onClick={() => void handlePin()}\n />\n <DropdownItem\n icon={\n conversation.isMuted ? (\n <Bell size={14} />\n ) : (\n <BellOff size={14} />\n )\n }\n label={\n conversation.isMuted\n ? tr(\n \"conversations.unmuteConversation\",\n \"Unmute conversation\",\n )\n : tr(\n \"conversations.muteConversation\",\n \"Mute conversation\",\n )\n }\n onClick={() => void handleMute()}\n />\n <div className=\"h-px bg-border my-1\" />\n <DropdownItem\n icon={<Archive size={14} />}\n label={tr(\n \"conversations.archiveConversation\",\n \"Archive conversation\",\n )}\n onClick={() => void handleArchive()}\n destructive\n />\n {conversation.isGroup && (\n <DropdownItem\n icon={<LogOut size={14} />}\n label={tr(\n \"conversations.leaveConversation\",\n \"Leave conversation\",\n )}\n onClick={() => void handleLeave()}\n destructive\n />\n )}\n </div>\n )}\n </div>\n </div>\n </header>\n\n <div className=\"flex-1 overflow-y-auto py-4 space-y-0.5\">\n {sourceContext?.pagePath && (\n <div className=\"mx-4 mb-4 rounded-lg border border-border-subtle bg-bg-sunken/20 px-3 py-2 text-xs text-text-secondary\">\n <span className=\"font-medium text-text-primary\">\n {sourceContext.contextTitle || sourceContext.pagePath}\n </span>\n {sourceContext.entityType && sourceContext.entityId\n ? tr(\n \"conversations.contextEntityMeta\",\n \" · {{entityType}} {{entityId}}\",\n {\n entityType: sourceContext.entityType,\n entityId: sourceContext.entityId,\n },\n )\n : \"\"}\n </div>\n )}\n {messagesError && (\n <div className=\"mx-4 mb-4 rounded-lg border border-status-warning-border bg-status-warning-bg px-3 py-2 text-sm text-status-warning-text\">\n {tr(\n \"conversations.messagesUnavailable\",\n \"Messages couldn't be loaded right now. The conversation header and actions are still available.\",\n )}\n </div>\n )}\n {messagesLoading && (\n <div className=\"mx-4 mb-4 flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-sunken/20 px-3 py-2 text-sm text-text-secondary\">\n <Loader2 size={14} className=\"animate-spin\" />\n {tr(\"conversations.loadingMessages\", \"Loading messages...\")}\n </div>\n )}\n {!messagesLoading &&\n !messagesError &&\n displayedMessages.length === 0 && (\n <div className=\"mx-4 mb-4 flex flex-col items-center gap-5 py-6\">\n <IllustratedEmptyState\n illustration=\"empty-vibes\"\n title={tr(\"conversations.noMessagesTitle\", \"No messages yet\")}\n description={tr(\n \"conversations.noMessagesYet\",\n \"Start the conversation below — say hello, ask a question, or share an update.\",\n )}\n action={\n <Button type=\"button\" onClick={focusComposer}>\n <SendHorizontal size={14} aria-hidden=\"true\" />\n {tr(\"conversations.writeFirstMessage\", \"Write a message\")}\n </Button>\n }\n />\n <NextSteps\n className=\"w-full max-w-md\"\n storageKey=\"conversations-detail\"\n title={tr(\n \"conversations.nextSteps.title\",\n \"Get the most out of this conversation\",\n )}\n steps={detailNextSteps}\n />\n </div>\n )}\n {displayedMessages.map((message, index) => {\n const prevMessage = displayedMessages[index - 1];\n const showAvatar =\n !prevMessage ||\n prevMessage.authorId !== message.authorId ||\n prevMessage.type === \"SYSTEM\";\n\n return (\n <div key={message.id}>\n {editingMessageId === message.id ? (\n <div className=\"px-4 py-2\">\n <div className=\"flex items-center gap-2 text-xs text-text-secondary mb-1\">\n <span>\n {tr(\"conversations.editingMessage\", \"Editing message\")}\n </span>\n <button\n type=\"button\"\n onClick={handleEditCancel}\n className=\"hover:text-text-primary\"\n >\n <X size={12} />\n </button>\n </div>\n <div className=\"flex items-end gap-2\">\n <textarea\n aria-label={tr(\n \"conversations.editMessageAria\",\n \"Edit message\",\n )}\n value={editingContent}\n onChange={(e) => setEditingContent(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n void handleEditSubmit();\n }\n if (e.key === \"Escape\") handleEditCancel();\n }}\n className=\"flex-1 px-3 py-2 text-sm rounded-lg border border-border-subtle bg-bg-sunken/30 resize-none focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n rows={2}\n />\n <button\n type=\"button\"\n onClick={() => void handleEditSubmit()}\n disabled={!editingContent.trim() || editingMessage}\n className=\"px-3 py-2 text-xs font-medium rounded-lg bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {tr(\"conversations.save\", \"Save\")}\n </button>\n </div>\n </div>\n ) : (\n <MessageBubble\n message={message}\n isOwnMessage={message.authorId === currentUserId}\n showAvatar={showAvatar}\n showSender={showAvatar}\n onReply={handleReply}\n onReact={(id, emoji) => void handleReact(id, emoji)}\n onEdit={handleEdit}\n onDelete={(id) => void handleDelete(id)}\n onTogglePin={(id, isPinned) =>\n void handleTogglePinMessage(id, isPinned)\n }\n />\n )}\n </div>\n );\n })}\n <div ref={messagesEndRef} />\n </div>\n\n <div\n data-tour=\"conversation-message-composer\"\n className=\"border-t border-border-seam px-4 py-3 bg-bg-surface\"\n >\n {/* Reply-to indicator */}\n {replyingTo && (\n <div className=\"flex items-center gap-2 mb-2 px-3 py-1.5 rounded-lg bg-bg-sunken/50 text-xs text-text-secondary\">\n <span>\n {tr(\"conversations.replyingTo\", \"Replying to\")}{\" \"}\n <strong className=\"text-text-primary\">\n {replyingTo.sender}\n </strong>\n : {replyingTo.content.slice(0, 80)}\n {replyingTo.content.length > 80 ? \"…\" : \"\"}\n </span>\n <button\n type=\"button\"\n onClick={() => setReplyingTo(null)}\n aria-label={tr(\"conversations.cancelReply\", \"Cancel reply\")}\n className=\"ml-auto flex min-h-9 min-w-9 items-center justify-center rounded-md hover:bg-bg-sunken/70 hover:text-text-primary\"\n >\n <X size={12} />\n </button>\n </div>\n )}\n <div className=\"flex items-end gap-2\">\n <div className=\"flex-1 relative\">\n <textarea\n aria-label={tr(\n \"conversations.messageComposer\",\n \"Message composer\",\n )}\n ref={composerRef}\n value={messageInput}\n onChange={(event) => setMessageInput(event.target.value)}\n onKeyDown={handleKeyDown}\n placeholder={`${tr(\"conversations.messageConversationPrefix\", \"Message\")} ${conversation.title}...`}\n rows={1}\n className={cn(\n \"w-full px-4 py-2.5 text-sm rounded-xl border border-border-subtle bg-bg-sunken/30\",\n \"resize-none overflow-hidden placeholder:text-text-secondary\",\n \"focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:border-transparent max-h-32\",\n )}\n style={{\n height: \"auto\",\n minHeight: \"40px\",\n }}\n onInput={(event) => {\n const target = event.target as HTMLTextAreaElement;\n target.style.height = \"auto\";\n target.style.height = `${Math.min(target.scrollHeight, 128)}px`;\n }}\n />\n </div>\n\n <button\n type=\"button\"\n aria-label={tr(\"conversations.sendMessage\", \"Send message\")}\n onClick={() => void handleSendMessage()}\n disabled={!messageInput.trim() || sendingMessage}\n className={cn(\n \"p-2.5 rounded-xl transition-all duration-150\",\n messageInput.trim() && !sendingMessage\n ? \"bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90 shadow-sm\"\n : \"bg-bg-sunken text-text-secondary cursor-not-allowed\",\n )}\n >\n {sendingMessage ? (\n <Loader2 size={18} className=\"animate-spin\" />\n ) : (\n <SendHorizontal size={18} />\n )}\n </button>\n </div>\n </div>\n </div>\n\n {isParticipantsPanelOpen && conversation.isGroup && (\n <aside className=\"absolute inset-y-0 right-0 z-20 w-full max-w-72 border-l border-border-seam bg-bg-surface shadow-lg animate-in slide-in-from-right-4 duration-200 md:relative md:z-auto md:w-64 md:shadow-none\">\n <div className=\"p-4\">\n <div className=\"flex items-center justify-between mb-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {tr(\"conversations.members\", \"Members\")} (\n {conversation.memberCount ?? participants.length})\n </h3>\n <button\n type=\"button\"\n onClick={toggleParticipantsPanel}\n className=\"p-1 rounded text-text-secondary hover:text-text-primary\"\n >\n <Pin size={14} />\n </button>\n </div>\n\n <div className=\"space-y-1\">\n {participants.map((participant) => (\n <div\n key={participant.id}\n className=\"flex items-center gap-2.5 px-2 py-1.5 rounded-lg hover:bg-bg-sunken/50\"\n >\n <div className=\"size-7 rounded-full bg-bg-sunken flex items-center justify-center text-xs font-medium text-text-secondary\">\n {participant.name\n .split(\" \")\n .map((word) => word[0])\n .join(\"\")\n .slice(0, 2)}\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-xs font-medium text-text-primary truncate\">\n {participant.name}\n </p>\n <p className=\"text-[10px] text-text-secondary\">\n {participant.role}\n </p>\n </div>\n </div>\n ))}\n {hiddenParticipantsCount > 0 && (\n <p className=\"px-2 pt-2 text-xs text-text-secondary\">\n {tr(\"conversations.moreMembersPrefix\", \"and\")}{\" \"}\n {hiddenParticipantsCount}{\" \"}\n {tr(\"conversations.moreMembersSuffix\", \"more\")}\n </p>\n )}\n </div>\n </div>\n </aside>\n )}\n <QuotaExhaustedDialog\n open={showMessageQuotaExhausted}\n title={tr(\n \"conversations.quota.messages.title\",\n \"Message Quota Reached\",\n )}\n description={tr(\n \"conversations.quota.messages.description\",\n \"You've reached the message limit for your current plan. View billing to upgrade or add more capacity.\",\n )}\n onClose={() => setShowMessageQuotaExhausted(false)}\n onConfirm={() => {\n setShowMessageQuotaExhausted(false);\n navigate(\"/billing\");\n }}\n />\n </div>\n );\n}\n\nfunction DropdownItem({\n icon,\n label,\n onClick,\n destructive,\n}: {\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n destructive?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className={cn(\n \"flex items-center gap-2.5 w-full px-3 py-2 text-sm transition-colors text-left\",\n destructive\n ? \"text-status-error-text hover:bg-status-error-bg/10\"\n : \"text-text-primary hover:bg-bg-sunken/50\",\n )}\n >\n {icon}\n <span>{label}</span>\n </button>\n );\n}\n\nfunction HeaderButton({\n icon,\n label,\n active,\n dataTour,\n onClick,\n}: {\n icon: React.ReactNode;\n label: string;\n active?: boolean;\n dataTour?: string;\n onClick?: () => void;\n}) {\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <button\n data-tour={dataTour}\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n title={label}\n className={cn(\n \"flex min-h-10 min-w-10 items-center justify-center rounded-lg transition-colors\",\n active\n ? \"bg-action-primary-bg/10 text-primary\"\n : \"text-text-secondary hover:text-text-primary hover:bg-bg-sunken/50\",\n )}\n >\n {icon}\n </button>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ConversationDetailPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationDetailPage.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport { isQuotaExhaustedError } from \"@burdenoff/fe-libs/shared/utils\";\nimport { useNavigate, useParams } from \"react-router\";\nimport {\n Archive,\n ArrowLeft,\n BellOff,\n Bell,\n ExternalLink,\n Hash,\n LogOut,\n Loader2,\n MoreVertical,\n Pin,\n PinOff,\n SendHorizontal,\n Users,\n X,\n} from \"lucide-react\";\nimport {\n Button,\n IllustratedEmptyState,\n NextSteps,\n type NextStep,\n} from \"@burdenoff/fe-libs/ui\";\nimport { MessageBubble } from \"../components/MessageBubble\";\nimport {\n useGetConversation,\n useConversationActions,\n useMarkConversationAsRead,\n} from \"../hooks/useConversationQueries\";\nimport {\n useGetMessages,\n useSendMessage,\n useEditMessage,\n useMessageActions,\n} from \"../hooks/useMessageQueries\";\nimport { useMessageStreamForConversation } from \"../hooks/useMessageSubscriptions\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport { cn } from \"../utils/cn\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { QuotaExhaustedDialog } from \"../components/QuotaExhaustedDialog\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { nativeImpact, nativeNotify } from \"../../utils/nativeBridge\";\nimport { useTr } from \"../utils/i18n\";\nimport type { Message } from \"../types\";\n\nconst MESSAGES_PAGE_SIZE = 100;\nconst MESSAGE_REFRESH_FALLBACK_MS = 30_000;\n/** Bounded retries for a failed read acknowledgement of one message target. */\nconst ACK_MAX_RETRIES = 3;\n\nexport function ConversationDetailPage() {\n const { conversationId } = useParams<{ conversationId: string }>();\n const navigate = useNavigate();\n const { basePath, currentUser } = useConversations();\n const bus = useEventBus();\n const tr = useTr();\n const {\n clearMessageDraft,\n isParticipantsPanelOpen,\n messageDrafts,\n setMessageDraft,\n toggleParticipantsPanel,\n } = useConversationsStore();\n\n const [messageInput, setMessageInput] = useState(\n messageDrafts[conversationId ?? \"\"] ?? \"\",\n );\n const [showMessageQuotaExhausted, setShowMessageQuotaExhausted] =\n useState(false);\n const [locallySentMessages, setLocallySentMessages] = useState<Message[]>([]);\n const messagesEndRef = useRef<HTMLDivElement>(null);\n const composerRef = useRef<HTMLTextAreaElement>(null);\n\n const focusComposer = useCallback(() => {\n composerRef.current?.focus();\n composerRef.current?.scrollIntoView({ behavior: \"smooth\" });\n }, []);\n\n const {\n conversation,\n members: conversationMembers,\n membersTotalCount,\n loading: conversationLoading,\n error: conversationError,\n } = useGetConversation(conversationId ?? \"\");\n const { markConversationAsRead } = useMarkConversationAsRead();\n // Last message id whose read acknowledgement SUCCEEDED. Never advanced\n // pre-emptively: a failed acknowledgement must stay retryable.\n const markedUpToMessageRef = useRef<string | null>(null);\n // Message ids (or the whole-conversation fallback key) currently being\n // acknowledged, so the effect never starts a duplicate in-flight mutation.\n const ackInFlightRef = useRef<Set<string>>(new Set());\n // Monotonic per-conversation attempt counter. A completion only advances\n // the marker when its attempt is still the latest one, so an older\n // mutation landing after a newer one cannot roll the marker backwards.\n const ackAttemptRef = useRef(0);\n // Retry budget bookkeeping: which target the retries are being spent on,\n // and how many have been used. A newer target resets the budget.\n const ackLastTargetRef = useRef<string | null>(null);\n const ackRetryCountRef = useRef(0);\n // Bumped when an acknowledgement fails so this effect re-runs and retries.\n const [ackRetryTick, setAckRetryTick] = useState(0);\n const {\n messages,\n loading: messagesLoading,\n error: messagesError,\n refetch: refetchMessages,\n } = useGetMessages(conversationId ?? \"\", {\n first: MESSAGES_PAGE_SIZE,\n pollInterval: MESSAGE_REFRESH_FALLBACK_MS,\n });\n // Subscribe to live message events; the query has a light polling fallback for\n // local/federated transports that keep the stream open but miss events.\n useMessageStreamForConversation(conversationId, MESSAGES_PAGE_SIZE);\n const { sendMessage, loading: sendingMessage } = useSendMessage();\n const { editMessage, loading: editingMessage } = useEditMessage();\n const {\n deleteMessage,\n addReaction,\n removeReaction,\n pinMessage,\n unpinMessage,\n } = useMessageActions();\n const {\n archiveConversation,\n pinConversation,\n unpinConversation,\n muteConversation,\n unmuteConversation,\n leaveConversation,\n } = useConversationActions();\n\n const [moreMenuOpen, setMoreMenuOpen] = useState(false);\n const [editingMessageId, setEditingMessageId] = useState<string | null>(null);\n const [editingContent, setEditingContent] = useState(\"\");\n const [replyingTo, setReplyingTo] = useState<{\n id: string;\n content: string;\n sender: string;\n } | null>(null);\n const moreMenuRef = useRef<HTMLDivElement>(null);\n\n const currentUserId = currentUser?.id ?? \"\";\n const sourceContext = conversation?.sourceContext;\n\n const displayedMessages = useMemo(() => {\n const byId = new Map<string, Message>();\n for (const message of locallySentMessages) {\n if (message.conversationId !== conversationId) continue;\n byId.set(message.id, message);\n }\n for (const message of messages) {\n byId.set(message.id, message);\n }\n return Array.from(byId.values()).sort(\n (a, b) =>\n new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),\n );\n }, [conversationId, locallySentMessages, messages]);\n\n useEffect(() => {\n messagesEndRef.current?.scrollIntoView({ behavior: \"smooth\" });\n }, [displayedMessages.length]);\n\n useEffect(() => {\n setLocallySentMessages([]);\n }, [conversationId]);\n\n useEffect(() => {\n if (locallySentMessages.length === 0 || messages.length === 0) return;\n const serverIds = new Set(messages.map((message) => message.id));\n setLocallySentMessages((current) => {\n const pending = current.filter((message) => !serverIds.has(message.id));\n return pending.length === current.length ? current : pending;\n });\n }, [locallySentMessages.length, messages, setLocallySentMessages]);\n\n useEffect(() => {\n if (!conversationId) return;\n if (messageInput.trim()) {\n setMessageDraft(conversationId, messageInput);\n return;\n }\n clearMessageDraft(conversationId);\n }, [messageInput, conversationId, setMessageDraft, clearMessageDraft]);\n\n const messageAuthorNames = useMemo(() => {\n const byAuthor = new Map<string, string>();\n for (const message of displayedMessages) {\n if (!message.authorId || byAuthor.has(message.authorId)) continue;\n if (!message.sender.name) continue;\n byAuthor.set(message.authorId, message.sender.name);\n }\n return byAuthor;\n }, [displayedMessages]);\n\n const participants = useMemo(() => {\n if (conversationMembers.length > 0) {\n return conversationMembers\n .filter((member) => member.status === \"ACTIVE\" && !member.leftAt)\n .map((member) => ({\n id: member.userId,\n name:\n messageAuthorNames.get(member.userId) ||\n member.customTitle ||\n tr(\"conversations.member\", \"Member\"),\n role: member.role,\n }));\n }\n\n return Array.from(messageAuthorNames.entries()).map(([id, name]) => ({\n id,\n name,\n role: tr(\"conversations.member\", \"Member\"),\n }));\n }, [conversationMembers, messageAuthorNames, tr]);\n\n const hiddenParticipantsCount = conversation?.memberCount\n ? Math.max(conversation.memberCount - participants.length, 0)\n : 0;\n\n const handleSendMessage = useCallback(async () => {\n if (!conversationId || !messageInput.trim() || sendingMessage) return;\n let message: Message | null;\n try {\n message = await sendMessage({\n conversationId,\n content: messageInput.trim(),\n type: \"TEXT\",\n parentMessageId: replyingTo?.id,\n refetchFirst: MESSAGES_PAGE_SIZE,\n });\n } catch (error) {\n if (isQuotaExhaustedError(error)) {\n setShowMessageQuotaExhausted(true);\n return;\n }\n nativeNotify(\"error\");\n throw error;\n }\n if (!message) return;\n setLocallySentMessages((current) => [...current, message]);\n bus.emit(\"conversations.message.sent\", {\n messageId: message.id,\n conversationId,\n });\n setMessageInput(\"\");\n setReplyingTo(null);\n clearMessageDraft(conversationId);\n }, [\n bus,\n conversationId,\n messageInput,\n sendingMessage,\n sendMessage,\n clearMessageDraft,\n replyingTo,\n ]);\n\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (event.key !== \"Enter\" || event.shiftKey) return;\n event.preventDefault();\n void handleSendMessage();\n },\n [handleSendMessage],\n );\n\n // Emit opened event so sinks (analytics/audit) can record the view\n useEffect(() => {\n if (!conversationId) return;\n bus.emit(\"conversations.conversation.opened\", { conversationId });\n }, [bus, conversationId]);\n\n // Reset the read marker and acknowledgement bookkeeping when switching\n // conversations so the new conversation is marked on its first load.\n useEffect(() => {\n markedUpToMessageRef.current = null;\n ackInFlightRef.current.clear();\n ackAttemptRef.current = 0;\n ackLastTargetRef.current = null;\n ackRetryCountRef.current = 0;\n }, [conversationId]);\n\n // Mark the conversation as read once its messages are visible. Re-runs when\n // newer messages arrive while the conversation is open (the user is viewing\n // them), so inbound messages don't linger as unread. Skip when the newest\n // message is already the last-read one and nothing is unread, to avoid a\n // redundant write on every open.\n //\n // Never acknowledges from a failed messages query: with `messages` empty\n // the fallback no-messageId mutation would clear unread state for messages\n // the user never saw. The effect re-runs once the query recovers, so the\n // then-visible messages are still acknowledged. The marker is only advanced\n // after the mutation succeeds; failures stay retryable (bounded), and an\n // older in-flight acknowledgement completing late can never move the marker\n // past a newer, still-unacknowledged message.\n useEffect(() => {\n if (!conversationId || conversationLoading || messagesLoading) return;\n if (messagesError) return;\n\n const newestMessageId = messages[messages.length - 1]?.id;\n const lastReadMessageId = conversation?.lastReadMessageId ?? \"\";\n const hasUnread = (conversation?.unreadCount ?? 0) > 0;\n\n if (newestMessageId) {\n if (ackLastTargetRef.current !== newestMessageId) {\n ackLastTargetRef.current = newestMessageId;\n ackRetryCountRef.current = 0;\n }\n const alreadyRead =\n !hasUnread &&\n (newestMessageId === lastReadMessageId ||\n newestMessageId === markedUpToMessageRef.current);\n if (alreadyRead) return;\n if (ackInFlightRef.current.has(newestMessageId)) return;\n\n const targetId = newestMessageId;\n const attempt = ++ackAttemptRef.current;\n ackInFlightRef.current.add(targetId);\n void markConversationAsRead({\n conversationId,\n messageId: targetId,\n })\n .then((acknowledged) => {\n if (!acknowledged) {\n throw new Error(\"markConversationAsRead returned no membership\");\n }\n if (ackAttemptRef.current === attempt) {\n markedUpToMessageRef.current = targetId;\n ackRetryCountRef.current = 0;\n }\n })\n .catch((err: Error) => {\n console.warn(\"[conversations] markConversationAsRead failed:\", err);\n // Leave the marker untouched so the failure is retried (bounded);\n // a newer message arriving resets the retry budget.\n if (ackRetryCountRef.current < ACK_MAX_RETRIES) {\n ackRetryCountRef.current += 1;\n setAckRetryTick((tick) => tick + 1);\n }\n })\n .finally(() => {\n ackInFlightRef.current.delete(targetId);\n });\n return;\n }\n\n // No messages are visible (the query succeeded with an empty list): only\n // then fall back to marking the whole conversation read.\n const CONVERSATION_TARGET = \"conversation\";\n if (ackLastTargetRef.current !== CONVERSATION_TARGET) {\n ackLastTargetRef.current = CONVERSATION_TARGET;\n ackRetryCountRef.current = 0;\n }\n if (!hasUnread || ackInFlightRef.current.has(CONVERSATION_TARGET)) return;\n ackInFlightRef.current.add(CONVERSATION_TARGET);\n void markConversationAsRead({ conversationId })\n .then((acknowledged) => {\n if (!acknowledged) {\n throw new Error(\"markConversationAsRead returned no membership\");\n }\n ackRetryCountRef.current = 0;\n })\n .catch((err: Error) => {\n console.warn(\"[conversations] markConversationAsRead failed:\", err);\n if (ackRetryCountRef.current < ACK_MAX_RETRIES) {\n ackRetryCountRef.current += 1;\n setAckRetryTick((tick) => tick + 1);\n }\n })\n .finally(() => {\n ackInFlightRef.current.delete(CONVERSATION_TARGET);\n });\n }, [\n conversationId,\n conversation?.lastReadMessageId,\n conversation?.unreadCount,\n conversationLoading,\n markConversationAsRead,\n messages,\n messagesError,\n messagesLoading,\n ackRetryTick,\n ]);\n\n // Close more-menu on outside click\n useEffect(() => {\n if (!moreMenuOpen) return;\n const handler = (e: MouseEvent) => {\n if (\n moreMenuRef.current &&\n !moreMenuRef.current.contains(e.target as Node)\n ) {\n setMoreMenuOpen(false);\n }\n };\n document.addEventListener(\"mousedown\", handler);\n return () => document.removeEventListener(\"mousedown\", handler);\n }, [moreMenuOpen]);\n\n const handleReply = useCallback(\n (message: { id: string; content: string; sender: { name: string } }) => {\n setReplyingTo({\n id: message.id,\n content: message.content,\n sender: message.sender.name,\n });\n },\n [],\n );\n\n const handleReact = useCallback(\n async (messageId: string, emoji: string) => {\n try {\n await addReaction(messageId, emoji);\n } catch {\n // If already reacted, toggle off\n await removeReaction(messageId, emoji).catch(() => {});\n }\n },\n [addReaction, removeReaction],\n );\n\n const handleEdit = useCallback((message: { id: string; content: string }) => {\n setEditingMessageId(message.id);\n setEditingContent(message.content);\n }, []);\n\n const handleEditSubmit = useCallback(async () => {\n if (!editingMessageId || !editingContent.trim() || editingMessage) return;\n await editMessage(editingMessageId, editingContent.trim());\n setEditingMessageId(null);\n setEditingContent(\"\");\n }, [editingMessageId, editingContent, editingMessage, editMessage]);\n\n const handleEditCancel = useCallback(() => {\n setEditingMessageId(null);\n setEditingContent(\"\");\n }, []);\n\n const handleDelete = useCallback(\n async (messageId: string) => {\n const confirmed = window.confirm(\n tr(\n \"conversations.confirmDeleteMessage\",\n \"Delete this message? This action cannot be undone.\",\n ),\n );\n if (!confirmed) return;\n nativeImpact(\"medium\");\n try {\n await deleteMessage(messageId);\n void refetchMessages();\n nativeNotify(\"success\");\n } catch (error) {\n console.error(\"[microfe-conversations] message delete failed\", error);\n nativeNotify(\"error\");\n }\n },\n [deleteMessage, refetchMessages, tr],\n );\n\n const handleTogglePinMessage = useCallback(\n async (messageId: string, isPinned: boolean) => {\n if (isPinned) {\n await unpinMessage(messageId);\n return;\n }\n await pinMessage(messageId);\n },\n [pinMessage, unpinMessage],\n );\n\n const handlePin = useCallback(async () => {\n if (!conversationId) return;\n if (conversation?.isPinned) {\n await unpinConversation(conversationId);\n } else {\n await pinConversation(conversationId);\n }\n setMoreMenuOpen(false);\n }, [\n conversationId,\n conversation?.isPinned,\n pinConversation,\n unpinConversation,\n ]);\n\n const handleMute = useCallback(async () => {\n if (!conversationId) return;\n if (conversation?.isMuted) {\n await unmuteConversation(conversationId);\n } else {\n await muteConversation(conversationId);\n }\n setMoreMenuOpen(false);\n }, [\n conversationId,\n conversation?.isMuted,\n muteConversation,\n unmuteConversation,\n ]);\n\n const handleArchive = useCallback(async () => {\n if (!conversationId) return;\n try {\n await archiveConversation(conversationId);\n bus.emit(\"conversations.conversation.archived\", { conversationId });\n setMoreMenuOpen(false);\n navigate(basePath || \"/conversations\");\n } catch (error) {\n console.error(\n \"[microfe-conversations] conversation archive failed\",\n error,\n );\n nativeNotify(\"error\");\n }\n }, [archiveConversation, basePath, bus, conversationId, navigate]);\n\n const handleLeave = useCallback(async () => {\n if (!conversationId) return;\n const confirmed = window.confirm(\n tr(\n \"conversations.confirmLeaveConversation\",\n \"Leave this conversation? You will lose access unless someone adds you again.\",\n ),\n );\n if (!confirmed) return;\n try {\n await leaveConversation(conversationId);\n if (currentUserId) {\n bus.emit(\"conversations.member.removed\", {\n conversationId,\n userId: currentUserId,\n });\n }\n setMoreMenuOpen(false);\n navigate(basePath || \"/conversations\");\n } catch (error) {\n console.error(\"[microfe-conversations] conversation leave failed\", error);\n nativeNotify(\"error\");\n }\n }, [\n basePath,\n bus,\n conversationId,\n currentUserId,\n leaveConversation,\n navigate,\n tr,\n ]);\n\n const handleOpenContext = useCallback(() => {\n if (!sourceContext?.contextHref) return;\n try {\n const url = new URL(sourceContext.contextHref);\n if (url.origin === window.location.origin) {\n navigate(`${url.pathname}${url.search}${url.hash}`);\n return;\n }\n } catch {\n // Fall through to full navigation for relative or invalid URLs.\n }\n\n window.location.assign(sourceContext.contextHref);\n }, [navigate, sourceContext?.contextHref]);\n\n // Concrete next actions surfaced on the (complex) detail page so a new user\n // isn't left staring at an unfamiliar surface. Each step is wired to a real\n // handler already present on this page; steps that don't apply to the\n // current conversation type/state are filtered out.\n const detailNextSteps = useMemo<NextStep[]>(() => {\n const steps: NextStep[] = [\n {\n id: \"send-message\",\n label: tr(\n \"conversations.nextSteps.send.label\",\n \"Send your first reply\",\n ),\n description: tr(\n \"conversations.nextSteps.send.description\",\n \"Jump to the composer and start the conversation.\",\n ),\n icon: <SendHorizontal size={14} aria-hidden=\"true\" />,\n onClick: focusComposer,\n done: displayedMessages.length > 0,\n },\n ];\n if (conversation?.isGroup) {\n steps.push({\n id: \"view-participants\",\n label: tr(\n \"conversations.nextSteps.participants.label\",\n \"Review participants\",\n ),\n description: tr(\n \"conversations.nextSteps.participants.description\",\n \"See who can read and post in this conversation.\",\n ),\n icon: <Users size={14} aria-hidden=\"true\" />,\n onClick: () => {\n if (!isParticipantsPanelOpen) toggleParticipantsPanel();\n },\n });\n }\n if (sourceContext?.contextHref) {\n steps.push({\n id: \"open-context\",\n label: tr(\n \"conversations.nextSteps.context.label\",\n \"Open the related item\",\n ),\n description: tr(\n \"conversations.nextSteps.context.description\",\n \"Jump to the page or record this conversation is about.\",\n ),\n icon: <ExternalLink size={14} aria-hidden=\"true\" />,\n onClick: handleOpenContext,\n });\n }\n steps.push({\n id: \"pin-conversation\",\n label: conversation?.isPinned\n ? tr(\"conversations.nextSteps.unpin.label\", \"Unpin this conversation\")\n : tr(\"conversations.nextSteps.pin.label\", \"Pin for quick access\"),\n description: tr(\n \"conversations.nextSteps.pin.description\",\n \"Keep important conversations at the top of your inbox.\",\n ),\n icon: <Pin size={14} aria-hidden=\"true\" />,\n onClick: () => void handlePin(),\n done: conversation?.isPinned,\n });\n return steps;\n }, [\n conversation?.isGroup,\n conversation?.isPinned,\n displayedMessages.length,\n focusComposer,\n handleOpenContext,\n handlePin,\n isParticipantsPanelOpen,\n sourceContext?.contextHref,\n toggleParticipantsPanel,\n tr,\n ]);\n\n if (!conversation && conversationLoading) {\n return (\n <div className=\"flex items-center justify-center h-[calc(100vh-64px)] text-sm text-text-secondary\">\n {tr(\"conversations.loading\", \"Loading conversation...\")}\n </div>\n );\n }\n\n if (!conversation || conversationError) {\n return (\n <div className=\"flex h-[calc(100vh-64px)] items-center justify-center p-8\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={\n conversationError\n ? tr(\"conversations.unavailableTitle\", \"Conversation unavailable\")\n : tr(\"conversations.notFound\", \"Conversation not found\")\n }\n description={\n conversationError\n ? tr(\n \"conversations.unavailable\",\n \"We couldn't load this conversation. Please try again.\",\n )\n : tr(\n \"conversations.notFoundDescription\",\n \"This conversation may have been deleted, archived, or you no longer have access to it.\",\n )\n }\n action={\n <Button\n type=\"button\"\n onClick={() => navigate(basePath || \"/conversations\")}\n >\n {tr(\"conversations.back\", \"Back to conversations\")}\n </Button>\n }\n />\n </div>\n );\n }\n\n return (\n <div className=\"relative flex h-[calc(100vh-64px)] bg-bg-surface\">\n <div className=\"flex-1 flex flex-col min-w-0\">\n <header\n data-tour=\"conversation-detail-header\"\n className=\"flex items-center justify-between px-4 py-3 border-b border-border-seam bg-bg-surface/95 backdrop-blur-sm\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <button\n type=\"button\"\n onClick={() => navigate(basePath || \"/conversations\")}\n className=\"flex min-h-10 min-w-10 items-center justify-center rounded-lg text-text-secondary transition-colors hover:bg-bg-sunken/50 hover:text-text-primary\"\n >\n <ArrowLeft size={18} />\n </button>\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-1.5\">\n {conversation.type === \"CHANNEL\" && (\n <Hash\n size={14}\n className=\"text-text-secondary flex-shrink-0\"\n />\n )}\n <h2 className=\"text-sm font-semibold text-text-primary truncate\">\n {conversation.title}\n </h2>\n </div>\n <p className=\"text-xs text-text-secondary\">\n {conversation.type === \"DIRECT\"\n ? tr(\n \"conversations.directConversation\",\n \"Direct conversation\",\n )\n : conversation.type === \"CHANNEL\"\n ? `${tr(\"conversations.channel\", \"Channel\")} · ${conversation.memberCount} ${tr(\"conversations.membersCountLabel\", \"members\")}`\n : conversation.type === \"ANNOUNCEMENT\"\n ? `${tr(\"conversations.announcement\", \"Announcement\")} · ${conversation.memberCount} ${tr(\"conversations.membersCountLabel\", \"members\")}`\n : `${conversation.memberCount} ${tr(\"conversations.membersCountLabel\", \"members\")}`}\n </p>\n </div>\n </div>\n\n <div className=\"flex shrink-0 items-center gap-1\">\n {sourceContext?.contextHref && (\n <HeaderButton\n dataTour=\"conversation-open-context\"\n icon={<ExternalLink size={16} />}\n label={tr(\"conversations.openContext\", \"Open context\")}\n onClick={handleOpenContext}\n />\n )}\n {conversation.isGroup && (\n <HeaderButton\n dataTour=\"conversation-participants-button\"\n icon={<Users size={16} />}\n label={tr(\"conversations.participants\", \"Participants\")}\n active={isParticipantsPanelOpen}\n onClick={toggleParticipantsPanel}\n />\n )}\n <div className=\"relative\" ref={moreMenuRef}>\n <HeaderButton\n icon={<MoreVertical size={16} />}\n label={tr(\"conversations.moreOptions\", \"More options\")}\n active={moreMenuOpen}\n onClick={() => setMoreMenuOpen((prev) => !prev)}\n />\n {moreMenuOpen && (\n <div className=\"absolute right-0 top-full mt-1 w-48 bg-popover border border-border-subtle rounded-lg shadow-lg z-50 py-1 animate-in fade-in-0 zoom-in-95 duration-100\">\n <DropdownItem\n icon={\n conversation.isPinned ? (\n <PinOff size={14} />\n ) : (\n <Pin size={14} />\n )\n }\n label={\n conversation.isPinned\n ? tr(\n \"conversations.unpinConversation\",\n \"Unpin conversation\",\n )\n : tr(\n \"conversations.pinConversation\",\n \"Pin conversation\",\n )\n }\n onClick={() => void handlePin()}\n />\n <DropdownItem\n icon={\n conversation.isMuted ? (\n <Bell size={14} />\n ) : (\n <BellOff size={14} />\n )\n }\n label={\n conversation.isMuted\n ? tr(\n \"conversations.unmuteConversation\",\n \"Unmute conversation\",\n )\n : tr(\n \"conversations.muteConversation\",\n \"Mute conversation\",\n )\n }\n onClick={() => void handleMute()}\n />\n <div className=\"h-px bg-border my-1\" />\n <DropdownItem\n icon={<Archive size={14} />}\n label={tr(\n \"conversations.archiveConversation\",\n \"Archive conversation\",\n )}\n onClick={() => void handleArchive()}\n destructive\n />\n {conversation.isGroup && (\n <DropdownItem\n icon={<LogOut size={14} />}\n label={tr(\n \"conversations.leaveConversation\",\n \"Leave conversation\",\n )}\n onClick={() => void handleLeave()}\n destructive\n />\n )}\n </div>\n )}\n </div>\n </div>\n </header>\n\n <div className=\"flex-1 overflow-y-auto py-4 space-y-0.5\">\n {sourceContext?.pagePath && (\n <div className=\"mx-4 mb-4 rounded-lg border border-border-subtle bg-bg-sunken/20 px-3 py-2 text-xs text-text-secondary\">\n <span className=\"font-medium text-text-primary\">\n {sourceContext.contextTitle || sourceContext.pagePath}\n </span>\n {sourceContext.entityType && sourceContext.entityId\n ? tr(\n \"conversations.contextEntityMeta\",\n \" · {{entityType}} {{entityId}}\",\n {\n entityType: sourceContext.entityType,\n entityId: sourceContext.entityId,\n },\n )\n : \"\"}\n </div>\n )}\n {messagesError && (\n <div className=\"mx-4 mb-4 rounded-lg border border-status-warning-border bg-status-warning-bg px-3 py-2 text-sm text-status-warning-text\">\n {tr(\n \"conversations.messagesUnavailable\",\n \"Messages couldn't be loaded right now. The conversation header and actions are still available.\",\n )}\n </div>\n )}\n {messagesLoading && (\n <div className=\"mx-4 mb-4 flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-sunken/20 px-3 py-2 text-sm text-text-secondary\">\n <Loader2 size={14} className=\"animate-spin\" />\n {tr(\"conversations.loadingMessages\", \"Loading messages...\")}\n </div>\n )}\n {!messagesLoading &&\n !messagesError &&\n displayedMessages.length === 0 && (\n <div className=\"mx-4 mb-4 flex flex-col items-center gap-5 py-6\">\n <IllustratedEmptyState\n illustration=\"empty-vibes\"\n title={tr(\"conversations.noMessagesTitle\", \"No messages yet\")}\n description={tr(\n \"conversations.noMessagesYet\",\n \"Start the conversation below — say hello, ask a question, or share an update.\",\n )}\n action={\n <Button type=\"button\" onClick={focusComposer}>\n <SendHorizontal size={14} aria-hidden=\"true\" />\n {tr(\"conversations.writeFirstMessage\", \"Write a message\")}\n </Button>\n }\n />\n <NextSteps\n className=\"w-full max-w-md\"\n storageKey=\"conversations-detail\"\n title={tr(\n \"conversations.nextSteps.title\",\n \"Get the most out of this conversation\",\n )}\n steps={detailNextSteps}\n />\n </div>\n )}\n {displayedMessages.map((message, index) => {\n const prevMessage = displayedMessages[index - 1];\n const showAvatar =\n !prevMessage ||\n prevMessage.authorId !== message.authorId ||\n prevMessage.type === \"SYSTEM\";\n\n return (\n <div key={message.id}>\n {editingMessageId === message.id ? (\n <div className=\"px-4 py-2\">\n <div className=\"flex items-center gap-2 text-xs text-text-secondary mb-1\">\n <span>\n {tr(\"conversations.editingMessage\", \"Editing message\")}\n </span>\n <button\n type=\"button\"\n onClick={handleEditCancel}\n className=\"hover:text-text-primary\"\n >\n <X size={12} />\n </button>\n </div>\n <div className=\"flex items-end gap-2\">\n <textarea\n aria-label={tr(\n \"conversations.editMessageAria\",\n \"Edit message\",\n )}\n value={editingContent}\n onChange={(e) => setEditingContent(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n void handleEditSubmit();\n }\n if (e.key === \"Escape\") handleEditCancel();\n }}\n className=\"flex-1 px-3 py-2 text-sm rounded-lg border border-border-subtle bg-bg-sunken/30 resize-none focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n rows={2}\n />\n <button\n type=\"button\"\n onClick={() => void handleEditSubmit()}\n disabled={!editingContent.trim() || editingMessage}\n className=\"px-3 py-2 text-xs font-medium rounded-lg bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {tr(\"conversations.save\", \"Save\")}\n </button>\n </div>\n </div>\n ) : (\n <MessageBubble\n message={message}\n isOwnMessage={message.authorId === currentUserId}\n showAvatar={showAvatar}\n showSender={showAvatar}\n onReply={handleReply}\n onReact={(id, emoji) => void handleReact(id, emoji)}\n onEdit={handleEdit}\n onDelete={(id) => void handleDelete(id)}\n onTogglePin={(id, isPinned) =>\n void handleTogglePinMessage(id, isPinned)\n }\n />\n )}\n </div>\n );\n })}\n <div ref={messagesEndRef} />\n </div>\n\n <div\n data-tour=\"conversation-message-composer\"\n className=\"border-t border-border-seam px-4 py-3 bg-bg-surface\"\n >\n {/* Reply-to indicator */}\n {replyingTo && (\n <div className=\"flex items-center gap-2 mb-2 px-3 py-1.5 rounded-lg bg-bg-sunken/50 text-xs text-text-secondary\">\n <span>\n {tr(\"conversations.replyingTo\", \"Replying to\")}{\" \"}\n <strong className=\"text-text-primary\">\n {replyingTo.sender}\n </strong>\n : {replyingTo.content.slice(0, 80)}\n {replyingTo.content.length > 80 ? \"…\" : \"\"}\n </span>\n <button\n type=\"button\"\n onClick={() => setReplyingTo(null)}\n aria-label={tr(\"conversations.cancelReply\", \"Cancel reply\")}\n className=\"ml-auto flex min-h-9 min-w-9 items-center justify-center rounded-md hover:bg-bg-sunken/70 hover:text-text-primary\"\n >\n <X size={12} />\n </button>\n </div>\n )}\n <div className=\"flex items-end gap-2\">\n <div className=\"flex-1 relative\">\n <textarea\n aria-label={tr(\n \"conversations.messageComposer\",\n \"Message composer\",\n )}\n ref={composerRef}\n value={messageInput}\n onChange={(event) => setMessageInput(event.target.value)}\n onKeyDown={handleKeyDown}\n placeholder={`${tr(\"conversations.messageConversationPrefix\", \"Message\")} ${conversation.title}...`}\n rows={1}\n className={cn(\n \"w-full px-4 py-2.5 text-sm rounded-xl border border-border-subtle bg-bg-sunken/30\",\n \"resize-none overflow-hidden placeholder:text-text-secondary\",\n \"focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:border-transparent max-h-32\",\n )}\n style={{\n height: \"auto\",\n minHeight: \"40px\",\n }}\n onInput={(event) => {\n const target = event.target as HTMLTextAreaElement;\n target.style.height = \"auto\";\n target.style.height = `${Math.min(target.scrollHeight, 128)}px`;\n }}\n />\n </div>\n\n <button\n type=\"button\"\n aria-label={tr(\"conversations.sendMessage\", \"Send message\")}\n onClick={() => void handleSendMessage()}\n disabled={!messageInput.trim() || sendingMessage}\n className={cn(\n \"p-2.5 rounded-xl transition-all duration-150\",\n messageInput.trim() && !sendingMessage\n ? \"bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90 shadow-sm\"\n : \"bg-bg-sunken text-text-secondary cursor-not-allowed\",\n )}\n >\n {sendingMessage ? (\n <Loader2 size={18} className=\"animate-spin\" />\n ) : (\n <SendHorizontal size={18} />\n )}\n </button>\n </div>\n </div>\n </div>\n\n {isParticipantsPanelOpen && conversation.isGroup && (\n <aside className=\"absolute inset-y-0 right-0 z-20 w-full max-w-72 border-l border-border-seam bg-bg-surface shadow-lg animate-in slide-in-from-right-4 duration-200 md:relative md:z-auto md:w-64 md:shadow-none\">\n <div className=\"p-4\">\n <div className=\"flex items-center justify-between mb-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {tr(\"conversations.members\", \"Members\")} (\n {conversation.memberCount ?? participants.length})\n </h3>\n <button\n type=\"button\"\n onClick={toggleParticipantsPanel}\n className=\"p-1 rounded text-text-secondary hover:text-text-primary\"\n >\n <Pin size={14} />\n </button>\n </div>\n\n <div className=\"space-y-1\">\n {participants.map((participant) => (\n <div\n key={participant.id}\n className=\"flex items-center gap-2.5 px-2 py-1.5 rounded-lg hover:bg-bg-sunken/50\"\n >\n <div className=\"size-7 rounded-full bg-bg-sunken flex items-center justify-center text-xs font-medium text-text-secondary\">\n {participant.name\n .split(\" \")\n .map((word) => word[0])\n .join(\"\")\n .slice(0, 2)}\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-xs font-medium text-text-primary truncate\">\n {participant.name}\n </p>\n <p className=\"text-[10px] text-text-secondary\">\n {participant.role}\n </p>\n </div>\n </div>\n ))}\n {hiddenParticipantsCount > 0 && (\n <p className=\"px-2 pt-2 text-xs text-text-secondary\">\n {tr(\"conversations.moreMembersPrefix\", \"and\")}{\" \"}\n {hiddenParticipantsCount}{\" \"}\n {tr(\"conversations.moreMembersSuffix\", \"more\")}\n </p>\n )}\n </div>\n </div>\n </aside>\n )}\n <QuotaExhaustedDialog\n open={showMessageQuotaExhausted}\n title={tr(\n \"conversations.quota.messages.title\",\n \"Message Quota Reached\",\n )}\n description={tr(\n \"conversations.quota.messages.description\",\n \"You've reached the message limit for your current plan. View billing to upgrade or add more capacity.\",\n )}\n onClose={() => setShowMessageQuotaExhausted(false)}\n onConfirm={() => {\n setShowMessageQuotaExhausted(false);\n navigate(\"/billing\");\n }}\n />\n </div>\n );\n}\n\nfunction DropdownItem({\n icon,\n label,\n onClick,\n destructive,\n}: {\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n destructive?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className={cn(\n \"flex items-center gap-2.5 w-full px-3 py-2 text-sm transition-colors text-left\",\n destructive\n ? \"text-status-error-text hover:bg-status-error-bg/10\"\n : \"text-text-primary hover:bg-bg-sunken/50\",\n )}\n >\n {icon}\n <span>{label}</span>\n </button>\n );\n}\n\nfunction HeaderButton({\n icon,\n label,\n active,\n dataTour,\n onClick,\n}: {\n icon: React.ReactNode;\n label: string;\n active?: boolean;\n dataTour?: string;\n onClick?: () => void;\n}) {\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <button\n data-tour={dataTour}\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n title={label}\n className={cn(\n \"flex min-h-10 min-w-10 items-center justify-center rounded-lg transition-colors\",\n active\n ? \"bg-action-primary-bg/10 text-primary\"\n : \"text-text-secondary hover:text-text-primary hover:bg-bg-sunken/50\",\n )}\n >\n {icon}\n </button>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region src/conversations/utils/mappers.ts
|
|
2
2
|
function e(e) {
|
|
3
|
-
let n = e.settings && typeof e.settings == "object" ? e.settings : void 0, r = n && typeof n.metadata == "object" && n.metadata !== null && typeof n.metadata.sourceContext == "object" && n.metadata.sourceContext !== null ? n.metadata.sourceContext : void 0;
|
|
3
|
+
let n = e.settings && typeof e.settings == "object" ? e.settings : void 0, r = n && typeof n.metadata == "object" && n.metadata !== null && typeof n.metadata.sourceContext == "object" && n.metadata.sourceContext !== null ? n.metadata.sourceContext : void 0, i = e.currentUserMembership && typeof e.currentUserMembership == "object" ? e.currentUserMembership : void 0;
|
|
4
4
|
return {
|
|
5
5
|
id: String(e.id ?? ""),
|
|
6
6
|
title: String(e.title ?? "Untitled"),
|
|
@@ -23,10 +23,11 @@ function e(e) {
|
|
|
23
23
|
memberCount: Number(e.memberCount ?? 0),
|
|
24
24
|
unreadCount: Number(e.unreadCount ?? 0),
|
|
25
25
|
mentionCount: Number(e.mentionCount ?? 0),
|
|
26
|
+
lastReadMessageId: i?.lastReadMessageId ? String(i.lastReadMessageId) : void 0,
|
|
26
27
|
lastMessage: e.lastMessage ? t(e.lastMessage) : void 0,
|
|
27
28
|
isPinned: !!(e.currentUserMembership && e.currentUserMembership.isPinned),
|
|
28
29
|
isMuted: !!(e.currentUserMembership && e.currentUserMembership.isMuted),
|
|
29
|
-
features: e.features ?
|
|
30
|
+
features: e.features ? o(e.features) : c(),
|
|
30
31
|
createdBy: {
|
|
31
32
|
id: String(e.createdBy && typeof e.createdBy == "object" ? e.createdBy.id : e.createdBy ?? ""),
|
|
32
33
|
name: String(e.createdBy && typeof e.createdBy == "object" ? e.createdBy.name : "")
|
|
@@ -36,19 +37,19 @@ function e(e) {
|
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
function t(e) {
|
|
39
|
-
let t = Array.isArray(e.reactions) ? e.reactions.map(n) : [],
|
|
40
|
+
let t = Array.isArray(e.reactions) ? e.reactions.map(n) : [], o = Array.isArray(e.mentions) ? e.mentions.map(r) : [], c = Array.isArray(e.attachments) ? e.attachments.map(i) : [];
|
|
40
41
|
return {
|
|
41
42
|
id: String(e.id ?? ""),
|
|
42
43
|
conversationId: String(e.conversationId ?? ""),
|
|
43
44
|
type: String(e.type ?? "TEXT"),
|
|
44
45
|
status: String(e.status ?? "SENT"),
|
|
45
|
-
content:
|
|
46
|
+
content: u(e.content),
|
|
46
47
|
authorId: String(e.authorId ?? ""),
|
|
47
48
|
authorType: String(e.authorType ?? "user"),
|
|
48
49
|
isAnonymous: !!e.isAnonymous,
|
|
49
50
|
sender: {
|
|
50
51
|
id: String(e.authorId ?? ""),
|
|
51
|
-
name:
|
|
52
|
+
name: a(e),
|
|
52
53
|
avatarUrl: e.sender && typeof e.sender == "object" ? e.sender.avatarUrl : void 0
|
|
53
54
|
},
|
|
54
55
|
parentMessageId: e.parentMessageId ? String(e.parentMessageId) : void 0,
|
|
@@ -59,9 +60,9 @@ function t(e) {
|
|
|
59
60
|
editedAt: e.editedAt ? String(e.editedAt) : void 0,
|
|
60
61
|
isPinned: !!e.isPinned,
|
|
61
62
|
reactions: t,
|
|
62
|
-
mentions:
|
|
63
|
-
attachments:
|
|
64
|
-
permissions: e.permissions ?
|
|
63
|
+
mentions: o,
|
|
64
|
+
attachments: c,
|
|
65
|
+
permissions: e.permissions ? s(e.permissions) : l(),
|
|
65
66
|
createdAt: String(e.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()),
|
|
66
67
|
updatedAt: String(e.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString())
|
|
67
68
|
};
|
|
@@ -102,6 +103,14 @@ function i(e) {
|
|
|
102
103
|
};
|
|
103
104
|
}
|
|
104
105
|
function a(e) {
|
|
106
|
+
if (typeof e.authorName == "string" && e.authorName.trim()) return e.authorName;
|
|
107
|
+
if (e.sender && typeof e.sender == "object") {
|
|
108
|
+
let t = e.sender.name;
|
|
109
|
+
if (typeof t == "string" && t.trim()) return t;
|
|
110
|
+
}
|
|
111
|
+
return "Unknown";
|
|
112
|
+
}
|
|
113
|
+
function o(e) {
|
|
105
114
|
return {
|
|
106
115
|
threads: !!(e.threads ?? !0),
|
|
107
116
|
reactions: !!(e.reactions ?? !0),
|
|
@@ -114,7 +123,7 @@ function a(e) {
|
|
|
114
123
|
recording: !!e.recording
|
|
115
124
|
};
|
|
116
125
|
}
|
|
117
|
-
function
|
|
126
|
+
function s(e) {
|
|
118
127
|
return {
|
|
119
128
|
canEdit: !!e.canEdit,
|
|
120
129
|
canDelete: !!e.canDelete,
|
|
@@ -125,7 +134,7 @@ function o(e) {
|
|
|
125
134
|
canReport: !!(e.canReport ?? !0)
|
|
126
135
|
};
|
|
127
136
|
}
|
|
128
|
-
function
|
|
137
|
+
function c() {
|
|
129
138
|
return {
|
|
130
139
|
threads: !0,
|
|
131
140
|
reactions: !0,
|
|
@@ -138,7 +147,7 @@ function s() {
|
|
|
138
147
|
recording: !1
|
|
139
148
|
};
|
|
140
149
|
}
|
|
141
|
-
function
|
|
150
|
+
function l() {
|
|
142
151
|
return {
|
|
143
152
|
canEdit: !1,
|
|
144
153
|
canDelete: !1,
|
|
@@ -149,7 +158,7 @@ function c() {
|
|
|
149
158
|
canReport: !0
|
|
150
159
|
};
|
|
151
160
|
}
|
|
152
|
-
function
|
|
161
|
+
function u(e) {
|
|
153
162
|
if (typeof e == "string") return e;
|
|
154
163
|
if (e && typeof e == "object") {
|
|
155
164
|
let t = e;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mappers.js","names":[],"sources":["../../../src/conversations/utils/mappers.ts"],"sourcesContent":["import type {\n Conversation,\n Message,\n Reaction,\n Attachment,\n Mention,\n MessagePermissions,\n ConversationFeatures,\n} from \"../types\";\n\n/**\n * Map a GraphQL Conversation response to the frontend Conversation type.\n * Gracefully handles undefined/null fields from partial queries.\n */\nexport function mapConversation(raw: Record<string, unknown>): Conversation {\n const settings =\n raw.settings && typeof raw.settings === \"object\"\n ? (raw.settings as Record<string, unknown>)\n : undefined;\n const sourceContext =\n settings &&\n typeof settings.metadata === \"object\" &&\n settings.metadata !== null &&\n typeof (settings.metadata as Record<string, unknown>).sourceContext ===\n \"object\" &&\n (settings.metadata as Record<string, unknown>).sourceContext !== null\n ? ((settings.metadata as Record<string, unknown>).sourceContext as Record<\n string,\n unknown\n >)\n : undefined;\n\n return {\n id: String(raw.id ?? \"\"),\n title: String(raw.title ?? \"Untitled\"),\n description: raw.description ? String(raw.description) : undefined,\n avatar: raw.avatar ? String(raw.avatar) : undefined,\n settings,\n sourceContext: sourceContext\n ? {\n product: sourceContext.product\n ? String(sourceContext.product)\n : undefined,\n pagePath: String(sourceContext.pagePath ?? \"\"),\n contextHref: String(sourceContext.contextHref ?? \"\"),\n contextTitle: sourceContext.contextTitle\n ? String(sourceContext.contextTitle)\n : undefined,\n entityType: sourceContext.entityType\n ? String(sourceContext.entityType)\n : undefined,\n entityId: sourceContext.entityId\n ? String(sourceContext.entityId)\n : undefined,\n activeTab: sourceContext.activeTab\n ? String(sourceContext.activeTab)\n : undefined,\n }\n : undefined,\n type: String(raw.type ?? \"DIRECT\") as Conversation[\"type\"],\n state: String(raw.state ?? \"ACTIVE\") as Conversation[\"state\"],\n visibility: String(\n raw.visibility ?? \"PRIVATE\",\n ) as Conversation[\"visibility\"],\n isGroup: Boolean(raw.isGroup) || String(raw.type ?? \"DIRECT\") !== \"DIRECT\",\n memberCount: Number(raw.memberCount ?? 0),\n unreadCount: Number(raw.unreadCount ?? 0),\n mentionCount: Number(raw.mentionCount ?? 0),\n lastMessage: raw.lastMessage\n ? mapMessage(raw.lastMessage as Record<string, unknown>)\n : undefined,\n isPinned: Boolean(\n raw.currentUserMembership\n ? (raw.currentUserMembership as Record<string, unknown>).isPinned\n : false,\n ),\n isMuted: Boolean(\n raw.currentUserMembership\n ? (raw.currentUserMembership as Record<string, unknown>).isMuted\n : false,\n ),\n features: raw.features\n ? mapConversationFeatures(raw.features as Record<string, unknown>)\n : defaultFeatures(),\n createdBy: {\n id: String(\n raw.createdBy && typeof raw.createdBy === \"object\"\n ? (raw.createdBy as Record<string, unknown>).id\n : (raw.createdBy ?? \"\"),\n ),\n name: String(\n raw.createdBy && typeof raw.createdBy === \"object\"\n ? (raw.createdBy as Record<string, unknown>).name\n : \"\",\n ),\n },\n createdAt: String(raw.createdAt ?? new Date().toISOString()),\n updatedAt: String(raw.updatedAt ?? new Date().toISOString()),\n };\n}\n\n/**\n * Map a GraphQL Message response to the frontend Message type.\n */\nexport function mapMessage(raw: Record<string, unknown>): Message {\n const reactions = Array.isArray(raw.reactions)\n ? (raw.reactions as Record<string, unknown>[]).map(mapReaction)\n : [];\n\n const mentions = Array.isArray(raw.mentions)\n ? (raw.mentions as Record<string, unknown>[]).map(mapMention)\n : [];\n\n const attachments = Array.isArray(raw.attachments)\n ? (raw.attachments as Record<string, unknown>[]).map(mapAttachment)\n : [];\n\n return {\n id: String(raw.id ?? \"\"),\n conversationId: String(raw.conversationId ?? \"\"),\n type: String(raw.type ?? \"TEXT\") as Message[\"type\"],\n status: String(raw.status ?? \"SENT\") as Message[\"status\"],\n content: extractTextContent(raw.content),\n authorId: String(raw.authorId ?? \"\"),\n authorType: String(raw.authorType ?? \"user\"),\n isAnonymous: Boolean(raw.isAnonymous),\n sender: {\n id: String(raw.authorId ?? \"\"),\n name: String(\n raw.sender && typeof raw.sender === \"object\"\n ? (raw.sender as Record<string, unknown>).name\n : (raw.authorId ?? \"Unknown\"),\n ),\n avatarUrl:\n raw.sender && typeof raw.sender === \"object\"\n ? ((raw.sender as Record<string, unknown>).avatarUrl as\n string | undefined)\n : undefined,\n },\n parentMessageId: raw.parentMessageId\n ? String(raw.parentMessageId)\n : undefined,\n threadRootId: raw.threadRootId ? String(raw.threadRootId) : undefined,\n threadLevel: Number(raw.threadLevel ?? 0),\n replyCount: Number(raw.replyCount ?? 0),\n isEdited: Boolean(raw.isEdited),\n editedAt: raw.editedAt ? String(raw.editedAt) : undefined,\n isPinned: Boolean(raw.isPinned),\n reactions,\n mentions,\n attachments,\n permissions: raw.permissions\n ? mapMessagePermissions(raw.permissions as Record<string, unknown>)\n : defaultMessagePermissions(),\n createdAt: String(raw.createdAt ?? new Date().toISOString()),\n updatedAt: String(raw.updatedAt ?? new Date().toISOString()),\n };\n}\n\n/**\n * Map a GraphQL Reaction response\n */\nfunction mapReaction(raw: Record<string, unknown>): Reaction {\n return {\n id: String(raw.id ?? \"\"),\n emoji: String(raw.emoji ?? \"\"),\n count: Number(raw.count ?? 1),\n users: Array.isArray(raw.users)\n ? (raw.users as Record<string, unknown>[]).map((u) => ({\n id: String(u.id ?? \"\"),\n name: String(u.name ?? \"\"),\n }))\n : [],\n currentUserReacted: Boolean(raw.currentUserReacted),\n };\n}\n\n/**\n * Map a GraphQL Mention response\n */\nfunction mapMention(raw: Record<string, unknown>): Mention {\n return {\n id: String(raw.id ?? \"\"),\n mentionedUser: raw.mentionedUser ? String(raw.mentionedUser) : undefined,\n mentionType: String(raw.mentionType ?? \"USER\") as Mention[\"mentionType\"],\n offset: Number(raw.offset ?? 0),\n length: Number(raw.length ?? 0),\n };\n}\n\n/**\n * Map a GraphQL Attachment response\n */\nfunction mapAttachment(raw: Record<string, unknown>): Attachment {\n return {\n id: String(raw.id ?? \"\"),\n fileId: String(raw.fileId ?? \"\"),\n fileName: String(raw.fileName ?? \"file\"),\n fileSize: Number(raw.fileSize ?? 0),\n mimeType: String(raw.mimeType ?? \"application/octet-stream\"),\n downloadUrl: raw.downloadUrl ? String(raw.downloadUrl) : undefined,\n width: raw.width ? Number(raw.width) : undefined,\n height: raw.height ? Number(raw.height) : undefined,\n duration: raw.duration ? Number(raw.duration) : undefined,\n thumbnail: raw.thumbnail ? String(raw.thumbnail) : undefined,\n };\n}\n\n// ============================================================================\n// HELPERS\n// ============================================================================\n\nfunction mapConversationFeatures(\n raw: Record<string, unknown>,\n): ConversationFeatures {\n return {\n threads: Boolean(raw.threads ?? true),\n reactions: Boolean(raw.reactions ?? true),\n mentions: Boolean(raw.mentions ?? true),\n calls: Boolean(raw.calls ?? true),\n apps: Boolean(raw.apps ?? true),\n polls: Boolean(raw.polls ?? true),\n anonymousMode: Boolean(raw.anonymousMode),\n screenShare: Boolean(raw.screenShare ?? true),\n recording: Boolean(raw.recording),\n };\n}\n\nfunction mapMessagePermissions(\n raw: Record<string, unknown>,\n): MessagePermissions {\n return {\n canEdit: Boolean(raw.canEdit),\n canDelete: Boolean(raw.canDelete),\n canReact: Boolean(raw.canReact ?? true),\n canReply: Boolean(raw.canReply ?? true),\n canPin: Boolean(raw.canPin),\n canHide: Boolean(raw.canHide),\n canReport: Boolean(raw.canReport ?? true),\n };\n}\n\nfunction defaultFeatures(): ConversationFeatures {\n return {\n threads: true,\n reactions: true,\n mentions: true,\n calls: true,\n apps: true,\n polls: true,\n anonymousMode: false,\n screenShare: true,\n recording: false,\n };\n}\n\nfunction defaultMessagePermissions(): MessagePermissions {\n return {\n canEdit: false,\n canDelete: false,\n canReact: true,\n canReply: true,\n canPin: false,\n canHide: false,\n canReport: true,\n };\n}\n\n/**\n * Extract text content from a JSON content field.\n * The GraphQL schema stores content as JSON — this handles\n * both string and structured rich-text formats.\n */\nfunction extractTextContent(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (content && typeof content === \"object\") {\n const obj = content as Record<string, unknown>;\n if (typeof obj.text === \"string\") return obj.text;\n if (typeof obj.payload === \"string\") return obj.payload;\n }\n return \"\";\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mappers.js","names":[],"sources":["../../../src/conversations/utils/mappers.ts"],"sourcesContent":["import type {\n Conversation,\n Message,\n Reaction,\n Attachment,\n Mention,\n MessagePermissions,\n ConversationFeatures,\n} from \"../types\";\n\n/**\n * Map a GraphQL Conversation response to the frontend Conversation type.\n * Gracefully handles undefined/null fields from partial queries.\n */\nexport function mapConversation(raw: Record<string, unknown>): Conversation {\n const settings =\n raw.settings && typeof raw.settings === \"object\"\n ? (raw.settings as Record<string, unknown>)\n : undefined;\n const sourceContext =\n settings &&\n typeof settings.metadata === \"object\" &&\n settings.metadata !== null &&\n typeof (settings.metadata as Record<string, unknown>).sourceContext ===\n \"object\" &&\n (settings.metadata as Record<string, unknown>).sourceContext !== null\n ? ((settings.metadata as Record<string, unknown>).sourceContext as Record<\n string,\n unknown\n >)\n : undefined;\n\n const currentUserMembership =\n raw.currentUserMembership && typeof raw.currentUserMembership === \"object\"\n ? (raw.currentUserMembership as Record<string, unknown>)\n : undefined;\n\n return {\n id: String(raw.id ?? \"\"),\n title: String(raw.title ?? \"Untitled\"),\n description: raw.description ? String(raw.description) : undefined,\n avatar: raw.avatar ? String(raw.avatar) : undefined,\n settings,\n sourceContext: sourceContext\n ? {\n product: sourceContext.product\n ? String(sourceContext.product)\n : undefined,\n pagePath: String(sourceContext.pagePath ?? \"\"),\n contextHref: String(sourceContext.contextHref ?? \"\"),\n contextTitle: sourceContext.contextTitle\n ? String(sourceContext.contextTitle)\n : undefined,\n entityType: sourceContext.entityType\n ? String(sourceContext.entityType)\n : undefined,\n entityId: sourceContext.entityId\n ? String(sourceContext.entityId)\n : undefined,\n activeTab: sourceContext.activeTab\n ? String(sourceContext.activeTab)\n : undefined,\n }\n : undefined,\n type: String(raw.type ?? \"DIRECT\") as Conversation[\"type\"],\n state: String(raw.state ?? \"ACTIVE\") as Conversation[\"state\"],\n visibility: String(\n raw.visibility ?? \"PRIVATE\",\n ) as Conversation[\"visibility\"],\n isGroup: Boolean(raw.isGroup) || String(raw.type ?? \"DIRECT\") !== \"DIRECT\",\n memberCount: Number(raw.memberCount ?? 0),\n unreadCount: Number(raw.unreadCount ?? 0),\n mentionCount: Number(raw.mentionCount ?? 0),\n lastReadMessageId: currentUserMembership?.lastReadMessageId\n ? String(currentUserMembership.lastReadMessageId)\n : undefined,\n lastMessage: raw.lastMessage\n ? mapMessage(raw.lastMessage as Record<string, unknown>)\n : undefined,\n isPinned: Boolean(\n raw.currentUserMembership\n ? (raw.currentUserMembership as Record<string, unknown>).isPinned\n : false,\n ),\n isMuted: Boolean(\n raw.currentUserMembership\n ? (raw.currentUserMembership as Record<string, unknown>).isMuted\n : false,\n ),\n features: raw.features\n ? mapConversationFeatures(raw.features as Record<string, unknown>)\n : defaultFeatures(),\n createdBy: {\n id: String(\n raw.createdBy && typeof raw.createdBy === \"object\"\n ? (raw.createdBy as Record<string, unknown>).id\n : (raw.createdBy ?? \"\"),\n ),\n name: String(\n raw.createdBy && typeof raw.createdBy === \"object\"\n ? (raw.createdBy as Record<string, unknown>).name\n : \"\",\n ),\n },\n createdAt: String(raw.createdAt ?? new Date().toISOString()),\n updatedAt: String(raw.updatedAt ?? new Date().toISOString()),\n };\n}\n\n/**\n * Map a GraphQL Message response to the frontend Message type.\n */\nexport function mapMessage(raw: Record<string, unknown>): Message {\n const reactions = Array.isArray(raw.reactions)\n ? (raw.reactions as Record<string, unknown>[]).map(mapReaction)\n : [];\n\n const mentions = Array.isArray(raw.mentions)\n ? (raw.mentions as Record<string, unknown>[]).map(mapMention)\n : [];\n\n const attachments = Array.isArray(raw.attachments)\n ? (raw.attachments as Record<string, unknown>[]).map(mapAttachment)\n : [];\n\n return {\n id: String(raw.id ?? \"\"),\n conversationId: String(raw.conversationId ?? \"\"),\n type: String(raw.type ?? \"TEXT\") as Message[\"type\"],\n status: String(raw.status ?? \"SENT\") as Message[\"status\"],\n content: extractTextContent(raw.content),\n authorId: String(raw.authorId ?? \"\"),\n authorType: String(raw.authorType ?? \"user\"),\n isAnonymous: Boolean(raw.isAnonymous),\n sender: {\n id: String(raw.authorId ?? \"\"),\n name: resolveSenderName(raw),\n avatarUrl:\n raw.sender && typeof raw.sender === \"object\"\n ? ((raw.sender as Record<string, unknown>).avatarUrl as\n | string\n | undefined)\n : undefined,\n },\n parentMessageId: raw.parentMessageId\n ? String(raw.parentMessageId)\n : undefined,\n threadRootId: raw.threadRootId ? String(raw.threadRootId) : undefined,\n threadLevel: Number(raw.threadLevel ?? 0),\n replyCount: Number(raw.replyCount ?? 0),\n isEdited: Boolean(raw.isEdited),\n editedAt: raw.editedAt ? String(raw.editedAt) : undefined,\n isPinned: Boolean(raw.isPinned),\n reactions,\n mentions,\n attachments,\n permissions: raw.permissions\n ? mapMessagePermissions(raw.permissions as Record<string, unknown>)\n : defaultMessagePermissions(),\n createdAt: String(raw.createdAt ?? new Date().toISOString()),\n updatedAt: String(raw.updatedAt ?? new Date().toISOString()),\n };\n}\n\n/**\n * Map a GraphQL Reaction response\n */\nfunction mapReaction(raw: Record<string, unknown>): Reaction {\n return {\n id: String(raw.id ?? \"\"),\n emoji: String(raw.emoji ?? \"\"),\n count: Number(raw.count ?? 1),\n users: Array.isArray(raw.users)\n ? (raw.users as Record<string, unknown>[]).map((u) => ({\n id: String(u.id ?? \"\"),\n name: String(u.name ?? \"\"),\n }))\n : [],\n currentUserReacted: Boolean(raw.currentUserReacted),\n };\n}\n\n/**\n * Map a GraphQL Mention response\n */\nfunction mapMention(raw: Record<string, unknown>): Mention {\n return {\n id: String(raw.id ?? \"\"),\n mentionedUser: raw.mentionedUser ? String(raw.mentionedUser) : undefined,\n mentionType: String(raw.mentionType ?? \"USER\") as Mention[\"mentionType\"],\n offset: Number(raw.offset ?? 0),\n length: Number(raw.length ?? 0),\n };\n}\n\n/**\n * Map a GraphQL Attachment response\n */\nfunction mapAttachment(raw: Record<string, unknown>): Attachment {\n return {\n id: String(raw.id ?? \"\"),\n fileId: String(raw.fileId ?? \"\"),\n fileName: String(raw.fileName ?? \"file\"),\n fileSize: Number(raw.fileSize ?? 0),\n mimeType: String(raw.mimeType ?? \"application/octet-stream\"),\n downloadUrl: raw.downloadUrl ? String(raw.downloadUrl) : undefined,\n width: raw.width ? Number(raw.width) : undefined,\n height: raw.height ? Number(raw.height) : undefined,\n duration: raw.duration ? Number(raw.duration) : undefined,\n thumbnail: raw.thumbnail ? String(raw.thumbnail) : undefined,\n };\n}\n\n// ============================================================================\n// HELPERS\n// ============================================================================\n\n/**\n * Resolve the sender display name for a message.\n *\n * Prefers the server-resolved `authorName` (batch-resolved against the\n * workspace member directory, with a neutral-label fallback), then a nested\n * `sender.name` object when a query happens to provide one. Never falls back\n * to `authorId` — rendering the internal UUID as a name is the bug this\n * ordering prevents (BOFF-7852).\n */\nfunction resolveSenderName(raw: Record<string, unknown>): string {\n if (typeof raw.authorName === \"string\" && raw.authorName.trim()) {\n return raw.authorName;\n }\n if (raw.sender && typeof raw.sender === \"object\") {\n const name = (raw.sender as Record<string, unknown>).name;\n if (typeof name === \"string\" && name.trim()) return name;\n }\n return \"Unknown\";\n}\n\nfunction mapConversationFeatures(\n raw: Record<string, unknown>,\n): ConversationFeatures {\n return {\n threads: Boolean(raw.threads ?? true),\n reactions: Boolean(raw.reactions ?? true),\n mentions: Boolean(raw.mentions ?? true),\n calls: Boolean(raw.calls ?? true),\n apps: Boolean(raw.apps ?? true),\n polls: Boolean(raw.polls ?? true),\n anonymousMode: Boolean(raw.anonymousMode),\n screenShare: Boolean(raw.screenShare ?? true),\n recording: Boolean(raw.recording),\n };\n}\n\nfunction mapMessagePermissions(\n raw: Record<string, unknown>,\n): MessagePermissions {\n return {\n canEdit: Boolean(raw.canEdit),\n canDelete: Boolean(raw.canDelete),\n canReact: Boolean(raw.canReact ?? true),\n canReply: Boolean(raw.canReply ?? true),\n canPin: Boolean(raw.canPin),\n canHide: Boolean(raw.canHide),\n canReport: Boolean(raw.canReport ?? true),\n };\n}\n\nfunction defaultFeatures(): ConversationFeatures {\n return {\n threads: true,\n reactions: true,\n mentions: true,\n calls: true,\n apps: true,\n polls: true,\n anonymousMode: false,\n screenShare: true,\n recording: false,\n };\n}\n\nfunction defaultMessagePermissions(): MessagePermissions {\n return {\n canEdit: false,\n canDelete: false,\n canReact: true,\n canReply: true,\n canPin: false,\n canHide: false,\n canReport: true,\n };\n}\n\n/**\n * Extract text content from a JSON content field.\n * The GraphQL schema stores content as JSON — this handles\n * both string and structured rich-text formats.\n */\nfunction extractTextContent(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (content && typeof content === \"object\") {\n const obj = content as Record<string, unknown>;\n if (typeof obj.text === \"string\") return obj.text;\n if (typeof obj.payload === \"string\") return obj.payload;\n }\n return \"\";\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/conversations/utils/readReceiptCache.ts
|
|
2
|
+
function e(e, i) {
|
|
3
|
+
let a = e.identify({
|
|
4
|
+
__typename: "Conversation",
|
|
5
|
+
id: i.conversationId
|
|
6
|
+
});
|
|
7
|
+
a && (e.modify({
|
|
8
|
+
id: a,
|
|
9
|
+
fields: {
|
|
10
|
+
unreadCount: () => 0,
|
|
11
|
+
mentionCount: () => 0
|
|
12
|
+
}
|
|
13
|
+
}), e.modify({
|
|
14
|
+
id: "ROOT_QUERY",
|
|
15
|
+
fields: { conversations(e, { storeFieldName: i }) {
|
|
16
|
+
if (t(i)?.filter?.hasUnread !== !0) return e;
|
|
17
|
+
let o = r(e);
|
|
18
|
+
if (!o?.edges) return e;
|
|
19
|
+
let s = o.edges.filter((e) => n(e?.node) !== a);
|
|
20
|
+
return s.length === o.edges.length ? e : {
|
|
21
|
+
...o,
|
|
22
|
+
edges: s,
|
|
23
|
+
...typeof o.totalCount == "number" ? { totalCount: Math.max(o.totalCount - 1, 0) } : {}
|
|
24
|
+
};
|
|
25
|
+
} }
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
function t(e) {
|
|
29
|
+
if (!e.startsWith("conversations(") || !e.endsWith(")")) return null;
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(e.slice(14, -1));
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function n(e) {
|
|
37
|
+
return e && typeof e == "object" && "__ref" in e ? e.__ref : e;
|
|
38
|
+
}
|
|
39
|
+
function r(e) {
|
|
40
|
+
return !e || typeof e != "object" || Array.isArray(e) ? null : e;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { e as applyMarkConversationAsReadToCache };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=readReceiptCache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readReceiptCache.js","names":[],"sources":["../../../src/conversations/utils/readReceiptCache.ts"],"sourcesContent":["import type { ApolloCache } from \"@apollo/client\";\n/**\n * Apollo cache update applied after a successful `markConversationAsRead`.\n *\n * Two concerns, both purely cache-local (no network refetch):\n *\n * 1. Zero the viewer-scoped `unreadCount`/`mentionCount` on the cached\n * `Conversation` entity. The conversation list badge reads those top-level\n * fields, which membership normalization alone leaves stale.\n *\n * 2. Remove the conversation from any cached `conversations` field that was\n * fetched with `filter: { hasUnread: true }` (the Inbox \"Unread\" list).\n * That list's edges were materialized server-side, so zeroing the entity\n * fields cannot drop the row — the conversation would stay visible in the\n * Unread list (badge-less) until a network refetch. Filtering the edge out\n * of the cached connection makes the removal immediate.\n */\nexport function applyMarkConversationAsReadToCache(\n cache: ApolloCache,\n membership: { conversationId: string },\n): void {\n const conversationEntityId = cache.identify({\n __typename: \"Conversation\",\n id: membership.conversationId,\n });\n if (!conversationEntityId) return;\n\n cache.modify({\n id: conversationEntityId,\n fields: {\n unreadCount: () => 0,\n mentionCount: () => 0,\n },\n });\n\n cache.modify({\n id: \"ROOT_QUERY\",\n fields: {\n conversations(\n existing: unknown,\n { storeFieldName }: { storeFieldName: string },\n ): unknown {\n const args = readConversationsArgs(storeFieldName);\n // Only touch unread-filtered variants; every other cached list\n // (inbox, pinned, archived, …) keeps its row, now badge-less.\n if (args?.filter?.hasUnread !== true) return existing;\n\n const connection = asConversationConnection(existing);\n if (!connection?.edges) return existing;\n\n const edges = connection.edges.filter(\n (edge) => refId(edge?.node) !== conversationEntityId,\n );\n if (edges.length === connection.edges.length) return existing;\n\n return {\n ...connection,\n edges,\n ...(typeof connection.totalCount === \"number\"\n ? { totalCount: Math.max(connection.totalCount - 1, 0) }\n : {}),\n };\n },\n },\n });\n}\n\ninterface ConversationFilterArgs {\n filter?: { hasUnread?: boolean | null } | null;\n}\n\ninterface CachedConversationConnection {\n edges?: ReadonlyArray<{ node?: unknown } | null> | null;\n totalCount?: number | null;\n}\n\nfunction readConversationsArgs(\n storeFieldName: string,\n): ConversationFilterArgs | null {\n const prefix = \"conversations(\";\n if (!storeFieldName.startsWith(prefix) || !storeFieldName.endsWith(\")\")) {\n return null;\n }\n try {\n return JSON.parse(\n storeFieldName.slice(prefix.length, -1),\n ) as ConversationFilterArgs;\n } catch {\n return null;\n }\n}\n\n/** Normalized cache references carry their entity id under `__ref`. */\nfunction refId(value: unknown): unknown {\n if (value && typeof value === \"object\" && \"__ref\" in value) {\n return (value as { __ref: unknown }).__ref;\n }\n return value;\n}\n\nfunction asConversationConnection(\n value: unknown,\n): CachedConversationConnection | null {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return null;\n return value as CachedConversationConnection;\n}\n"],"mappings":";AAiBA,SAAgB,EACd,GACA,GACM;CACN,IAAM,IAAuB,EAAM,SAAS;EAC1C,YAAY;EACZ,IAAI,EAAW;EAChB,CAAC;AACG,OAEL,EAAM,OAAO;EACX,IAAI;EACJ,QAAQ;GACN,mBAAmB;GACnB,oBAAoB;GACrB;EACF,CAAC,EAEF,EAAM,OAAO;EACX,IAAI;EACJ,QAAQ,EACN,cACE,GACA,EAAE,qBACO;AAIT,OAHa,EAAsB,EAAe,EAGxC,QAAQ,cAAc,GAAM,QAAO;GAE7C,IAAM,IAAa,EAAyB,EAAS;AACrD,OAAI,CAAC,GAAY,MAAO,QAAO;GAE/B,IAAM,IAAQ,EAAW,MAAM,QAC5B,MAAS,EAAM,GAAM,KAAK,KAAK,EACjC;AAGD,UAFI,EAAM,WAAW,EAAW,MAAM,SAAe,IAE9C;IACL,GAAG;IACH;IACA,GAAI,OAAO,EAAW,cAAe,WACjC,EAAE,YAAY,KAAK,IAAI,EAAW,aAAa,GAAG,EAAE,EAAE,GACtD,EAAE;IACP;KAEJ;EACF,CAAC;;AAYJ,SAAS,EACP,GAC+B;AAE/B,KAAI,CAAC,EAAe,WADL,iBACuB,IAAI,CAAC,EAAe,SAAS,IAAI,CACrE,QAAO;AAET,KAAI;AACF,SAAO,KAAK,MACV,EAAe,MAAM,IAAe,GAAG,CACxC;SACK;AACN,SAAO;;;AAKX,SAAS,EAAM,GAAyB;AAItC,QAHI,KAAS,OAAO,KAAU,YAAY,WAAW,IAC3C,EAA6B,QAEhC;;AAGT,SAAS,EACP,GACqC;AAErC,QADI,CAAC,KAAS,OAAO,KAAU,YAAY,MAAM,QAAQ,EAAM,GAAS,OACjE"}
|