@burdenoff/microfe-conversations 2026.625.1 → 2026.625.3

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 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-text-secondary\">\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-text-secondary\" />\n <p className=\"text-sm text-text-secondary\">\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-subtle hover:bg-bg-sunken/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-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 ? ` · ${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-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 rounded-lg border border-dashed border-border-subtle bg-bg-sunken/20 px-3 py-6 text-center text-sm text-text-secondary\">\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-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=\"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-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\">{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-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 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 </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":";;;;;;;;;;;;;;;;;AAyCA,IAAM,IAAqB,KACrB,KAA8B;AAEpC,SAAgB,IAAyB;CACvC,IAAM,EAAE,sBAAmB,IAAuC,EAC5D,IAAW,IAAa,EACxB,EAAE,aAAU,oBAAgB,GAAkB,EAC9C,IAAM,IAAa,EACnB,EAAE,SAAM,IAAS,EACjB,IAAK,GACR,GAAa,MAA6B;EACzC,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;IAEpC,CAAC,EAAE,CACJ,EACK,EACJ,sBACA,4BACA,mBACA,oBACA,+BACE,GAAuB,EAErB,CAAC,GAAc,KAAmB,EACtC,GAAc,KAAkB,OAAO,GACxC,EACK,CAAC,GAAqB,KAA0B,EAAoB,EAAE,CAAC,EACvE,IAAiB,EAAuB,KAAK,EAE7C,EACJ,iBACA,SAAS,GACT,uBACA,SAAS,IACT,OAAO,OACL,EAAmB,KAAkB,GAAG,EACtC,EACJ,aACA,SAAS,IACT,OAAO,IACP,SAAS,OACP,GAAe,KAAkB,IAAI;EACvC,OAAO;EACP,cAAc;EACf,CAAC;AAGF,IAAgC,GAAgB,EAAmB;CACnE,IAAM,EAAE,iBAAa,SAAS,MAAmB,IAAgB,EAC3D,EAAE,iBAAa,SAAS,MAAmB,IAAgB,EAC3D,EACJ,mBACA,iBACA,oBACA,gBACA,qBACE,IAAmB,EACjB,EACJ,yBACA,qBACA,uBACA,sBACA,wBACA,0BACE,GAAwB,EAEtB,CAAC,GAAc,KAAmB,EAAS,GAAM,EACjD,CAAC,GAAkB,KAAuB,EAAwB,KAAK,EACvE,CAAC,GAAgB,KAAqB,EAAS,GAAG,EAClD,CAAC,GAAY,KAAiB,EAI1B,KAAK,EACT,IAAc,EAAuB,KAAK,EAE1C,IAAgB,IAAa,MAAM,IACnC,IAAgB,GAAc,eAE9B,IAAoB,QAAc;EACtC,IAAM,oBAAO,IAAI,KAAsB;AACvC,OAAK,IAAM,KAAW,EAChB,GAAQ,mBAAmB,KAC/B,EAAK,IAAI,EAAQ,IAAI,EAAQ;AAE/B,OAAK,IAAM,KAAW,EACpB,GAAK,IAAI,EAAQ,IAAI,EAAQ;AAE/B,SAAO,MAAM,KAAK,EAAK,QAAQ,CAAC,CAAC,MAC9B,GAAG,MACF,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,GAAG,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,CACpE;IACA;EAAC;EAAgB;EAAqB;EAAS,CAAC;AAmBnD,CAjBA,QAAgB;AACd,IAAe,SAAS,eAAe,EAAE,UAAU,UAAU,CAAC;IAC7D,CAAC,EAAkB,OAAO,CAAC,EAE9B,QAAgB;AACd,IAAuB,EAAE,CAAC;IACzB,CAAC,EAAe,CAAC,EAEpB,QAAgB;AACd,MAAI,EAAoB,WAAW,KAAK,EAAS,WAAW,EAAG;EAC/D,IAAM,IAAY,IAAI,IAAI,EAAS,KAAK,MAAY,EAAQ,GAAG,CAAC;AAChE,KAAwB,MAAY;GAClC,IAAM,IAAU,EAAQ,QAAQ,MAAY,CAAC,EAAU,IAAI,EAAQ,GAAG,CAAC;AACvE,UAAO,EAAQ,WAAW,EAAQ,SAAS,IAAU;IACrD;IACD;EAAC,EAAoB;EAAQ;EAAU;EAAuB,CAAC,EAElE,QAAgB;AACT,SACL;OAAI,EAAa,MAAM,EAAE;AACvB,MAAgB,GAAgB,EAAa;AAC7C;;AAEF,KAAkB,EAAe;;IAChC;EAAC;EAAc;EAAgB;EAAiB;EAAkB,CAAC;CAEtE,IAAM,IAAqB,QAAc;EACvC,IAAM,oBAAW,IAAI,KAAqB;AAC1C,OAAK,IAAM,KAAW,EAChB,EAAC,EAAQ,YAAY,EAAS,IAAI,EAAQ,SAAS,IAClD,EAAQ,OAAO,QACpB,EAAS,IAAI,EAAQ,UAAU,EAAQ,OAAO,KAAK;AAErD,SAAO;IACN,CAAC,EAAkB,CAAC,EAEjB,IAAe,QACf,EAAoB,SAAS,IACxB,EACJ,QAAQ,MAAW,EAAO,WAAW,YAAY,CAAC,EAAO,OAAO,CAChE,KAAK,OAAY;EAChB,IAAI,EAAO;EACX,MACE,EAAmB,IAAI,EAAO,OAAO,IACrC,EAAO,eACP,EAAG,wBAAwB,SAAS;EACtC,MAAM,EAAO;EACd,EAAE,GAGA,MAAM,KAAK,EAAmB,SAAS,CAAC,CAAC,KAAK,CAAC,GAAI,QAAW;EACnE;EACA;EACA,MAAM,EAAG,wBAAwB,SAAS;EAC3C,EAAE,EACF;EAAC;EAAqB;EAAoB;EAAG,CAAC,EAE3C,IAA0B,GAAc,cAC1C,KAAK,IAAI,EAAa,cAAc,EAAa,QAAQ,EAAE,GAC3D,GAEE,IAAoB,EAAY,YAAY;AAChD,MAAI,CAAC,KAAkB,CAAC,EAAa,MAAM,IAAI,EAAgB;EAC/D,IAAM,IAAU,MAAM,GAAY;GAChC;GACA,SAAS,EAAa,MAAM;GAC5B,MAAM;GACN,iBAAiB,GAAY;GAC7B,cAAc;GACf,CAAC;AACG,QACL,GAAwB,MAAY,CAAC,GAAG,GAAS,EAAQ,CAAC,EAC1D,EAAI,KAAK,8BAA8B;GACrC,WAAW,EAAQ;GACnB;GACD,CAAC,EACF,EAAgB,GAAG,EACnB,EAAc,KAAK,EACnB,EAAkB,EAAe;IAChC;EACD;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,EAEI,KAAgB,GACnB,MAAoD;AAC/C,IAAM,QAAQ,WAAW,EAAM,aACnC,EAAM,gBAAgB,EACjB,GAAmB;IAE1B,CAAC,EAAkB,CACpB;AASD,CANA,QAAgB;AACT,OACL,EAAI,KAAK,qCAAqC,EAAE,mBAAgB,CAAC;IAChE,CAAC,GAAK,EAAe,CAAC,EAGzB,QAAgB;AACd,MAAI,CAAC,EAAc;EACnB,IAAM,KAAW,MAAkB;AACjC,GACE,EAAY,WACZ,CAAC,EAAY,QAAQ,SAAS,EAAE,OAAe,IAE/C,EAAgB,GAAM;;AAI1B,SADA,SAAS,iBAAiB,aAAa,EAAQ,QAClC,SAAS,oBAAoB,aAAa,EAAQ;IAC9D,CAAC,EAAa,CAAC;CAElB,IAAM,KAAc,GACjB,MAAuE;AACtE,IAAc;GACZ,IAAI,EAAQ;GACZ,SAAS,EAAQ;GACjB,QAAQ,EAAQ,OAAO;GACxB,CAAC;IAEJ,EAAE,CACH,EAEK,KAAc,EAClB,OAAO,GAAmB,MAAkB;AAC1C,MAAI;AACF,SAAM,GAAY,GAAW,EAAM;UAC7B;AAEN,SAAM,GAAe,GAAW,EAAM,CAAC,YAAY,GAAG;;IAG1D,CAAC,IAAa,GAAe,CAC9B,EAEK,KAAa,GAAa,MAA6C;AAE3E,EADA,EAAoB,EAAQ,GAAG,EAC/B,EAAkB,EAAQ,QAAQ;IACjC,EAAE,CAAC,EAEA,KAAmB,EAAY,YAAY;AAC3C,GAAC,KAAoB,CAAC,EAAe,MAAM,IAAI,MACnD,MAAM,GAAY,GAAkB,EAAe,MAAM,CAAC,EAC1D,EAAoB,KAAK,EACzB,EAAkB,GAAG;IACpB;EAAC;EAAkB;EAAgB;EAAgB;EAAY,CAAC,EAE7D,IAAmB,QAAkB;AAEzC,EADA,EAAoB,KAAK,EACzB,EAAkB,GAAG;IACpB,EAAE,CAAC,EAEA,KAAe,EACnB,OAAO,MAAsB;AACT,aAAO,QACvB,EACE,sCACA,qDACD,CACF,EAED;MAAa,SAAS;AACtB,OAAI;AAGF,IAFA,MAAM,GAAc,EAAU,EACzB,IAAiB,EACtB,EAAa,UAAU;YAChB,GAAO;AAEd,IADA,QAAQ,MAAM,iDAAiD,EAAM,EACrE,EAAa,QAAQ;;;IAGzB;EAAC;EAAe;EAAiB;EAAG,CACrC,EAEK,KAAyB,EAC7B,OAAO,GAAmB,MAAsB;AAC9C,MAAI,GAAU;AACZ,SAAM,GAAa,EAAU;AAC7B;;AAEF,QAAM,GAAW,EAAU;IAE7B,CAAC,IAAY,GAAa,CAC3B,EAEK,KAAY,EAAY,YAAY;AACnC,QACD,GAAc,WAChB,MAAM,GAAkB,EAAe,GAEvC,MAAM,GAAgB,EAAe,EAEvC,EAAgB,GAAM;IACrB;EACD;EACA,GAAc;EACd;EACA;EACD,CAAC,EAEI,KAAa,EAAY,YAAY;AACpC,QACD,GAAc,UAChB,MAAM,GAAmB,EAAe,GAExC,MAAM,GAAiB,EAAe,EAExC,EAAgB,GAAM;IACrB;EACD;EACA,GAAc;EACd;EACA;EACD,CAAC,EAEI,KAAgB,EAAY,YAAY;AACvC,QACL,KAAI;AAIF,GAHA,MAAM,GAAoB,EAAe,EACzC,EAAI,KAAK,uCAAuC,EAAE,mBAAgB,CAAC,EACnE,EAAgB,GAAM,EACtB,EAAS,KAAY,iBAAiB;WAC/B,GAAO;AAKd,GAJA,QAAQ,MACN,uDACA,EACD,EACD,EAAa,QAAQ;;IAEtB;EAAC;EAAqB;EAAU;EAAK;EAAgB;EAAS,CAAC,EAE5D,KAAc,EAAY,YAAY;AACrC,WACa,OAAO,QACvB,EACE,0CACA,+EACD,CACF,CAED,KAAI;AASF,GARA,MAAM,GAAkB,EAAe,EACnC,KACF,EAAI,KAAK,gCAAgC;IACvC;IACA,QAAQ;IACT,CAAC,EAEJ,EAAgB,GAAM,EACtB,EAAS,KAAY,iBAAiB;WAC/B,GAAO;AAEd,GADA,QAAQ,MAAM,qDAAqD,EAAM,EACzE,EAAa,QAAQ;;IAEtB;EACD;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,EAEI,KAAoB,QAAkB;AACrC,SAAe,aACpB;OAAI;IACF,IAAM,IAAM,IAAI,IAAI,EAAc,YAAY;AAC9C,QAAI,EAAI,WAAW,OAAO,SAAS,QAAQ;AACzC,OAAS,GAAG,EAAI,WAAW,EAAI,SAAS,EAAI,OAAO;AACnD;;WAEI;AAIR,UAAO,SAAS,OAAO,EAAc,YAAY;;IAChD,CAAC,GAAU,GAAe,YAAY,CAAC;AAiC1C,QA/BI,CAAC,KAAgB,KAEjB,kBAAC,OAAD;EAAK,WAAU;YACZ,EAAG,yBAAyB,0BAA0B;EACnD,CAAA,GAIN,CAAC,KAAgB,KAEjB,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,IAAD,EAAe,WAAU,8BAA+B,CAAA;GACxD,kBAAC,KAAD;IAAG,WAAU;cACV,KACG,EACE,6BACA,wDACD,GACD,EAAG,0BAA0B,yBAAyB;IACxD,CAAA;GACJ,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAS,KAAY,iBAAiB;IACrD,WAAU;cAET,EAAG,sBAAsB,wBAAwB;IAC3C,CAAA;GACL;MAKR,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,UAAD;KACE,aAAU;KACV,WAAU;eAFZ,CAIE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAS,KAAY,iBAAiB;OACrD,WAAU;iBAEV,kBAAC,IAAD,EAAW,MAAM,IAAM,CAAA;OAChB,CAAA,EACT,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACG,EAAa,SAAS,aACrB,kBAAC,IAAD;SACE,MAAM;SACN,WAAU;SACV,CAAA,EAEJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAa;SACX,CAAA,CACD;WACN,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAa,SAAS,WACnB,EACE,oCACA,sBACD,GACD,EAAa,SAAS,YACpB,GAAG,EAAG,yBAAyB,UAAU,CAAC,KAAK,EAAa,YAAY,GAAG,EAAG,mCAAmC,UAAU,KAC3H,EAAa,SAAS,iBACpB,GAAG,EAAG,8BAA8B,eAAe,CAAC,KAAK,EAAa,YAAY,GAAG,EAAG,mCAAmC,UAAU,KACrI,GAAG,EAAa,YAAY,GAAG,EAAG,mCAAmC,UAAU;QACrF,CAAA,CACA;SACF;SAEN,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACG,GAAe,eACd,kBAAC,GAAD;QACE,UAAS;QACT,MAAM,kBAAC,IAAD,EAAc,MAAM,IAAM,CAAA;QAChC,OAAO,EAAG,6BAA6B,eAAe;QACtD,SAAS;QACT,CAAA;OAEH,EAAa,WACZ,kBAAC,GAAD;QACE,UAAS;QACT,MAAM,kBAAC,IAAD,EAAO,MAAM,IAAM,CAAA;QACzB,OAAO,EAAG,8BAA8B,eAAe;QACvD,QAAQ;QACR,SAAS;QACT,CAAA;OAEJ,kBAAC,OAAD;QAAK,WAAU;QAAW,KAAK;kBAA/B,CACE,kBAAC,GAAD;SACE,MAAM,kBAAC,IAAD,EAAc,MAAM,IAAM,CAAA;SAChC,OAAO,EAAG,6BAA6B,eAAe;SACtD,QAAQ;SACR,eAAe,GAAiB,MAAS,CAAC,EAAK;SAC/C,CAAA,EACD,KACC,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACE,kBAAC,GAAD;WACE,MACE,EAAa,WACX,kBAAC,IAAD,EAAQ,MAAM,IAAM,CAAA,GAEpB,kBAAC,IAAD,EAAK,MAAM,IAAM,CAAA;WAGrB,OACE,EAAa,WACT,EACE,mCACA,qBACD,GACD,EACE,iCACA,mBACD;WAEP,eAAe,KAAK,IAAW;WAC/B,CAAA;UACF,kBAAC,GAAD;WACE,MACE,EAAa,UACX,kBAAC,IAAD,EAAM,MAAM,IAAM,CAAA,GAElB,kBAAC,IAAD,EAAS,MAAM,IAAM,CAAA;WAGzB,OACE,EAAa,UACT,EACE,oCACA,sBACD,GACD,EACE,kCACA,oBACD;WAEP,eAAe,KAAK,IAAY;WAChC,CAAA;UACF,kBAAC,OAAD,EAAK,WAAU,uBAAwB,CAAA;UACvC,kBAAC,GAAD;WACE,MAAM,kBAAC,IAAD,EAAS,MAAM,IAAM,CAAA;WAC3B,OAAO,EACL,qCACA,uBACD;WACD,eAAe,KAAK,IAAe;WACnC,aAAA;WACA,CAAA;UACD,EAAa,WACZ,kBAAC,GAAD;WACE,MAAM,kBAAC,IAAD,EAAQ,MAAM,IAAM,CAAA;WAC1B,OAAO,EACL,mCACA,qBACD;WACD,eAAe,KAAK,IAAa;WACjC,aAAA;WACA,CAAA;UAEA;WAEJ;;OACF;QACC;;IAET,kBAAC,OAAD;KAAK,WAAU;eAAf;MACG,GAAe,YACd,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAc,gBAAgB,EAAc;QACxC,CAAA,EACN,EAAc,cAAc,EAAc,WACvC,MAAM,EAAc,WAAW,GAAG,EAAc,aAChD,GACA;;MAEP,MACC,kBAAC,OAAD;OAAK,WAAU;iBACZ,EACC,qCACA,kGACD;OACG,CAAA;MAEP,MACC,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,IAAD;QAAS,MAAM;QAAI,WAAU;QAAiB,CAAA,EAC7C,EAAG,iCAAiC,sBAAsB,CACvD;;MAEP,CAAC,MACA,CAAC,MACD,EAAkB,WAAW,KAC3B,kBAAC,OAAD;OAAK,WAAU;iBACZ,EACC,+BACA,iDACD;OACG,CAAA;MAET,EAAkB,KAAK,GAAS,MAAU;OACzC,IAAM,IAAc,EAAkB,IAAQ,IACxC,IACJ,CAAC,KACD,EAAY,aAAa,EAAQ,YACjC,EAAY,SAAS;AAEvB,cACE,kBAAC,OAAD,EAAA,UACG,MAAqB,EAAQ,KAC5B,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD,EAAA,UACG,EAAG,gCAAgC,kBAAkB,EACjD,CAAA,EACP,kBAAC,UAAD;UACE,MAAK;UACL,SAAS;UACT,WAAU;oBAEV,kBAAC,IAAD,EAAG,MAAM,IAAM,CAAA;UACR,CAAA,CACL;YACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,YAAD;UACE,cAAW;UACX,OAAO;UACP,WAAW,MAAM,EAAkB,EAAE,OAAO,MAAM;UAClD,YAAY,MAAM;AAKhB,WAJI,EAAE,QAAQ,WAAW,CAAC,EAAE,aAC1B,EAAE,gBAAgB,EACb,IAAkB,GAErB,EAAE,QAAQ,YAAU,GAAkB;;UAE5C,WAAU;UACV,MAAM;UACN,CAAA,EACF,kBAAC,UAAD;UACE,MAAK;UACL,eAAe,KAAK,IAAkB;UACtC,UAAU,CAAC,EAAe,MAAM,IAAI;UACpC,WAAU;oBAET,EAAG,sBAAsB,OAAO;UAC1B,CAAA,CACL;WACF;YAEN,kBAAC,GAAD;QACW;QACT,cAAc,EAAQ,aAAa;QACvB;QACZ,YAAY;QACZ,SAAS;QACT,UAAU,GAAI,MAAU,KAAK,GAAY,GAAI,EAAM;QACnD,QAAQ;QACR,WAAW,MAAO,KAAK,GAAa,EAAG;QACvC,cAAc,GAAI,MAChB,KAAK,GAAuB,GAAI,EAAS;QAE3C,CAAA,EAEA,EAvDI,EAAQ,GAuDZ;QAER;MACF,kBAAC,OAAD,EAAK,KAAK,GAAkB,CAAA;MACxB;;IAEN,kBAAC,OAAD;KACE,aAAU;KACV,WAAU;eAFZ,CAKG,KACC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD,EAAA,UAAA;OACG,EAAG,4BAA4B,cAAc;OAAE;OAChD,kBAAC,UAAD;QAAQ,WAAU;kBAAqB,EAAW;QAAgB,CAAA;;OAC/D,EAAW,QAAQ,MAAM,GAAG,GAAG;OACjC,EAAW,QAAQ,SAAS,KAAK,MAAM;OACnC,EAAA,CAAA,EACP,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAc,KAAK;OAClC,cAAY,EAAG,6BAA6B,eAAe;OAC3D,WAAU;iBAEV,kBAAC,IAAD,EAAG,MAAM,IAAM,CAAA;OACR,CAAA,CACL;SAER,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,YAAD;QACE,cAAY,EACV,iCACA,mBACD;QACD,OAAO;QACP,WAAW,MAAU,EAAgB,EAAM,OAAO,MAAM;QACxD,WAAW;QACX,aAAa,GAAG,EAAG,2CAA2C,UAAU,CAAC,GAAG,EAAa,MAAM;QAC/F,MAAM;QACN,WAAW,EACT,qFACA,+DACA,yGACD;QACD,OAAO;SACL,QAAQ;SACR,WAAW;SACZ;QACD,UAAU,MAAU;SAClB,IAAM,IAAS,EAAM;AAErB,SADA,EAAO,MAAM,SAAS,QACtB,EAAO,MAAM,SAAS,GAAG,KAAK,IAAI,EAAO,cAAc,IAAI,CAAC;;QAE9D,CAAA;OACE,CAAA,EAEN,kBAAC,UAAD;OACE,MAAK;OACL,cAAY,EAAG,6BAA6B,eAAe;OAC3D,eAAe,KAAK,GAAmB;OACvC,UAAU,CAAC,EAAa,MAAM,IAAI;OAClC,WAAW,EACT,gDACA,EAAa,MAAM,IAAI,CAAC,IACpB,0FACA,sDACL;iBAEA,IACC,kBAAC,IAAD;QAAS,MAAM;QAAI,WAAU;QAAiB,CAAA,GAE9C,kBAAC,IAAD,EAAgB,MAAM,IAAM,CAAA;OAEvB,CAAA,CACL;QACF;;IACF;MAEL,KAA2B,EAAa,WACvC,kBAAC,SAAD;GAAO,WAAU;aACf,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,MAAD;MAAI,WAAU;gBAAd;OACG,EAAG,yBAAyB,UAAU;OAAC;OACvC,EAAa,eAAe,EAAa;OAAO;OAC9C;SACL,kBAAC,UAAD;MACE,MAAK;MACL,SAAS;MACT,WAAU;gBAEV,kBAAC,IAAD,EAAK,MAAM,IAAM,CAAA;MACV,CAAA,CACL;QAEN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACG,EAAa,KAAK,MACjB,kBAAC,OAAD;MAEE,WAAU;gBAFZ,CAIE,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAY,KACV,MAAM,IAAI,CACV,KAAK,MAAS,EAAK,GAAG,CACtB,KAAK,GAAG,CACR,MAAM,GAAG,EAAE;OACV,CAAA,EACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAY;QACX,CAAA,EACJ,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAY;QACX,CAAA,CACA;SACF;QAlBC,EAAY,GAkBb,CACN,EACD,IAA0B,KACzB,kBAAC,KAAD;MAAG,WAAU;gBAAb;OACG,EAAG,mCAAmC,MAAM;OAAE;OAC9C;OAAyB;OACzB,EAAG,mCAAmC,OAAO;OAC5C;QAEF;OACF;;GACA,CAAA,CAEN;;;AAIV,SAAS,EAAa,EACpB,SACA,UACA,YACA,kBAMC;AACD,QACE,kBAAC,UAAD;EACE,MAAK;EACI;EACT,WAAW,EACT,kFACA,IACI,uDACA,0CACL;YARH,CAUG,GACD,kBAAC,QAAD,EAAA,UAAO,GAAa,CAAA,CACb;;;AAIb,SAAS,EAAa,EACpB,SACA,UACA,WACA,aACA,cAOC;AACD,QACE,kBAAC,GAAD;EACE,YAAY,EAA0B;EACtC,6BAAA;YAEA,kBAAC,UAAD;GACE,aAAW;GACX,MAAK;GACI;GACT,cAAY;GACZ,OAAO;GACP,WAAW,EACT,mFACA,IACI,yCACA,oEACL;aAEA;GACM,CAAA;EACO,CAAA"}
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 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 { 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 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 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 // 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 ? ` · ${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-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=\"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-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 </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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"}
@@ -2,17 +2,17 @@ import { useConversations as e } from "../providers/ConversationsProvider.js";
2
2
  import { joinPath as t } from "../utils/navigation.js";
3
3
  import { useTr as n } from "../utils/i18n.js";
4
4
  import { useConversationsStore as r } from "../store/conversationsStore.js";
5
- import { useGetConversations as i, useSearchConversations as a } from "../hooks/useConversationQueries.js";
6
- import { useConversationStream as ee } from "../hooks/useMessageSubscriptions.js";
5
+ import { useGetConversations as i, useSearchConversations as ee } from "../hooks/useConversationQueries.js";
6
+ import { useConversationStream as a } from "../hooks/useMessageSubscriptions.js";
7
7
  import { PermissionGuard as o } from "../components/PermissionGuard.js";
8
8
  import { CONVERSATIONS_PERMISSIONS as s } from "../constants/permissions.js";
9
9
  import { ConversationRow as c } from "../components/ConversationRow.js";
10
- import { useCallback as te, useDeferredValue as ne, useEffect as l, useMemo as u, useState as d } from "react";
11
- import { AlertCircle as f, Archive as p, Hash as m, MessageCircle as h, Pin as g, Users as _ } from "lucide-react";
12
- import { useNavigate as v, useSearchParams as re } from "react-router-dom";
13
- import { useEventBus as ie } from "@burdenoff/fe-libs/shared/events";
14
- import { Fragment as y, jsx as b, jsxs as x } from "react/jsx-runtime";
15
- import { EmptyState as S } from "@burdenoff/fe-libs/ui";
10
+ import { useCallback as te, useDeferredValue as l, useEffect as u, useMemo as d, useState as f } from "react";
11
+ import { AlertCircle as p, Archive as m, Hash as h, MessageCircle as g, Pin as ne, Users as re } from "lucide-react";
12
+ import { useNavigate as ie, useSearchParams as ae } from "react-router-dom";
13
+ import { useEventBus as _ } from "@burdenoff/fe-libs/shared/events";
14
+ import { Fragment as v, jsx as y, jsxs as b } from "react/jsx-runtime";
15
+ import { EmptyState as x, IllustratedEmptyState as S } from "@burdenoff/fe-libs/ui";
16
16
  //#region src/conversations/pages/ConversationListPage.tsx
17
17
  var C = {
18
18
  state: "all",
@@ -23,16 +23,16 @@ var C = {
23
23
  isMuted: void 0
24
24
  };
25
25
  function w(e, t) {
26
- let [n, r] = d(e);
27
- return l(() => {
26
+ let [n, r] = f(e);
27
+ return u(() => {
28
28
  let n = window.setTimeout(() => r(e), t);
29
29
  return () => window.clearTimeout(n);
30
30
  }, [t, e]), n;
31
31
  }
32
- function T({ forcedFilterParam: d, embedded: T = !1 } = {}) {
33
- let E = v(), { basePath: D, onConversationSelect: O } = e(), k = ie(), A = n(), { searchQuery: ae, conversationFilter: j, updateConversationFilter: M, activeConversationId: N, setActiveConversationId: P } = r(), [F] = re(), I = ne(w(ae, 250)), L = I.trim(), R = L.length >= 2;
34
- l(() => {
35
- let e = d ?? F.get("filter");
32
+ function T({ forcedFilterParam: f, embedded: T = !1 } = {}) {
33
+ let E = ie(), { basePath: D, onConversationSelect: O } = e(), k = _(), A = n(), { searchQuery: oe, conversationFilter: j, updateConversationFilter: M, activeConversationId: N, setActiveConversationId: P } = r(), [F] = ae(), I = l(w(oe, 250)), L = I.trim(), R = L.length >= 2;
34
+ u(() => {
35
+ let e = f ?? F.get("filter");
36
36
  if (!e) {
37
37
  M(C);
38
38
  return;
@@ -55,11 +55,11 @@ function T({ forcedFilterParam: d, embedded: T = !1 } = {}) {
55
55
  type: t
56
56
  });
57
57
  }, [
58
- d,
58
+ f,
59
59
  F,
60
60
  M
61
61
  ]);
62
- let z = u(() => {
62
+ let z = d(() => {
63
63
  if (!(j.state === "all" || j.state === "ACTIVE")) return [j.state];
64
64
  }, [j.state]), { conversations: B, loading: V, error: H } = i({
65
65
  first: 50,
@@ -68,8 +68,8 @@ function T({ forcedFilterParam: d, embedded: T = !1 } = {}) {
68
68
  hasUnread: j.hasUnread,
69
69
  isPinned: j.isPinned
70
70
  });
71
- ee();
72
- let { conversations: U, loading: W, error: G } = a(I), K = R ? U : B, q = u(() => K.filter((e) => e.isPinned), [K]), J = u(() => K.filter((e) => !e.isPinned), [K]), Y = te((e) => {
71
+ a();
72
+ let { conversations: U, loading: W, error: G } = ee(I), K = R ? U : B, q = d(() => K.filter((e) => e.isPinned), [K]), J = d(() => K.filter((e) => !e.isPinned), [K]), Y = te((e) => {
73
73
  if (P(e.id), O?.(e), k.emit("conversations.conversation.opened", {
74
74
  conversationId: e.id,
75
75
  conversationType: e.type
@@ -87,145 +87,151 @@ function T({ forcedFilterParam: d, embedded: T = !1 } = {}) {
87
87
  E,
88
88
  O,
89
89
  P
90
- ]), X = V || W, Z = H ?? G, Q = d === "archived" || j.state === "ARCHIVED", $ = u(() => {
90
+ ]), X = V || W, Z = H ?? G, Q = f === "archived" || j.state === "ARCHIVED", $ = d(() => {
91
91
  if (R) return {
92
92
  title: A("conversations.empty.search.title", "No matches"),
93
93
  description: `${A("conversations.empty.search.description", "No conversations matching")} "${L}"`,
94
- icon: /* @__PURE__ */ b(h, {
94
+ icon: /* @__PURE__ */ y(g, {
95
95
  size: 28,
96
96
  className: "text-text-secondary"
97
- })
97
+ }),
98
+ illustration: "empty-search"
98
99
  };
99
100
  if (Q) return {
100
101
  title: A("conversations.empty.archived.title", "No archived conversations"),
101
102
  description: A("conversations.empty.archived.description", "Archived conversations are hidden from your main inbox. Move conversations here from the row actions menu."),
102
- icon: /* @__PURE__ */ b(p, {
103
+ icon: /* @__PURE__ */ y(m, {
103
104
  size: 28,
104
105
  className: "text-text-secondary"
105
- })
106
+ }),
107
+ illustration: "empty-files"
106
108
  };
107
- switch (d) {
109
+ switch (f) {
108
110
  case "direct": return {
109
111
  title: A("conversations.empty.direct.title", "No direct conversations yet"),
110
112
  description: A("conversations.empty.direct.description", "Start one with a workspace member from the New conversation button above."),
111
- icon: /* @__PURE__ */ b(h, {
113
+ icon: /* @__PURE__ */ y(g, {
112
114
  size: 28,
113
115
  className: "text-text-secondary"
114
- })
116
+ }),
117
+ illustration: "empty-vibes"
115
118
  };
116
119
  case "group": return {
117
120
  title: A("conversations.empty.groups.title", "Create a group"),
118
121
  description: A("conversations.empty.groups.description", "Groups let you chat with multiple people in one thread. Tap New to start one."),
119
- icon: /* @__PURE__ */ b(_, {
122
+ icon: /* @__PURE__ */ y(re, {
120
123
  size: 28,
121
124
  className: "text-text-secondary"
122
- })
125
+ }),
126
+ illustration: "empty-people"
123
127
  };
124
128
  case "channel": return {
125
129
  title: A("conversations.empty.channels.title", "Subscribe to a channel"),
126
130
  description: A("conversations.empty.channels.description", "Channels are topic-based discussions visible to everyone who joins them."),
127
- icon: /* @__PURE__ */ b(m, {
131
+ icon: /* @__PURE__ */ y(h, {
128
132
  size: 28,
129
133
  className: "text-text-secondary"
130
- })
134
+ }),
135
+ illustration: "empty-vibes"
131
136
  };
132
137
  default: return {
133
138
  title: A("conversations.empty.all.title", "No conversations yet"),
134
139
  description: A("conversations.empty.all.description", "Start your first conversation with someone in this workspace."),
135
- icon: /* @__PURE__ */ b(h, {
140
+ icon: /* @__PURE__ */ y(g, {
136
141
  size: 28,
137
142
  className: "text-text-secondary"
138
- })
143
+ }),
144
+ illustration: "empty-vibes"
139
145
  };
140
146
  }
141
147
  }, [
142
- d,
148
+ f,
143
149
  Q,
144
150
  R,
145
151
  A,
146
152
  L
147
153
  ]);
148
- return T ? /* @__PURE__ */ b(o, {
154
+ return T ? /* @__PURE__ */ y(o, {
149
155
  permission: s.CONVERSATION_READ,
150
156
  allowUnavailablePermissions: !0,
151
- children: /* @__PURE__ */ b("div", {
157
+ children: /* @__PURE__ */ y("div", {
152
158
  "data-tour": "conversation-list",
153
159
  className: "flex h-full min-h-0 w-full flex-col",
154
160
  "aria-live": "polite",
155
- children: X ? /* @__PURE__ */ b("div", {
161
+ children: X ? /* @__PURE__ */ y("div", {
156
162
  role: "status",
157
163
  className: "px-4 py-8 text-sm text-text-secondary",
158
164
  children: A("conversations.loading", "Loading conversations...")
159
- }) : Z ? /* @__PURE__ */ b(S, {
160
- icon: /* @__PURE__ */ b(f, {
165
+ }) : Z ? /* @__PURE__ */ y(x, {
166
+ icon: /* @__PURE__ */ y(p, {
161
167
  size: 28,
162
168
  className: "text-status-error-text",
163
169
  "aria-hidden": "true"
164
170
  }),
165
171
  title: A("conversations.couldNotLoad", "Could not load conversations"),
166
172
  description: Z.message
167
- }) : K.length === 0 ? /* @__PURE__ */ b(S, {
168
- icon: $.icon,
173
+ }) : K.length === 0 ? /* @__PURE__ */ y(S, {
174
+ illustration: $.illustration,
169
175
  title: $.title,
170
176
  description: $.description
171
- }) : /* @__PURE__ */ x("div", {
177
+ }) : /* @__PURE__ */ b("div", {
172
178
  "data-tour": "conversation-list-results",
173
179
  className: "flex flex-col gap-0.5 px-2 py-2",
174
- children: [q.length > 0 && /* @__PURE__ */ x(y, { children: [
175
- /* @__PURE__ */ x("div", {
180
+ children: [q.length > 0 && /* @__PURE__ */ b(v, { children: [
181
+ /* @__PURE__ */ b("div", {
176
182
  className: "flex items-center gap-1.5 px-2 pt-2 pb-1",
177
- children: [/* @__PURE__ */ b(g, {
183
+ children: [/* @__PURE__ */ y(ne, {
178
184
  size: 11,
179
185
  className: "text-text-secondary",
180
186
  "aria-hidden": "true"
181
- }), /* @__PURE__ */ b("span", {
187
+ }), /* @__PURE__ */ y("span", {
182
188
  className: "text-[10px] font-semibold uppercase tracking-wider text-text-secondary",
183
189
  children: A("conversations.pinned", "Pinned")
184
190
  })]
185
191
  }),
186
- q.map((e) => /* @__PURE__ */ b(c, {
192
+ q.map((e) => /* @__PURE__ */ y(c, {
187
193
  conversation: e,
188
194
  isActive: N === e.id,
189
195
  onClick: Y
190
196
  }, e.id)),
191
- J.length > 0 && /* @__PURE__ */ b("div", {
197
+ J.length > 0 && /* @__PURE__ */ y("div", {
192
198
  className: "flex items-center gap-1.5 px-2 pt-3 pb-1",
193
- children: /* @__PURE__ */ b("span", {
199
+ children: /* @__PURE__ */ y("span", {
194
200
  className: "text-[10px] font-semibold uppercase tracking-wider text-text-secondary",
195
201
  children: A("conversations.tabs.all", "All")
196
202
  })
197
203
  })
198
- ] }), J.map((e) => /* @__PURE__ */ b(c, {
204
+ ] }), J.map((e) => /* @__PURE__ */ y(c, {
199
205
  conversation: e,
200
206
  isActive: N === e.id,
201
207
  onClick: Y
202
208
  }, e.id))]
203
209
  })
204
210
  })
205
- }) : /* @__PURE__ */ b(o, {
211
+ }) : /* @__PURE__ */ y(o, {
206
212
  permission: s.CONVERSATION_READ,
207
213
  allowUnavailablePermissions: !0,
208
- children: /* @__PURE__ */ b("div", {
214
+ children: /* @__PURE__ */ y("div", {
209
215
  "data-tour": "conversation-list-standalone",
210
216
  className: "flex h-full min-h-0 w-full flex-col bg-bg-surface",
211
- children: X ? /* @__PURE__ */ b("div", {
217
+ children: X ? /* @__PURE__ */ y("div", {
212
218
  className: "px-4 py-8 text-sm text-text-secondary",
213
219
  children: A("conversations.loading", "Loading conversations...")
214
- }) : Z ? /* @__PURE__ */ b(S, {
215
- icon: /* @__PURE__ */ b(f, {
220
+ }) : Z ? /* @__PURE__ */ y(x, {
221
+ icon: /* @__PURE__ */ y(p, {
216
222
  size: 28,
217
223
  className: "text-status-error-text",
218
224
  "aria-hidden": "true"
219
225
  }),
220
226
  title: A("conversations.couldNotLoad", "Could not load conversations"),
221
227
  description: Z.message
222
- }) : K.length === 0 ? /* @__PURE__ */ b(S, {
223
- icon: $.icon,
228
+ }) : K.length === 0 ? /* @__PURE__ */ y(S, {
229
+ illustration: $.illustration,
224
230
  title: $.title,
225
231
  description: $.description
226
- }) : /* @__PURE__ */ b("div", {
232
+ }) : /* @__PURE__ */ y("div", {
227
233
  className: "flex flex-col gap-0.5 px-2 py-2",
228
- children: K.map((e) => /* @__PURE__ */ b(c, {
234
+ children: K.map((e) => /* @__PURE__ */ y(c, {
229
235
  conversation: e,
230
236
  isActive: N === e.id,
231
237
  onClick: Y
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationListPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationListPage.tsx"],"sourcesContent":["import {\n useDeferredValue,\n useEffect,\n useMemo,\n useState,\n useCallback,\n} from \"react\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport { useNavigate, useSearchParams } from \"react-router-dom\";\nimport { EmptyState } from \"@burdenoff/fe-libs/ui\";\nimport {\n Archive,\n AlertCircle,\n Hash,\n MessageCircle,\n Pin,\n Users,\n} from \"lucide-react\";\nimport { ConversationRow } from \"../components/ConversationRow\";\nimport {\n useSearchConversations,\n useGetConversations,\n} from \"../hooks/useConversationQueries\";\nimport { useConversationStream } from \"../hooks/useMessageSubscriptions\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport type {\n Conversation,\n ConversationFilter,\n ConversationState,\n} from \"../types\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { useTr } from \"../utils/i18n\";\nimport { joinPath } from \"../utils/navigation\";\n\n// Default filter shape applied whenever the `?filter=` URL param is absent\n// or unrecognised. Covers every dimension on `ConversationFilter` except\n// `searchQuery` — the text search is independent of URL-driven filters and\n// must not be wiped when the URL flips between filter values.\n//\n// The boolean toggles (hasUnread / isPinned / isMuted) are explicitly set\n// to `undefined` here (rather than omitted). Zustand's `set` merges\n// shallowly — the store's `updateConversationFilter` does\n// `{ ...currentFilter, ...partial }` — so an omitted key would preserve the\n// previous value, leaving a toggle like `hasUnread: true` sticky across a\n// URL that drops the filter param. Typing the constant as\n// `Partial<ConversationFilter>` widens the type so the explicit\n// `undefined` entries are accepted as `boolean | undefined` rather than\n// narrowed to the literal `undefined` under `as const`.\nconst FILTER_DEFAULTS: Partial<ConversationFilter> = {\n state: \"all\",\n type: \"all\",\n visibility: \"all\",\n hasUnread: undefined,\n isPinned: undefined,\n isMuted: undefined,\n};\n\nfunction useDebouncedValue<T>(value: T, delayMs: number): T {\n const [debouncedValue, setDebouncedValue] = useState(value);\n\n useEffect(() => {\n const timeoutId = window.setTimeout(\n () => setDebouncedValue(value),\n delayMs,\n );\n return () => window.clearTimeout(timeoutId);\n }, [delayMs, value]);\n\n return debouncedValue;\n}\n\nexport interface ConversationListPageProps {\n /**\n * Filter dimension forced by the parent inbox shell (driven by the\n * currently active tab). Mutually exclusive with the `?filter=` URL\n * param. When set, the URL param is ignored and the store filter is\n * driven by the prop on every change.\n */\n forcedFilterParam?: \"channel\" | \"direct\" | \"group\" | \"archived\";\n /**\n * When true the page renders bare-list chrome (no own header/search/\n * filter chips) so it can be embedded inside `InboxLayout`. Defaults to\n * false for backwards compatibility with the legacy `/conversations/list`\n * route.\n */\n embedded?: boolean;\n}\n\nexport function ConversationListPage({\n forcedFilterParam,\n embedded = false,\n}: ConversationListPageProps = {}) {\n const navigate = useNavigate();\n const { basePath, onConversationSelect } = useConversations();\n const bus = useEventBus();\n const tr = useTr();\n const {\n searchQuery,\n conversationFilter,\n updateConversationFilter,\n activeConversationId,\n setActiveConversationId,\n } = useConversationsStore();\n const [searchParams] = useSearchParams();\n const debouncedSearchQuery = useDebouncedValue(searchQuery, 250);\n const deferredSearchQuery = useDeferredValue(debouncedSearchQuery);\n const trimmedSearchQuery = deferredSearchQuery.trim();\n const isSearchActive = trimmedSearchQuery.length >= 2;\n\n // Sync URL filter param (?filter=channel|direct|group|archived) to the store\n // on mount / URL change. When the param is absent or unrecognised, reset\n // every filter dimension to its module-level default (`FILTER_DEFAULTS`) —\n // but deliberately do NOT touch `searchQuery`. The list's text search is\n // independent of URL-driven filters; zeroing it here would wipe in-progress\n // searches every time the URL flips between filter values or drops the\n // param entirely.\n useEffect(() => {\n const filterParam = forcedFilterParam ?? searchParams.get(\"filter\");\n\n if (!filterParam) {\n updateConversationFilter(FILTER_DEFAULTS);\n return;\n }\n\n const filterMap: Record<string, string> = {\n channel: \"CHANNEL\",\n direct: \"DIRECT\",\n group: \"GROUP\",\n archived: \"ARCHIVED\",\n };\n\n const mapped = filterMap[filterParam.toLowerCase()];\n if (!mapped) {\n updateConversationFilter(FILTER_DEFAULTS);\n return;\n }\n\n if (mapped === \"ARCHIVED\") {\n updateConversationFilter({ ...FILTER_DEFAULTS, state: \"ARCHIVED\" });\n } else {\n updateConversationFilter({\n ...FILTER_DEFAULTS,\n type: mapped as \"CHANNEL\" | \"DIRECT\" | \"GROUP\",\n });\n }\n }, [forcedFilterParam, searchParams, updateConversationFilter]);\n\n // Map filter.state (\"all\" | \"ACTIVE\" | \"ARCHIVED\" | \"DELETED\") to the\n // GraphQL `states: [ConversationState!]` filter. When the user is on the\n // Archived tab we send `states: [ARCHIVED]`; otherwise we omit the filter\n // so the backend default (ACTIVE only) kicks in.\n const stateFilter = useMemo<ConversationState[] | undefined>(() => {\n if (\n conversationFilter.state === \"all\" ||\n conversationFilter.state === \"ACTIVE\"\n ) {\n return undefined;\n }\n return [conversationFilter.state as ConversationState];\n }, [conversationFilter.state]);\n\n const {\n conversations: baseConversations,\n loading,\n error,\n } = useGetConversations({\n first: 50,\n types:\n conversationFilter.type === \"all\" ? undefined : [conversationFilter.type],\n states: stateFilter,\n hasUnread: conversationFilter.hasUnread,\n isPinned: conversationFilter.isPinned,\n });\n\n // Live conversation lifecycle stream — keeps the list current without polling.\n // Apollo cache updates are handled inside the hook (append on add, evict on\n // delete, normalisation handles updates).\n useConversationStream();\n\n const {\n conversations: searchedConversations,\n loading: searchLoading,\n error: searchError,\n } = useSearchConversations(deferredSearchQuery);\n\n const conversations = isSearchActive\n ? searchedConversations\n : baseConversations;\n\n // When the BE is queried with states:[ARCHIVED] the inbox tab is \"archived\",\n // so we skip the client-side state filter to avoid double-filtering.\n // For the inbox view the BE returns ACTIVE by default; no extra filter\n // needed here either.\n const filteredConversations = conversations;\n\n const pinnedConversations = useMemo(\n () => filteredConversations.filter((conversation) => conversation.isPinned),\n [filteredConversations],\n );\n\n const unpinnedConversations = useMemo(\n () =>\n filteredConversations.filter((conversation) => !conversation.isPinned),\n [filteredConversations],\n );\n\n const handleConversationClick = useCallback(\n (conversation: Conversation) => {\n setActiveConversationId(conversation.id);\n onConversationSelect?.(conversation);\n bus.emit(\"conversations.conversation.opened\", {\n conversationId: conversation.id,\n conversationType: conversation.type,\n });\n\n if (conversation.sourceContext?.contextHref) {\n try {\n const url = new URL(conversation.sourceContext.contextHref);\n if (url.origin === window.location.origin) {\n navigate(`${url.pathname}${url.search}${url.hash}`);\n return;\n }\n } catch {\n // Fall back to conversation detail below.\n }\n }\n\n navigate(joinPath(basePath || \"/conversations\", conversation.id));\n },\n [basePath, bus, navigate, onConversationSelect, setActiveConversationId],\n );\n\n const isLoading = loading || searchLoading;\n const activeError = error ?? searchError;\n const isArchivedTab =\n forcedFilterParam === \"archived\" || conversationFilter.state === \"ARCHIVED\";\n\n // Empty-state copy is tab-aware so users understand why a list is blank.\n const emptyState = useMemo(() => {\n if (isSearchActive) {\n return {\n title: tr(\"conversations.empty.search.title\", \"No matches\"),\n description: `${tr(\n \"conversations.empty.search.description\",\n \"No conversations matching\",\n )} \"${trimmedSearchQuery}\"`,\n icon: <MessageCircle size={28} className=\"text-text-secondary\" />,\n };\n }\n if (isArchivedTab) {\n return {\n title: tr(\n \"conversations.empty.archived.title\",\n \"No archived conversations\",\n ),\n description: tr(\n \"conversations.empty.archived.description\",\n \"Archived conversations are hidden from your main inbox. Move conversations here from the row actions menu.\",\n ),\n icon: <Archive size={28} className=\"text-text-secondary\" />,\n };\n }\n switch (forcedFilterParam) {\n case \"direct\":\n return {\n title: tr(\n \"conversations.empty.direct.title\",\n \"No direct conversations yet\",\n ),\n description: tr(\n \"conversations.empty.direct.description\",\n \"Start one with a workspace member from the New conversation button above.\",\n ),\n icon: <MessageCircle size={28} className=\"text-text-secondary\" />,\n };\n case \"group\":\n return {\n title: tr(\"conversations.empty.groups.title\", \"Create a group\"),\n description: tr(\n \"conversations.empty.groups.description\",\n \"Groups let you chat with multiple people in one thread. Tap New to start one.\",\n ),\n icon: <Users size={28} className=\"text-text-secondary\" />,\n };\n case \"channel\":\n return {\n title: tr(\n \"conversations.empty.channels.title\",\n \"Subscribe to a channel\",\n ),\n description: tr(\n \"conversations.empty.channels.description\",\n \"Channels are topic-based discussions visible to everyone who joins them.\",\n ),\n icon: <Hash size={28} className=\"text-text-secondary\" />,\n };\n default:\n return {\n title: tr(\"conversations.empty.all.title\", \"No conversations yet\"),\n description: tr(\n \"conversations.empty.all.description\",\n \"Start your first conversation with someone in this workspace.\",\n ),\n icon: <MessageCircle size={28} className=\"text-text-secondary\" />,\n };\n }\n }, [\n forcedFilterParam,\n isArchivedTab,\n isSearchActive,\n tr,\n trimmedSearchQuery,\n ]);\n\n // ─── Embedded mode: bare list, no own chrome (used inside InboxLayout) ───\n if (embedded) {\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <div\n data-tour=\"conversation-list\"\n className=\"flex h-full min-h-0 w-full flex-col\"\n aria-live=\"polite\"\n >\n {isLoading ? (\n <div\n role=\"status\"\n className=\"px-4 py-8 text-sm text-text-secondary\"\n >\n {tr(\"conversations.loading\", \"Loading conversations...\")}\n </div>\n ) : activeError ? (\n <EmptyState\n icon={\n <AlertCircle\n size={28}\n className=\"text-status-error-text\"\n aria-hidden=\"true\"\n />\n }\n title={tr(\n \"conversations.couldNotLoad\",\n \"Could not load conversations\",\n )}\n description={activeError.message}\n />\n ) : filteredConversations.length === 0 ? (\n <EmptyState\n icon={emptyState.icon}\n title={emptyState.title}\n description={emptyState.description}\n />\n ) : (\n <div\n data-tour=\"conversation-list-results\"\n className=\"flex flex-col gap-0.5 px-2 py-2\"\n >\n {pinnedConversations.length > 0 && (\n <>\n <div className=\"flex items-center gap-1.5 px-2 pt-2 pb-1\">\n <Pin\n size={11}\n className=\"text-text-secondary\"\n aria-hidden=\"true\"\n />\n <span className=\"text-[10px] font-semibold uppercase tracking-wider text-text-secondary\">\n {tr(\"conversations.pinned\", \"Pinned\")}\n </span>\n </div>\n {pinnedConversations.map((conversation) => (\n <ConversationRow\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n {unpinnedConversations.length > 0 && (\n <div className=\"flex items-center gap-1.5 px-2 pt-3 pb-1\">\n <span className=\"text-[10px] font-semibold uppercase tracking-wider text-text-secondary\">\n {tr(\"conversations.tabs.all\", \"All\")}\n </span>\n </div>\n )}\n </>\n )}\n {unpinnedConversations.map((conversation) => (\n <ConversationRow\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n </div>\n )}\n </div>\n </PermissionGuard>\n );\n }\n\n // ─── Standalone mode (legacy /conversations/list route) ───\n // Retains the original look but uses semantic tokens.\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <div\n data-tour=\"conversation-list-standalone\"\n className=\"flex h-full min-h-0 w-full flex-col bg-bg-surface\"\n >\n {isLoading ? (\n <div className=\"px-4 py-8 text-sm text-text-secondary\">\n {tr(\"conversations.loading\", \"Loading conversations...\")}\n </div>\n ) : activeError ? (\n <EmptyState\n icon={\n <AlertCircle\n size={28}\n className=\"text-status-error-text\"\n aria-hidden=\"true\"\n />\n }\n title={tr(\n \"conversations.couldNotLoad\",\n \"Could not load conversations\",\n )}\n description={activeError.message}\n />\n ) : filteredConversations.length === 0 ? (\n <EmptyState\n icon={emptyState.icon}\n title={emptyState.title}\n description={emptyState.description}\n />\n ) : (\n <div className=\"flex flex-col gap-0.5 px-2 py-2\">\n {filteredConversations.map((conversation) => (\n <ConversationRow\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n </div>\n )}\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ConversationListPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationListPage.tsx"],"sourcesContent":["import {\n useDeferredValue,\n useEffect,\n useMemo,\n useState,\n useCallback,\n} from \"react\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport { useNavigate, useSearchParams } from \"react-router-dom\";\nimport { EmptyState, IllustratedEmptyState } from \"@burdenoff/fe-libs/ui\";\nimport {\n Archive,\n AlertCircle,\n Hash,\n MessageCircle,\n Pin,\n Users,\n} from \"lucide-react\";\nimport { ConversationRow } from \"../components/ConversationRow\";\nimport {\n useSearchConversations,\n useGetConversations,\n} from \"../hooks/useConversationQueries\";\nimport { useConversationStream } from \"../hooks/useMessageSubscriptions\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport type {\n Conversation,\n ConversationFilter,\n ConversationState,\n} from \"../types\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { useTr } from \"../utils/i18n\";\nimport { joinPath } from \"../utils/navigation\";\n\n// Default filter shape applied whenever the `?filter=` URL param is absent\n// or unrecognised. Covers every dimension on `ConversationFilter` except\n// `searchQuery` — the text search is independent of URL-driven filters and\n// must not be wiped when the URL flips between filter values.\n//\n// The boolean toggles (hasUnread / isPinned / isMuted) are explicitly set\n// to `undefined` here (rather than omitted). Zustand's `set` merges\n// shallowly — the store's `updateConversationFilter` does\n// `{ ...currentFilter, ...partial }` — so an omitted key would preserve the\n// previous value, leaving a toggle like `hasUnread: true` sticky across a\n// URL that drops the filter param. Typing the constant as\n// `Partial<ConversationFilter>` widens the type so the explicit\n// `undefined` entries are accepted as `boolean | undefined` rather than\n// narrowed to the literal `undefined` under `as const`.\nconst FILTER_DEFAULTS: Partial<ConversationFilter> = {\n state: \"all\",\n type: \"all\",\n visibility: \"all\",\n hasUnread: undefined,\n isPinned: undefined,\n isMuted: undefined,\n};\n\nfunction useDebouncedValue<T>(value: T, delayMs: number): T {\n const [debouncedValue, setDebouncedValue] = useState(value);\n\n useEffect(() => {\n const timeoutId = window.setTimeout(\n () => setDebouncedValue(value),\n delayMs,\n );\n return () => window.clearTimeout(timeoutId);\n }, [delayMs, value]);\n\n return debouncedValue;\n}\n\nexport interface ConversationListPageProps {\n /**\n * Filter dimension forced by the parent inbox shell (driven by the\n * currently active tab). Mutually exclusive with the `?filter=` URL\n * param. When set, the URL param is ignored and the store filter is\n * driven by the prop on every change.\n */\n forcedFilterParam?: \"channel\" | \"direct\" | \"group\" | \"archived\";\n /**\n * When true the page renders bare-list chrome (no own header/search/\n * filter chips) so it can be embedded inside `InboxLayout`. Defaults to\n * false for backwards compatibility with the legacy `/conversations/list`\n * route.\n */\n embedded?: boolean;\n}\n\nexport function ConversationListPage({\n forcedFilterParam,\n embedded = false,\n}: ConversationListPageProps = {}) {\n const navigate = useNavigate();\n const { basePath, onConversationSelect } = useConversations();\n const bus = useEventBus();\n const tr = useTr();\n const {\n searchQuery,\n conversationFilter,\n updateConversationFilter,\n activeConversationId,\n setActiveConversationId,\n } = useConversationsStore();\n const [searchParams] = useSearchParams();\n const debouncedSearchQuery = useDebouncedValue(searchQuery, 250);\n const deferredSearchQuery = useDeferredValue(debouncedSearchQuery);\n const trimmedSearchQuery = deferredSearchQuery.trim();\n const isSearchActive = trimmedSearchQuery.length >= 2;\n\n // Sync URL filter param (?filter=channel|direct|group|archived) to the store\n // on mount / URL change. When the param is absent or unrecognised, reset\n // every filter dimension to its module-level default (`FILTER_DEFAULTS`) —\n // but deliberately do NOT touch `searchQuery`. The list's text search is\n // independent of URL-driven filters; zeroing it here would wipe in-progress\n // searches every time the URL flips between filter values or drops the\n // param entirely.\n useEffect(() => {\n const filterParam = forcedFilterParam ?? searchParams.get(\"filter\");\n\n if (!filterParam) {\n updateConversationFilter(FILTER_DEFAULTS);\n return;\n }\n\n const filterMap: Record<string, string> = {\n channel: \"CHANNEL\",\n direct: \"DIRECT\",\n group: \"GROUP\",\n archived: \"ARCHIVED\",\n };\n\n const mapped = filterMap[filterParam.toLowerCase()];\n if (!mapped) {\n updateConversationFilter(FILTER_DEFAULTS);\n return;\n }\n\n if (mapped === \"ARCHIVED\") {\n updateConversationFilter({ ...FILTER_DEFAULTS, state: \"ARCHIVED\" });\n } else {\n updateConversationFilter({\n ...FILTER_DEFAULTS,\n type: mapped as \"CHANNEL\" | \"DIRECT\" | \"GROUP\",\n });\n }\n }, [forcedFilterParam, searchParams, updateConversationFilter]);\n\n // Map filter.state (\"all\" | \"ACTIVE\" | \"ARCHIVED\" | \"DELETED\") to the\n // GraphQL `states: [ConversationState!]` filter. When the user is on the\n // Archived tab we send `states: [ARCHIVED]`; otherwise we omit the filter\n // so the backend default (ACTIVE only) kicks in.\n const stateFilter = useMemo<ConversationState[] | undefined>(() => {\n if (\n conversationFilter.state === \"all\" ||\n conversationFilter.state === \"ACTIVE\"\n ) {\n return undefined;\n }\n return [conversationFilter.state as ConversationState];\n }, [conversationFilter.state]);\n\n const {\n conversations: baseConversations,\n loading,\n error,\n } = useGetConversations({\n first: 50,\n types:\n conversationFilter.type === \"all\" ? undefined : [conversationFilter.type],\n states: stateFilter,\n hasUnread: conversationFilter.hasUnread,\n isPinned: conversationFilter.isPinned,\n });\n\n // Live conversation lifecycle stream — keeps the list current without polling.\n // Apollo cache updates are handled inside the hook (append on add, evict on\n // delete, normalisation handles updates).\n useConversationStream();\n\n const {\n conversations: searchedConversations,\n loading: searchLoading,\n error: searchError,\n } = useSearchConversations(deferredSearchQuery);\n\n const conversations = isSearchActive\n ? searchedConversations\n : baseConversations;\n\n // When the BE is queried with states:[ARCHIVED] the inbox tab is \"archived\",\n // so we skip the client-side state filter to avoid double-filtering.\n // For the inbox view the BE returns ACTIVE by default; no extra filter\n // needed here either.\n const filteredConversations = conversations;\n\n const pinnedConversations = useMemo(\n () => filteredConversations.filter((conversation) => conversation.isPinned),\n [filteredConversations],\n );\n\n const unpinnedConversations = useMemo(\n () =>\n filteredConversations.filter((conversation) => !conversation.isPinned),\n [filteredConversations],\n );\n\n const handleConversationClick = useCallback(\n (conversation: Conversation) => {\n setActiveConversationId(conversation.id);\n onConversationSelect?.(conversation);\n bus.emit(\"conversations.conversation.opened\", {\n conversationId: conversation.id,\n conversationType: conversation.type,\n });\n\n if (conversation.sourceContext?.contextHref) {\n try {\n const url = new URL(conversation.sourceContext.contextHref);\n if (url.origin === window.location.origin) {\n navigate(`${url.pathname}${url.search}${url.hash}`);\n return;\n }\n } catch {\n // Fall back to conversation detail below.\n }\n }\n\n navigate(joinPath(basePath || \"/conversations\", conversation.id));\n },\n [basePath, bus, navigate, onConversationSelect, setActiveConversationId],\n );\n\n const isLoading = loading || searchLoading;\n const activeError = error ?? searchError;\n const isArchivedTab =\n forcedFilterParam === \"archived\" || conversationFilter.state === \"ARCHIVED\";\n\n // Empty-state copy is tab-aware so users understand why a list is blank.\n const emptyState = useMemo(() => {\n if (isSearchActive) {\n return {\n title: tr(\"conversations.empty.search.title\", \"No matches\"),\n description: `${tr(\n \"conversations.empty.search.description\",\n \"No conversations matching\",\n )} \"${trimmedSearchQuery}\"`,\n icon: <MessageCircle size={28} className=\"text-text-secondary\" />,\n illustration: \"empty-search\",\n };\n }\n if (isArchivedTab) {\n return {\n title: tr(\n \"conversations.empty.archived.title\",\n \"No archived conversations\",\n ),\n description: tr(\n \"conversations.empty.archived.description\",\n \"Archived conversations are hidden from your main inbox. Move conversations here from the row actions menu.\",\n ),\n icon: <Archive size={28} className=\"text-text-secondary\" />,\n illustration: \"empty-files\",\n };\n }\n switch (forcedFilterParam) {\n case \"direct\":\n return {\n title: tr(\n \"conversations.empty.direct.title\",\n \"No direct conversations yet\",\n ),\n description: tr(\n \"conversations.empty.direct.description\",\n \"Start one with a workspace member from the New conversation button above.\",\n ),\n icon: <MessageCircle size={28} className=\"text-text-secondary\" />,\n illustration: \"empty-vibes\",\n };\n case \"group\":\n return {\n title: tr(\"conversations.empty.groups.title\", \"Create a group\"),\n description: tr(\n \"conversations.empty.groups.description\",\n \"Groups let you chat with multiple people in one thread. Tap New to start one.\",\n ),\n icon: <Users size={28} className=\"text-text-secondary\" />,\n illustration: \"empty-people\",\n };\n case \"channel\":\n return {\n title: tr(\n \"conversations.empty.channels.title\",\n \"Subscribe to a channel\",\n ),\n description: tr(\n \"conversations.empty.channels.description\",\n \"Channels are topic-based discussions visible to everyone who joins them.\",\n ),\n icon: <Hash size={28} className=\"text-text-secondary\" />,\n illustration: \"empty-vibes\",\n };\n default:\n return {\n title: tr(\"conversations.empty.all.title\", \"No conversations yet\"),\n description: tr(\n \"conversations.empty.all.description\",\n \"Start your first conversation with someone in this workspace.\",\n ),\n icon: <MessageCircle size={28} className=\"text-text-secondary\" />,\n illustration: \"empty-vibes\",\n };\n }\n }, [\n forcedFilterParam,\n isArchivedTab,\n isSearchActive,\n tr,\n trimmedSearchQuery,\n ]);\n\n // ─── Embedded mode: bare list, no own chrome (used inside InboxLayout) ───\n if (embedded) {\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <div\n data-tour=\"conversation-list\"\n className=\"flex h-full min-h-0 w-full flex-col\"\n aria-live=\"polite\"\n >\n {isLoading ? (\n <div\n role=\"status\"\n className=\"px-4 py-8 text-sm text-text-secondary\"\n >\n {tr(\"conversations.loading\", \"Loading conversations...\")}\n </div>\n ) : activeError ? (\n <EmptyState\n icon={\n <AlertCircle\n size={28}\n className=\"text-status-error-text\"\n aria-hidden=\"true\"\n />\n }\n title={tr(\n \"conversations.couldNotLoad\",\n \"Could not load conversations\",\n )}\n description={activeError.message}\n />\n ) : filteredConversations.length === 0 ? (\n <IllustratedEmptyState\n illustration={emptyState.illustration}\n title={emptyState.title}\n description={emptyState.description}\n />\n ) : (\n <div\n data-tour=\"conversation-list-results\"\n className=\"flex flex-col gap-0.5 px-2 py-2\"\n >\n {pinnedConversations.length > 0 && (\n <>\n <div className=\"flex items-center gap-1.5 px-2 pt-2 pb-1\">\n <Pin\n size={11}\n className=\"text-text-secondary\"\n aria-hidden=\"true\"\n />\n <span className=\"text-[10px] font-semibold uppercase tracking-wider text-text-secondary\">\n {tr(\"conversations.pinned\", \"Pinned\")}\n </span>\n </div>\n {pinnedConversations.map((conversation) => (\n <ConversationRow\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n {unpinnedConversations.length > 0 && (\n <div className=\"flex items-center gap-1.5 px-2 pt-3 pb-1\">\n <span className=\"text-[10px] font-semibold uppercase tracking-wider text-text-secondary\">\n {tr(\"conversations.tabs.all\", \"All\")}\n </span>\n </div>\n )}\n </>\n )}\n {unpinnedConversations.map((conversation) => (\n <ConversationRow\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n </div>\n )}\n </div>\n </PermissionGuard>\n );\n }\n\n // ─── Standalone mode (legacy /conversations/list route) ───\n // Retains the original look but uses semantic tokens.\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <div\n data-tour=\"conversation-list-standalone\"\n className=\"flex h-full min-h-0 w-full flex-col bg-bg-surface\"\n >\n {isLoading ? (\n <div className=\"px-4 py-8 text-sm text-text-secondary\">\n {tr(\"conversations.loading\", \"Loading conversations...\")}\n </div>\n ) : activeError ? (\n <EmptyState\n icon={\n <AlertCircle\n size={28}\n className=\"text-status-error-text\"\n aria-hidden=\"true\"\n />\n }\n title={tr(\n \"conversations.couldNotLoad\",\n \"Could not load conversations\",\n )}\n description={activeError.message}\n />\n ) : filteredConversations.length === 0 ? (\n <IllustratedEmptyState\n illustration={emptyState.illustration}\n title={emptyState.title}\n description={emptyState.description}\n />\n ) : (\n <div className=\"flex flex-col gap-0.5 px-2 py-2\">\n {filteredConversations.map((conversation) => (\n <ConversationRow\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n </div>\n )}\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}