@burdenoff/microfe-conversations 2026.530.1 → 2026.601.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.
@@ -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 { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport { useNavigate, useParams } from \"react-router-dom\";\nimport {\n Archive,\n ArrowLeft,\n BellOff,\n Bell,\n ExternalLink,\n Hash,\n Loader2,\n MessageCircle,\n MoreVertical,\n Pin,\n PinOff,\n SendHorizontal,\n Users,\n X,\n} from \"lucide-react\";\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 { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { nativeImpact, nativeNotify } from \"../../utils/nativeBridge\";\nimport type { Message } from \"../types\";\n\nconst MESSAGES_PAGE_SIZE = 100;\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 { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\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 [locallySentMessages, setLocallySentMessages] = useState<Message[]>([]);\n const messagesEndRef = useRef<HTMLDivElement>(null);\n\n const {\n conversation,\n loading: conversationLoading,\n error: conversationError,\n } = useGetConversation(conversationId ?? \"\");\n const {\n messages,\n loading: messagesLoading,\n error: messagesError,\n } = useGetMessages(conversationId ?? \"\", {\n first: MESSAGES_PAGE_SIZE,\n });\n // Subscribe to live message events so the list updates without polling.\n // Apollo cache merges by id; the hook handles append / evict explicitly.\n useMessageStreamForConversation(conversationId);\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 } = 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]);\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 participants = useMemo(() => {\n const byAuthor = new Map<string, { id: string; name: string }>();\n for (const message of displayedMessages) {\n if (!message.authorId || byAuthor.has(message.authorId)) continue;\n byAuthor.set(message.authorId, {\n id: message.authorId,\n name: message.sender.name || message.authorId,\n });\n }\n return Array.from(byAuthor.values());\n }, [displayedMessages]);\n\n const handleSendMessage = useCallback(async () => {\n if (!conversationId || !messageInput.trim() || sendingMessage) return;\n const message = await sendMessage({\n conversationId,\n content: messageInput.trim(),\n type: \"TEXT\",\n parentMessageId: replyingTo?.id,\n refetchFirst: MESSAGES_PAGE_SIZE,\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 nativeImpact(\"medium\");\n try {\n await deleteMessage(messageId);\n nativeNotify(\"success\");\n } catch (error) {\n nativeNotify(\"error\");\n throw error;\n }\n },\n [deleteMessage],\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 await archiveConversation(conversationId);\n bus.emit(\"conversations.conversation.archived\", { conversationId });\n setMoreMenuOpen(false);\n navigate(basePath || \"/conversations\");\n }, [archiveConversation, basePath, bus, conversationId, navigate]);\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 if (!conversation && conversationLoading) {\n return (\n <div className=\"flex items-center justify-center h-[calc(100vh-64px)] text-sm text-muted-foreground\">\n {tr(\"conversations.loading\", \"Loading conversation...\")}\n </div>\n );\n }\n\n if (!conversation || conversationError) {\n return (\n <div className=\"flex flex-col items-center justify-center h-[calc(100vh-64px)] text-center gap-3 px-4\">\n <MessageCircle className=\"size-8 text-muted-foreground\" />\n <p className=\"text-sm text-muted-foreground\">\n {conversationError\n ? tr(\n \"conversations.unavailable\",\n \"We couldn't load this conversation. Please try again.\",\n )\n : tr(\"conversations.notFound\", \"Conversation not found\")}\n </p>\n <button\n type=\"button\"\n onClick={() => navigate(basePath || \"/conversations\")}\n className=\"px-3 py-2 text-sm font-medium rounded-lg border border-border hover:bg-muted/50\"\n >\n {tr(\"conversations.back\", \"Back to conversations\")}\n </button>\n </div>\n );\n }\n\n return (\n <div className=\"relative flex h-[calc(100vh-64px)] bg-background\">\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 bg-background/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-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground\"\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-muted-foreground flex-shrink-0\"\n />\n )}\n <h2 className=\"text-sm font-semibold text-foreground truncate\">\n {conversation.title}\n </h2>\n </div>\n <p className=\"text-xs text-muted-foreground\">\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 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 </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 bg-muted/20 px-3 py-2 text-xs text-muted-foreground\">\n <span className=\"font-medium text-foreground\">\n {sourceContext.contextTitle || sourceContext.pagePath}\n </span>\n {sourceContext.entityType && sourceContext.entityId\n ? ` · ${sourceContext.entityType} ${sourceContext.entityId}`\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 bg-muted/20 px-3 py-2 text-sm text-muted-foreground\">\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 rounded-lg border border-dashed border-border bg-muted/20 px-3 py-6 text-center text-sm text-muted-foreground\">\n {tr(\n \"conversations.noMessagesYet\",\n \"No messages yet. Start the conversation below.\",\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-muted-foreground mb-1\">\n <span>\n {tr(\"conversations.editingMessage\", \"Editing message\")}\n </span>\n <button\n type=\"button\"\n onClick={handleEditCancel}\n className=\"hover:text-foreground\"\n >\n <X size={12} />\n </button>\n </div>\n <div className=\"flex items-end gap-2\">\n <textarea\n aria-label=\"Edit message\"\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 bg-muted/30 resize-none focus:outline-none focus:ring-2 focus:ring-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-primary text-primary-foreground hover:bg-primary/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 px-4 py-3 bg-background\"\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-muted/50 text-xs text-muted-foreground\">\n <span>\n {tr(\"conversations.replyingTo\", \"Replying to\")}{\" \"}\n <strong className=\"text-foreground\">{replyingTo.sender}</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-muted/70 hover:text-foreground\"\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 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 bg-muted/30\",\n \"resize-none overflow-hidden placeholder:text-muted-foreground\",\n \"focus:outline-none focus:ring-2 focus:ring-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-primary text-primary-foreground hover:bg-primary/90 shadow-sm\"\n : \"bg-muted text-muted-foreground 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 bg-background 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-foreground\">\n {tr(\"conversations.members\", \"Members\")} (\n {participants.length || conversation.memberCount})\n </h3>\n <button\n type=\"button\"\n onClick={toggleParticipantsPanel}\n className=\"p-1 rounded text-muted-foreground hover:text-foreground\"\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-muted/50\"\n >\n <div className=\"size-7 rounded-full bg-muted flex items-center justify-center text-xs font-medium text-muted-foreground\">\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-foreground truncate\">\n {participant.name}\n </p>\n <p className=\"text-[10px] text-muted-foreground\">\n {tr(\"conversations.member\", \"Member\")}\n </p>\n </div>\n </div>\n ))}\n </div>\n </div>\n </aside>\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-destructive hover:bg-destructive/10\"\n : \"text-foreground hover:bg-muted/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-primary/10 text-primary\"\n : \"text-muted-foreground hover:text-foreground hover:bg-muted/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 { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport { useNavigate, useParams } from \"react-router-dom\";\nimport {\n Archive,\n ArrowLeft,\n BellOff,\n Bell,\n ExternalLink,\n Hash,\n LogOut,\n Loader2,\n MessageCircle,\n MoreVertical,\n Pin,\n PinOff,\n SendHorizontal,\n Users,\n X,\n} from \"lucide-react\";\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 { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { nativeImpact, nativeNotify } from \"../../utils/nativeBridge\";\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 { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n },\n [t],\n );\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 [locallySentMessages, setLocallySentMessages] = useState<Message[]>([]);\n const messagesEndRef = useRef<HTMLDivElement>(null);\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 const message = await sendMessage({\n conversationId,\n content: messageInput.trim(),\n type: \"TEXT\",\n parentMessageId: replyingTo?.id,\n refetchFirst: MESSAGES_PAGE_SIZE,\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 if (!conversation && conversationLoading) {\n return (\n <div className=\"flex items-center justify-center h-[calc(100vh-64px)] text-sm text-muted-foreground\">\n {tr(\"conversations.loading\", \"Loading conversation...\")}\n </div>\n );\n }\n\n if (!conversation || conversationError) {\n return (\n <div className=\"flex flex-col items-center justify-center h-[calc(100vh-64px)] text-center gap-3 px-4\">\n <MessageCircle className=\"size-8 text-muted-foreground\" />\n <p className=\"text-sm text-muted-foreground\">\n {conversationError\n ? tr(\n \"conversations.unavailable\",\n \"We couldn't load this conversation. Please try again.\",\n )\n : tr(\"conversations.notFound\", \"Conversation not found\")}\n </p>\n <button\n type=\"button\"\n onClick={() => navigate(basePath || \"/conversations\")}\n className=\"px-3 py-2 text-sm font-medium rounded-lg border border-border hover:bg-muted/50\"\n >\n {tr(\"conversations.back\", \"Back to conversations\")}\n </button>\n </div>\n );\n }\n\n return (\n <div className=\"relative flex h-[calc(100vh-64px)] bg-background\">\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 bg-background/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-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground\"\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-muted-foreground flex-shrink-0\"\n />\n )}\n <h2 className=\"text-sm font-semibold text-foreground truncate\">\n {conversation.title}\n </h2>\n </div>\n <p className=\"text-xs text-muted-foreground\">\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 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 bg-muted/20 px-3 py-2 text-xs text-muted-foreground\">\n <span className=\"font-medium text-foreground\">\n {sourceContext.contextTitle || sourceContext.pagePath}\n </span>\n {sourceContext.entityType && sourceContext.entityId\n ? ` · ${sourceContext.entityType} ${sourceContext.entityId}`\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 bg-muted/20 px-3 py-2 text-sm text-muted-foreground\">\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 rounded-lg border border-dashed border-border bg-muted/20 px-3 py-6 text-center text-sm text-muted-foreground\">\n {tr(\n \"conversations.noMessagesYet\",\n \"No messages yet. Start the conversation below.\",\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-muted-foreground mb-1\">\n <span>\n {tr(\"conversations.editingMessage\", \"Editing message\")}\n </span>\n <button\n type=\"button\"\n onClick={handleEditCancel}\n className=\"hover:text-foreground\"\n >\n <X size={12} />\n </button>\n </div>\n <div className=\"flex items-end gap-2\">\n <textarea\n aria-label=\"Edit message\"\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 bg-muted/30 resize-none focus:outline-none focus:ring-2 focus:ring-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-primary text-primary-foreground hover:bg-primary/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 px-4 py-3 bg-background\"\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-muted/50 text-xs text-muted-foreground\">\n <span>\n {tr(\"conversations.replyingTo\", \"Replying to\")}{\" \"}\n <strong className=\"text-foreground\">{replyingTo.sender}</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-muted/70 hover:text-foreground\"\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 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 bg-muted/30\",\n \"resize-none overflow-hidden placeholder:text-muted-foreground\",\n \"focus:outline-none focus:ring-2 focus:ring-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-primary text-primary-foreground hover:bg-primary/90 shadow-sm\"\n : \"bg-muted text-muted-foreground 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 bg-background 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-foreground\">\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-muted-foreground hover:text-foreground\"\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-muted/50\"\n >\n <div className=\"size-7 rounded-full bg-muted flex items-center justify-center text-xs font-medium text-muted-foreground\">\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-foreground truncate\">\n {participant.name}\n </p>\n <p className=\"text-[10px] text-muted-foreground\">\n {participant.role}\n </p>\n </div>\n </div>\n ))}\n {hiddenParticipantsCount > 0 && (\n <p className=\"px-2 pt-2 text-xs text-muted-foreground\">\n {tr(\"conversations.moreMembersPrefix\", \"and\")}{\" \"}\n {hiddenParticipantsCount}{\" \"}\n {tr(\"conversations.moreMembersSuffix\", \"more\")}\n </p>\n )}\n </div>\n </div>\n </aside>\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-destructive hover:bg-destructive/10\"\n : \"text-foreground hover:bg-muted/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-primary/10 text-primary\"\n : \"text-muted-foreground hover:text-foreground hover:bg-muted/50\",\n )}\n >\n {icon}\n </button>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
@@ -1,111 +1,119 @@
1
1
  import { createContext as e, use as t, useMemo as n, useRef as r } from "react";
2
2
  import { MemoryRouter as i } from "react-router-dom";
3
- import { QueryClient as a, QueryClientProvider as o } from "@tanstack/react-query";
4
- import { ApolloClient as s, HttpLink as c, InMemoryCache as l, Observable as u, from as d } from "@apollo/client";
5
- import { onError as f } from "@apollo/client/link/error";
6
- import { ApolloProvider as p, useApolloClient as m } from "@apollo/client/react";
7
- import { PermissionProvider as h } from "@burdenoff/fe-libs/shared/providers/shell";
8
- import { isAuthNetworkError as g, redirectToLogin as _ } from "@burdenoff/fe-libs/shared/utils";
9
- import { useEvent as v } from "@burdenoff/fe-libs/shared/events";
10
- import { Fragment as y, jsx as b, jsxs as x } from "react/jsx-runtime";
3
+ import { MutationCache as a, QueryClient as o, QueryClientProvider as s } from "@tanstack/react-query";
4
+ import { ApolloClient as c, HttpLink as l, InMemoryCache as u, Observable as d, from as f } from "@apollo/client";
5
+ import { onError as p } from "@apollo/client/link/error";
6
+ import { ApolloProvider as m, useApolloClient as h } from "@apollo/client/react";
7
+ import { PermissionProvider as g } from "@burdenoff/fe-libs/shared/providers/shell";
8
+ import { isAuthNetworkError as _, redirectToLogin as v } from "@burdenoff/fe-libs/shared/utils";
9
+ import { useEvent as y } from "@burdenoff/fe-libs/shared/events";
10
+ import { Fragment as b, jsx as x, jsxs as S } from "react/jsx-runtime";
11
11
  //#region src/conversations/providers/ConversationsProvider.tsx
12
- function S() {
13
- let e = m();
14
- return v("auth.signed_out", () => {
12
+ function C() {
13
+ let e = h();
14
+ return y("auth.signed_out", () => {
15
15
  e.clearStore();
16
16
  }), null;
17
17
  }
18
- function C(e = []) {
18
+ function w(e = []) {
19
19
  return e.some((e) => {
20
20
  let t = typeof e.extensions?.code == "string" ? e.extensions.code : "", n = typeof e.extensions?.errorCode == "string" ? e.extensions.errorCode : "", r = (e.message ?? "").toLowerCase();
21
21
  return t === "UNAUTHENTICATED" || n === "TOKEN_EXPIRED" || r.includes("token_expired") || r.includes("token expired") || r.includes("session expired") || r.includes("unauthorized") || r.includes("unauthenticated") || r.includes("authentication required") || r.includes("workspace context missing");
22
22
  });
23
23
  }
24
- var w = e(null), T = () => {
25
- let e = t(w);
24
+ var T = e(null), E = () => {
25
+ let e = t(T);
26
26
  if (!e) throw Error("useConversations must be used within ConversationsProvider");
27
27
  return e;
28
- }, E = ({ children: e, basePath: t, navigate: m, currentUser: v, workspaceId: T, apiGatewayUrl: E, authToken: D, workspaceToken: O, getWorkspaceToken: k, onConversationSelect: A, onMessageSend: j, defaultView: M, variant: N }) => {
29
- let P = {
28
+ }, D = ({ children: e, basePath: t, navigate: h, currentUser: y, workspaceId: E, apiGatewayUrl: D, authToken: O, workspaceToken: k, getWorkspaceToken: A, onConversationSelect: j, onMessageSend: M, defaultView: N, variant: P }) => {
29
+ let F = {
30
30
  basePath: t,
31
- navigate: m,
32
- currentUser: v,
33
- workspaceId: T,
34
- apiGatewayUrl: E,
35
- authToken: D,
36
- workspaceToken: O,
37
- getWorkspaceToken: k,
38
- onConversationSelect: A,
39
- onMessageSend: j,
40
- defaultView: M,
41
- variant: N
42
- }, F = r(!1), I = r(k);
43
- I.current = k;
44
- let L = !!t, R = n(() => new a({ defaultOptions: { queries: {
45
- retry: 1,
46
- refetchOnWindowFocus: !1,
47
- staleTime: 300 * 1e3
48
- } } }), []), z = n(() => {
49
- if (L) return null;
50
- let e = typeof window < "u" && (window.location.hostname === "localhost" || window.location.hostname.endsWith(".local.burdenoff.com")), t = e ? `${window.location.origin}/__local/conversations/graphql` : E ? `${E}/workspaces/graphql` : "/workspaces/graphql", n = {};
51
- D && (n.Authorization = `Bearer ${D}`), O && (n["X-Workspace-Authorization"] = `Bearer ${O}`), T && (n["x-workspace-id"] = T), e && v?.id && (n["x-actor-id"] = v.id, n["x-actor-type"] = "user");
52
- let r = new c({
31
+ navigate: h,
32
+ currentUser: y,
33
+ workspaceId: E,
34
+ apiGatewayUrl: D,
35
+ authToken: O,
36
+ workspaceToken: k,
37
+ getWorkspaceToken: A,
38
+ onConversationSelect: j,
39
+ onMessageSend: M,
40
+ defaultView: N,
41
+ variant: P
42
+ }, I = r(!1), L = r(A);
43
+ L.current = A;
44
+ let R = !!t, z = n(() => {
45
+ let e = new o({
46
+ mutationCache: new a({ onSuccess: () => {
47
+ e.invalidateQueries();
48
+ } }),
49
+ defaultOptions: { queries: {
50
+ retry: 1,
51
+ refetchOnWindowFocus: !1,
52
+ staleTime: 300 * 1e3
53
+ } }
54
+ });
55
+ return e;
56
+ }, []), B = n(() => {
57
+ if (R) return null;
58
+ let e = typeof window < "u" && (window.location.hostname === "localhost" || window.location.hostname.endsWith(".local.burdenoff.com")), t = e ? `${window.location.origin}/__local/conversations/graphql` : D ? `${D}/workspaces/graphql` : "/workspaces/graphql", n = {};
59
+ O && (n.Authorization = `Bearer ${O}`), k && (n["X-Workspace-Authorization"] = `Bearer ${k}`), E && (n["x-workspace-id"] = E), e && y?.id && (n["x-actor-id"] = y.id, n["x-actor-type"] = "user");
60
+ let r = new l({
53
61
  uri: t,
54
62
  headers: Object.keys(n).length > 0 ? n : void 0
55
63
  });
56
- return new s({
57
- link: d([f(({ error: e, operation: t, forward: n }) => {
64
+ return new c({
65
+ link: f([p(({ error: e, operation: t, forward: n }) => {
58
66
  if (!e) return;
59
- let r = C("errors" in e && Array.isArray(e.errors) ? e.errors : []);
60
- if (!I.current || !r || F.current) {
61
- e instanceof Error && g(e) && _();
67
+ let r = w("errors" in e && Array.isArray(e.errors) ? e.errors : []);
68
+ if (!L.current || !r || I.current) {
69
+ e instanceof Error && _(e) && v();
62
70
  return;
63
71
  }
64
- return new u((e) => {
65
- F.current = !0, I.current?.().then((r) => {
66
- if (F.current = !1, !r) {
67
- _(), e.error(/* @__PURE__ */ Error("Failed to refresh workspace token"));
72
+ return new d((e) => {
73
+ I.current = !0, L.current?.().then((r) => {
74
+ if (I.current = !1, !r) {
75
+ v(), e.error(/* @__PURE__ */ Error("Failed to refresh workspace token"));
68
76
  return;
69
77
  }
70
78
  t.setContext(({ headers: e = {} }) => ({ headers: {
71
79
  ...e,
72
80
  "X-Workspace-Authorization": `Bearer ${r}`,
73
- ...T ? { "x-workspace-id": T } : {}
81
+ ...E ? { "x-workspace-id": E } : {}
74
82
  } }));
75
83
  let i = n(t).subscribe(e);
76
84
  return () => i.unsubscribe();
77
85
  }).catch((t) => {
78
- F.current = !1, e.error(t);
86
+ I.current = !1, e.error(t);
79
87
  });
80
88
  });
81
89
  }), r]),
82
- cache: new l(),
90
+ cache: new u(),
83
91
  defaultOptions: { watchQuery: { fetchPolicy: "cache-and-network" } }
84
92
  });
85
93
  }, [
86
- E,
87
94
  D,
88
- v?.id,
89
- L,
90
- T,
91
- O
92
- ]), B = /* @__PURE__ */ b(o, {
93
- client: R,
94
- children: /* @__PURE__ */ b(w.Provider, {
95
- value: P,
96
- children: L ? /* @__PURE__ */ x(h, {
97
- scopeId: T ?? null,
95
+ O,
96
+ y?.id,
97
+ R,
98
+ E,
99
+ k
100
+ ]), V = /* @__PURE__ */ x(s, {
101
+ client: z,
102
+ children: /* @__PURE__ */ x(T.Provider, {
103
+ value: F,
104
+ children: R ? /* @__PURE__ */ S(g, {
105
+ scopeId: E ?? null,
98
106
  gateway: "workspace",
99
- children: [/* @__PURE__ */ b(S, {}), e]
100
- }) : /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b(S, {}), e] })
107
+ children: [/* @__PURE__ */ x(C, {}), e]
108
+ }) : /* @__PURE__ */ S(b, { children: [/* @__PURE__ */ x(C, {}), e] })
101
109
  })
102
110
  });
103
- return L ? B : z ? /* @__PURE__ */ b(p, {
104
- client: z,
105
- children: /* @__PURE__ */ b(i, { children: B })
111
+ return R ? V : B ? /* @__PURE__ */ x(m, {
112
+ client: B,
113
+ children: /* @__PURE__ */ x(i, { children: V })
106
114
  }) : null;
107
115
  };
108
116
  //#endregion
109
- export { E as ConversationsProvider, T as useConversations };
117
+ export { D as ConversationsProvider, E as useConversations };
110
118
 
111
119
  //# sourceMappingURL=ConversationsProvider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationsProvider.js","names":[],"sources":["../../../src/conversations/providers/ConversationsProvider.tsx"],"sourcesContent":["import type { FC, PropsWithChildren } from \"react\";\nimport { createContext, use, useMemo, useRef } from \"react\";\nimport { MemoryRouter } from \"react-router-dom\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport {\n ApolloClient,\n InMemoryCache,\n HttpLink,\n from,\n Observable,\n} from \"@apollo/client\";\nimport { onError } from \"@apollo/client/link/error\";\nimport { ApolloProvider, useApolloClient } from \"@apollo/client/react\";\nimport { PermissionProvider } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport {\n isAuthNetworkError,\n redirectToLogin,\n} from \"@burdenoff/fe-libs/shared/utils\";\nimport { useEvent } from \"@burdenoff/fe-libs/shared/events\";\nimport type { ConversationsRootProps } from \"../types\";\n\n/**\n * Clears Apollo cache on auth.signed_out so stale conversation data\n * is not retained across sessions. Must render inside ApolloProvider.\n */\nfunction AuthCacheClearer() {\n const client = useApolloClient();\n useEvent(\"auth.signed_out\", () => {\n void client.clearStore();\n });\n return null;\n}\n\nfunction hasSessionAuthErrors(\n errors: ReadonlyArray<{\n message?: string;\n extensions?: Record<string, unknown>;\n }> = [],\n): boolean {\n return errors.some((error) => {\n const code =\n typeof error.extensions?.code === \"string\" ? error.extensions.code : \"\";\n const errorCode =\n typeof error.extensions?.errorCode === \"string\"\n ? error.extensions.errorCode\n : \"\";\n const message = (error.message ?? \"\").toLowerCase();\n\n return (\n code === \"UNAUTHENTICATED\" ||\n errorCode === \"TOKEN_EXPIRED\" ||\n message.includes(\"token_expired\") ||\n message.includes(\"token expired\") ||\n message.includes(\"session expired\") ||\n message.includes(\"unauthorized\") ||\n message.includes(\"unauthenticated\") ||\n message.includes(\"authentication required\") ||\n message.includes(\"workspace context missing\")\n );\n });\n}\n\n/**\n * Context value for Conversations microfrontend\n */\ntype ConversationsContextValue = ConversationsRootProps;\n\nconst ConversationsContext = createContext<ConversationsContextValue | null>(\n null,\n);\n\n/**\n * Hook to access Conversations context\n */\nexport const useConversations = () => {\n const context = use(ConversationsContext);\n if (!context) {\n throw new Error(\n \"useConversations must be used within ConversationsProvider\",\n );\n }\n return context;\n};\n\n/**\n * Provider component for Conversations microfrontend\n * Sets up routing, context, Apollo Client, and React Query client\n */\nexport const ConversationsProvider: FC<\n PropsWithChildren<ConversationsRootProps>\n> = ({\n children,\n basePath,\n navigate,\n currentUser,\n workspaceId,\n apiGatewayUrl,\n authToken,\n workspaceToken,\n getWorkspaceToken,\n onConversationSelect,\n onMessageSend,\n defaultView,\n variant,\n}) => {\n const contextValue: ConversationsContextValue = {\n basePath,\n navigate,\n currentUser,\n workspaceId,\n apiGatewayUrl,\n authToken,\n workspaceToken,\n getWorkspaceToken,\n onConversationSelect,\n onMessageSend,\n defaultView,\n variant,\n };\n\n const isRefreshingRef = useRef(false);\n const getWorkspaceTokenRef = useRef(getWorkspaceToken);\n getWorkspaceTokenRef.current = getWorkspaceToken;\n // PermissionProvider requires the shell's AuthProvider — only mount it in integrated mode.\n const isIntegrated = !!basePath;\n\n // Create QueryClient instance (memoized to avoid recreation on re-renders)\n const queryClient = useMemo(\n () =>\n new QueryClient({\n defaultOptions: {\n queries: {\n retry: 1,\n refetchOnWindowFocus: false,\n staleTime: 5 * 60 * 1000, // 5 minutes\n },\n },\n }),\n [],\n );\n\n // Create ApolloClient instance (memoized, recreated when tokens change)\n // The workspace gateway requires dual-token auth:\n // Authorization: Bearer <authToken> (platform identity)\n // X-Workspace-Authorization: Bearer <workspaceToken> (workspace access)\n const apolloClient = useMemo(() => {\n if (isIntegrated) return null;\n\n const isLocalDirectMode =\n typeof window !== \"undefined\" &&\n (window.location.hostname === \"localhost\" ||\n window.location.hostname.endsWith(\".local.burdenoff.com\"));\n const workspaceEndpoint = isLocalDirectMode\n ? `${window.location.origin}/__local/conversations/graphql`\n : apiGatewayUrl\n ? `${apiGatewayUrl}/workspaces/graphql`\n : \"/workspaces/graphql\";\n\n const headers: Record<string, string> = {};\n if (authToken) {\n headers[\"Authorization\"] = `Bearer ${authToken}`;\n }\n if (workspaceToken) {\n headers[\"X-Workspace-Authorization\"] = `Bearer ${workspaceToken}`;\n }\n if (workspaceId) {\n headers[\"x-workspace-id\"] = workspaceId;\n }\n if (isLocalDirectMode && currentUser?.id) {\n headers[\"x-actor-id\"] = currentUser.id;\n headers[\"x-actor-type\"] = \"user\";\n }\n\n const httpLink = new HttpLink({\n uri: workspaceEndpoint,\n headers: Object.keys(headers).length > 0 ? headers : undefined,\n });\n\n const errorLink = onError(({ error, operation, forward }) => {\n if (!error) return;\n\n const errors =\n \"errors\" in error && Array.isArray(error.errors) ? error.errors : [];\n const hasAuthError = hasSessionAuthErrors(errors);\n\n if (\n !getWorkspaceTokenRef.current ||\n !hasAuthError ||\n isRefreshingRef.current\n ) {\n if (error instanceof Error && isAuthNetworkError(error)) {\n redirectToLogin();\n }\n return;\n }\n\n return new Observable((observer) => {\n isRefreshingRef.current = true;\n getWorkspaceTokenRef\n .current?.()\n .then((freshToken) => {\n isRefreshingRef.current = false;\n if (!freshToken) {\n redirectToLogin();\n observer.error(new Error(\"Failed to refresh workspace token\"));\n return;\n }\n\n operation.setContext(\n ({\n headers: existingHeaders = {},\n }: {\n headers?: Record<string, string>;\n }) => ({\n headers: {\n ...existingHeaders,\n \"X-Workspace-Authorization\": `Bearer ${freshToken}`,\n ...(workspaceId ? { \"x-workspace-id\": workspaceId } : {}),\n },\n }),\n );\n\n const subscriber = forward(operation).subscribe(observer);\n return () => subscriber.unsubscribe();\n })\n .catch((refreshError: unknown) => {\n isRefreshingRef.current = false;\n observer.error(refreshError);\n });\n });\n });\n\n return new ApolloClient({\n link: from([errorLink, httpLink]),\n cache: new InMemoryCache(),\n defaultOptions: {\n watchQuery: {\n fetchPolicy: \"cache-and-network\",\n },\n },\n });\n }, [\n apiGatewayUrl,\n authToken,\n currentUser?.id,\n isIntegrated,\n workspaceId,\n workspaceToken,\n ]);\n\n // Wrap content with QueryClientProvider and ConversationsContext.Provider\n const content = (\n <QueryClientProvider client={queryClient}>\n <ConversationsContext.Provider value={contextValue}>\n {isIntegrated ? (\n <PermissionProvider scopeId={workspaceId ?? null} gateway=\"workspace\">\n <AuthCacheClearer />\n {children}\n </PermissionProvider>\n ) : (\n <>\n <AuthCacheClearer />\n {children}\n </>\n )}\n </ConversationsContext.Provider>\n </QueryClientProvider>\n );\n\n // When integrated into a shell (basePath provided), the shell already has a router and ApolloProvider\n // so we don't create another one. When standalone (no basePath), use MemoryRouter and ApolloProvider.\n if (isIntegrated) {\n return content;\n }\n\n if (!apolloClient) {\n return null;\n }\n\n return (\n <ApolloProvider client={apolloClient}>\n <MemoryRouter>{content}</MemoryRouter>\n </ApolloProvider>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"ConversationsProvider.js","names":[],"sources":["../../../src/conversations/providers/ConversationsProvider.tsx"],"sourcesContent":["import type { FC, PropsWithChildren } from \"react\";\nimport { createContext, use, useMemo, useRef } from \"react\";\nimport { MemoryRouter } from \"react-router-dom\";\nimport { MutationCache, QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport {\n ApolloClient,\n InMemoryCache,\n HttpLink,\n from,\n Observable,\n} from \"@apollo/client\";\nimport { onError } from \"@apollo/client/link/error\";\nimport { ApolloProvider, useApolloClient } from \"@apollo/client/react\";\nimport { PermissionProvider } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport {\n isAuthNetworkError,\n redirectToLogin,\n} from \"@burdenoff/fe-libs/shared/utils\";\nimport { useEvent } from \"@burdenoff/fe-libs/shared/events\";\nimport type { ConversationsRootProps } from \"../types\";\n\n/**\n * Clears Apollo cache on auth.signed_out so stale conversation data\n * is not retained across sessions. Must render inside ApolloProvider.\n */\nfunction AuthCacheClearer() {\n const client = useApolloClient();\n useEvent(\"auth.signed_out\", () => {\n void client.clearStore();\n });\n return null;\n}\n\nfunction hasSessionAuthErrors(\n errors: ReadonlyArray<{\n message?: string;\n extensions?: Record<string, unknown>;\n }> = [],\n): boolean {\n return errors.some((error) => {\n const code =\n typeof error.extensions?.code === \"string\" ? error.extensions.code : \"\";\n const errorCode =\n typeof error.extensions?.errorCode === \"string\"\n ? error.extensions.errorCode\n : \"\";\n const message = (error.message ?? \"\").toLowerCase();\n\n return (\n code === \"UNAUTHENTICATED\" ||\n errorCode === \"TOKEN_EXPIRED\" ||\n message.includes(\"token_expired\") ||\n message.includes(\"token expired\") ||\n message.includes(\"session expired\") ||\n message.includes(\"unauthorized\") ||\n message.includes(\"unauthenticated\") ||\n message.includes(\"authentication required\") ||\n message.includes(\"workspace context missing\")\n );\n });\n}\n\n/**\n * Context value for Conversations microfrontend\n */\ntype ConversationsContextValue = ConversationsRootProps;\n\nconst ConversationsContext = createContext<ConversationsContextValue | null>(\n null,\n);\n\n/**\n * Hook to access Conversations context\n */\nexport const useConversations = () => {\n const context = use(ConversationsContext);\n if (!context) {\n throw new Error(\n \"useConversations must be used within ConversationsProvider\",\n );\n }\n return context;\n};\n\n/**\n * Provider component for Conversations microfrontend\n * Sets up routing, context, Apollo Client, and React Query client\n */\nexport const ConversationsProvider: FC<\n PropsWithChildren<ConversationsRootProps>\n> = ({\n children,\n basePath,\n navigate,\n currentUser,\n workspaceId,\n apiGatewayUrl,\n authToken,\n workspaceToken,\n getWorkspaceToken,\n onConversationSelect,\n onMessageSend,\n defaultView,\n variant,\n}) => {\n const contextValue: ConversationsContextValue = {\n basePath,\n navigate,\n currentUser,\n workspaceId,\n apiGatewayUrl,\n authToken,\n workspaceToken,\n getWorkspaceToken,\n onConversationSelect,\n onMessageSend,\n defaultView,\n variant,\n };\n\n const isRefreshingRef = useRef(false);\n const getWorkspaceTokenRef = useRef(getWorkspaceToken);\n getWorkspaceTokenRef.current = getWorkspaceToken;\n // PermissionProvider requires the shell's AuthProvider — only mount it in integrated mode.\n const isIntegrated = !!basePath;\n\n // Create QueryClient instance (memoized to avoid recreation on re-renders)\n const queryClient = useMemo(\n () => {\n const client = new QueryClient({\n mutationCache: new MutationCache({\n onSuccess: () => {\n void client.invalidateQueries();\n },\n }),\n defaultOptions: {\n queries: {\n retry: 1,\n refetchOnWindowFocus: false,\n staleTime: 5 * 60 * 1000, // 5 minutes\n },\n },\n });\n return client;\n },\n [],\n );\n\n // Create ApolloClient instance (memoized, recreated when tokens change)\n // The workspace gateway requires dual-token auth:\n // Authorization: Bearer <authToken> (platform identity)\n // X-Workspace-Authorization: Bearer <workspaceToken> (workspace access)\n const apolloClient = useMemo(() => {\n if (isIntegrated) return null;\n\n const isLocalDirectMode =\n typeof window !== \"undefined\" &&\n (window.location.hostname === \"localhost\" ||\n window.location.hostname.endsWith(\".local.burdenoff.com\"));\n const workspaceEndpoint = isLocalDirectMode\n ? `${window.location.origin}/__local/conversations/graphql`\n : apiGatewayUrl\n ? `${apiGatewayUrl}/workspaces/graphql`\n : \"/workspaces/graphql\";\n\n const headers: Record<string, string> = {};\n if (authToken) {\n headers[\"Authorization\"] = `Bearer ${authToken}`;\n }\n if (workspaceToken) {\n headers[\"X-Workspace-Authorization\"] = `Bearer ${workspaceToken}`;\n }\n if (workspaceId) {\n headers[\"x-workspace-id\"] = workspaceId;\n }\n if (isLocalDirectMode && currentUser?.id) {\n headers[\"x-actor-id\"] = currentUser.id;\n headers[\"x-actor-type\"] = \"user\";\n }\n\n const httpLink = new HttpLink({\n uri: workspaceEndpoint,\n headers: Object.keys(headers).length > 0 ? headers : undefined,\n });\n\n const errorLink = onError(({ error, operation, forward }) => {\n if (!error) return;\n\n const errors =\n \"errors\" in error && Array.isArray(error.errors) ? error.errors : [];\n const hasAuthError = hasSessionAuthErrors(errors);\n\n if (\n !getWorkspaceTokenRef.current ||\n !hasAuthError ||\n isRefreshingRef.current\n ) {\n if (error instanceof Error && isAuthNetworkError(error)) {\n redirectToLogin();\n }\n return;\n }\n\n return new Observable((observer) => {\n isRefreshingRef.current = true;\n getWorkspaceTokenRef\n .current?.()\n .then((freshToken) => {\n isRefreshingRef.current = false;\n if (!freshToken) {\n redirectToLogin();\n observer.error(new Error(\"Failed to refresh workspace token\"));\n return;\n }\n\n operation.setContext(\n ({\n headers: existingHeaders = {},\n }: {\n headers?: Record<string, string>;\n }) => ({\n headers: {\n ...existingHeaders,\n \"X-Workspace-Authorization\": `Bearer ${freshToken}`,\n ...(workspaceId ? { \"x-workspace-id\": workspaceId } : {}),\n },\n }),\n );\n\n const subscriber = forward(operation).subscribe(observer);\n return () => subscriber.unsubscribe();\n })\n .catch((refreshError: unknown) => {\n isRefreshingRef.current = false;\n observer.error(refreshError);\n });\n });\n });\n\n return new ApolloClient({\n link: from([errorLink, httpLink]),\n cache: new InMemoryCache(),\n defaultOptions: {\n watchQuery: {\n fetchPolicy: \"cache-and-network\",\n },\n },\n });\n }, [\n apiGatewayUrl,\n authToken,\n currentUser?.id,\n isIntegrated,\n workspaceId,\n workspaceToken,\n ]);\n\n // Wrap content with QueryClientProvider and ConversationsContext.Provider\n const content = (\n <QueryClientProvider client={queryClient}>\n <ConversationsContext.Provider value={contextValue}>\n {isIntegrated ? (\n <PermissionProvider scopeId={workspaceId ?? null} gateway=\"workspace\">\n <AuthCacheClearer />\n {children}\n </PermissionProvider>\n ) : (\n <>\n <AuthCacheClearer />\n {children}\n </>\n )}\n </ConversationsContext.Provider>\n </QueryClientProvider>\n );\n\n // When integrated into a shell (basePath provided), the shell already has a router and ApolloProvider\n // so we don't create another one. When standalone (no basePath), use MemoryRouter and ApolloProvider.\n if (isIntegrated) {\n return content;\n }\n\n if (!apolloClient) {\n return null;\n }\n\n return (\n <ApolloProvider client={apolloClient}>\n <MemoryRouter>{content}</MemoryRouter>\n </ApolloProvider>\n );\n};\n"],"mappings":"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"}