@burdenoff/microfe-conversations 2026.626.1 → 2026.731.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conversations/components/NewConversationDialog.js +143 -125
- package/dist/conversations/components/NewConversationDialog.js.map +1 -1
- package/dist/conversations/components/QuotaExhaustedDialog.js +30 -0
- package/dist/conversations/components/QuotaExhaustedDialog.js.map +1 -0
- package/dist/conversations/hooks/useConversationQueries.js +5 -3
- package/dist/conversations/hooks/useConversationQueries.js.map +1 -1
- package/dist/conversations/hooks/useMessageQueries.js +5 -3
- package/dist/conversations/hooks/useMessageQueries.js.map +1 -1
- package/dist/conversations/pages/ConversationDetailPage.js +408 -385
- package/dist/conversations/pages/ConversationDetailPage.js.map +1 -1
- package/dist/conversations/pages/ConversationsLandingPage.js +2 -2
- package/dist/conversations/pages/ConversationsLandingPage.js.map +1 -1
- package/dist/conversations/pages/NewConversationPage.js +25 -15
- package/dist/conversations/pages/NewConversationPage.js.map +1 -1
- package/package.json +6 -6
|
@@ -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 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":";;;;;;;;;;;;;;;;;;AA8CA,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,UAAM,IAAS,EACjB,IAAK,GACR,GAAa,MAA6B;EACzC,IAAM,IAAQ,GAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;IAEpC,CAAC,GAAE,CACJ,EACK,EACJ,sBACA,4BACA,mBACA,qBACA,+BACE,GAAuB,EAErB,CAAC,GAAc,MAAmB,EACtC,GAAc,KAAkB,OAAO,GACxC,EACK,CAAC,GAAqB,KAA0B,EAAoB,EAAE,CAAC,EACvE,KAAiB,EAAuB,KAAK,EAC7C,IAAc,EAA4B,KAAK,EAE/C,IAAgB,QAAkB;AAEtC,EADA,EAAY,SAAS,OAAO,EAC5B,EAAY,SAAS,eAAe,EAAE,UAAU,UAAU,CAAC;IAC1D,EAAE,CAAC,EAEA,EACJ,iBACA,SAAS,GACT,uBACA,SAAS,IACT,OAAO,MACL,GAAmB,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,KAAe,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,OAAgB,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,KAA0B,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,GAAgB,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,KAAmB,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,IAAY,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,IAAoB,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,EAMpC,KAAkB,QAA0B;EAChD,IAAM,IAAoB,CACxB;GACE,IAAI;GACJ,OAAO,EACL,sCACA,wBACD;GACD,aAAa,EACX,4CACA,mDACD;GACD,MAAM,kBAAC,GAAD;IAAgB,MAAM;IAAI,eAAY;IAAS,CAAA;GACrD,SAAS;GACT,MAAM,EAAkB,SAAS;GAClC,CACF;AA8CD,SA7CI,GAAc,WAChB,EAAM,KAAK;GACT,IAAI;GACJ,OAAO,EACL,8CACA,sBACD;GACD,aAAa,EACX,oDACA,kDACD;GACD,MAAM,kBAAC,IAAD;IAAO,MAAM;IAAI,eAAY;IAAS,CAAA;GAC5C,eAAe;AACb,IAAK,KAAyB,GAAyB;;GAE1D,CAAC,EAEA,GAAe,eACjB,EAAM,KAAK;GACT,IAAI;GACJ,OAAO,EACL,yCACA,wBACD;GACD,aAAa,EACX,+CACA,yDACD;GACD,MAAM,kBAAC,IAAD;IAAc,MAAM;IAAI,eAAY;IAAS,CAAA;GACnD,SAAS;GACV,CAAC,EAEJ,EAAM,KAAK;GACT,IAAI;GACJ,OAAO,GAAc,WACjB,EAAG,uCAAuC,0BAA0B,GACpE,EAAG,qCAAqC,uBAAuB;GACnE,aAAa,EACX,2CACA,yDACD;GACD,MAAM,kBAAC,GAAD;IAAK,MAAM;IAAI,eAAY;IAAS,CAAA;GAC1C,eAAe,KAAK,GAAW;GAC/B,MAAM,GAAc;GACrB,CAAC,EACK;IACN;EACD,GAAc;EACd,GAAc;EACd,EAAkB;EAClB;EACA;EACA;EACA;EACA,GAAe;EACf;EACA;EACD,CAAC;AA4CF,QA1CI,CAAC,KAAgB,KAEjB,kBAAC,OAAD;EAAK,WAAU;YACZ,EAAG,yBAAyB,0BAA0B;EACnD,CAAA,GAIN,CAAC,KAAgB,IAEjB,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,IAAD;GACE,cAAa;GACb,OACE,IACI,EAAG,kCAAkC,2BAA2B,GAChE,EAAG,0BAA0B,yBAAyB;GAE5D,aACE,IACI,EACE,6BACA,wDACD,GACD,EACE,qCACA,yFACD;GAEP,QACE,kBAAC,IAAD;IACE,MAAK;IACL,eAAe,EAAS,KAAY,iBAAiB;cAEpD,EAAG,sBAAsB,wBAAwB;IAC3C,CAAA;GAEX,CAAA;EACE,CAAA,GAKR,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,GAAD,EAAK,MAAM,IAAM,CAAA;WAGrB,OACE,EAAa,WACT,EACE,mCACA,qBACD,GACD,EACE,iCACA,mBACD;WAEP,eAAe,KAAK,GAAW;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;iBAAf,CACE,kBAAC,IAAD;QACE,cAAa;QACb,OAAO,EAAG,iCAAiC,kBAAkB;QAC7D,aAAa,EACX,+BACA,gFACD;QACD,QACE,kBAAC,IAAD;SAAQ,MAAK;SAAS,SAAS;mBAA/B,CACE,kBAAC,GAAD;UAAgB,MAAM;UAAI,eAAY;UAAS,CAAA,EAC9C,EAAG,mCAAmC,kBAAkB,CAClD;;QAEX,CAAA,EACF,kBAAC,IAAD;QACE,WAAU;QACV,YAAW;QACX,OAAO,EACL,iCACA,wCACD;QACD,OAAO;QACP,CAAA,CACE;;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,IAAkB;;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,IAAkB,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;kBACf,EAAW;QACL,CAAA;;OACN,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,KAAK;QACL,OAAO;QACP,WAAW,MAAU,GAAgB,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,GAAD,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,GAAD,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,KAA0B,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,eAOC;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 { isQuotaExhaustedError } from \"@burdenoff/fe-libs/shared/utils\";\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 { QuotaExhaustedDialog } from \"../components/QuotaExhaustedDialog\";\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 [showMessageQuotaExhausted, setShowMessageQuotaExhausted] =\n useState(false);\n const [locallySentMessages, setLocallySentMessages] = useState<Message[]>([]);\n const messagesEndRef = useRef<HTMLDivElement>(null);\n const composerRef = useRef<HTMLTextAreaElement>(null);\n\n const focusComposer = useCallback(() => {\n composerRef.current?.focus();\n composerRef.current?.scrollIntoView({ behavior: \"smooth\" });\n }, []);\n\n const {\n conversation,\n members: conversationMembers,\n membersTotalCount,\n loading: conversationLoading,\n error: conversationError,\n } = useGetConversation(conversationId ?? \"\");\n const {\n messages,\n loading: messagesLoading,\n error: messagesError,\n refetch: refetchMessages,\n } = useGetMessages(conversationId ?? \"\", {\n first: MESSAGES_PAGE_SIZE,\n pollInterval: MESSAGE_REFRESH_FALLBACK_MS,\n });\n // Subscribe to live message events; the query has a light polling fallback for\n // local/federated transports that keep the stream open but miss events.\n useMessageStreamForConversation(conversationId, MESSAGES_PAGE_SIZE);\n const { sendMessage, loading: sendingMessage } = useSendMessage();\n const { editMessage, loading: editingMessage } = useEditMessage();\n const {\n deleteMessage,\n addReaction,\n removeReaction,\n pinMessage,\n unpinMessage,\n } = useMessageActions();\n const {\n archiveConversation,\n pinConversation,\n unpinConversation,\n muteConversation,\n unmuteConversation,\n leaveConversation,\n } = useConversationActions();\n\n const [moreMenuOpen, setMoreMenuOpen] = useState(false);\n const [editingMessageId, setEditingMessageId] = useState<string | null>(null);\n const [editingContent, setEditingContent] = useState(\"\");\n const [replyingTo, setReplyingTo] = useState<{\n id: string;\n content: string;\n sender: string;\n } | null>(null);\n const moreMenuRef = useRef<HTMLDivElement>(null);\n\n const currentUserId = currentUser?.id ?? \"\";\n const sourceContext = conversation?.sourceContext;\n\n const displayedMessages = useMemo(() => {\n const byId = new Map<string, Message>();\n for (const message of locallySentMessages) {\n if (message.conversationId !== conversationId) continue;\n byId.set(message.id, message);\n }\n for (const message of messages) {\n byId.set(message.id, message);\n }\n return Array.from(byId.values()).sort(\n (a, b) =>\n new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),\n );\n }, [conversationId, locallySentMessages, messages]);\n\n useEffect(() => {\n messagesEndRef.current?.scrollIntoView({ behavior: \"smooth\" });\n }, [displayedMessages.length]);\n\n useEffect(() => {\n setLocallySentMessages([]);\n }, [conversationId]);\n\n useEffect(() => {\n if (locallySentMessages.length === 0 || messages.length === 0) return;\n const serverIds = new Set(messages.map((message) => message.id));\n setLocallySentMessages((current) => {\n const pending = current.filter((message) => !serverIds.has(message.id));\n return pending.length === current.length ? current : pending;\n });\n }, [locallySentMessages.length, messages, setLocallySentMessages]);\n\n useEffect(() => {\n if (!conversationId) return;\n if (messageInput.trim()) {\n setMessageDraft(conversationId, messageInput);\n return;\n }\n clearMessageDraft(conversationId);\n }, [messageInput, conversationId, setMessageDraft, clearMessageDraft]);\n\n const messageAuthorNames = useMemo(() => {\n const byAuthor = new Map<string, string>();\n for (const message of displayedMessages) {\n if (!message.authorId || byAuthor.has(message.authorId)) continue;\n if (!message.sender.name) continue;\n byAuthor.set(message.authorId, message.sender.name);\n }\n return byAuthor;\n }, [displayedMessages]);\n\n const participants = useMemo(() => {\n if (conversationMembers.length > 0) {\n return conversationMembers\n .filter((member) => member.status === \"ACTIVE\" && !member.leftAt)\n .map((member) => ({\n id: member.userId,\n name:\n messageAuthorNames.get(member.userId) ||\n member.customTitle ||\n tr(\"conversations.member\", \"Member\"),\n role: member.role,\n }));\n }\n\n return Array.from(messageAuthorNames.entries()).map(([id, name]) => ({\n id,\n name,\n role: tr(\"conversations.member\", \"Member\"),\n }));\n }, [conversationMembers, messageAuthorNames, tr]);\n\n const hiddenParticipantsCount = conversation?.memberCount\n ? Math.max(conversation.memberCount - participants.length, 0)\n : 0;\n\n const handleSendMessage = useCallback(async () => {\n if (!conversationId || !messageInput.trim() || sendingMessage) return;\n let message: Message | null;\n try {\n message = await sendMessage({\n conversationId,\n content: messageInput.trim(),\n type: \"TEXT\",\n parentMessageId: replyingTo?.id,\n refetchFirst: MESSAGES_PAGE_SIZE,\n });\n } catch (error) {\n if (isQuotaExhaustedError(error)) {\n setShowMessageQuotaExhausted(true);\n return;\n }\n nativeNotify(\"error\");\n throw error;\n }\n if (!message) return;\n setLocallySentMessages((current) => [...current, message]);\n bus.emit(\"conversations.message.sent\", {\n messageId: message.id,\n conversationId,\n });\n setMessageInput(\"\");\n setReplyingTo(null);\n clearMessageDraft(conversationId);\n }, [\n bus,\n conversationId,\n messageInput,\n sendingMessage,\n sendMessage,\n clearMessageDraft,\n replyingTo,\n ]);\n\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (event.key !== \"Enter\" || event.shiftKey) return;\n event.preventDefault();\n void handleSendMessage();\n },\n [handleSendMessage],\n );\n\n // Emit opened event so sinks (analytics/audit) can record the view\n useEffect(() => {\n if (!conversationId) return;\n bus.emit(\"conversations.conversation.opened\", { conversationId });\n }, [bus, conversationId]);\n\n // Close more-menu on outside click\n useEffect(() => {\n if (!moreMenuOpen) return;\n const handler = (e: MouseEvent) => {\n if (\n moreMenuRef.current &&\n !moreMenuRef.current.contains(e.target as Node)\n ) {\n setMoreMenuOpen(false);\n }\n };\n document.addEventListener(\"mousedown\", handler);\n return () => document.removeEventListener(\"mousedown\", handler);\n }, [moreMenuOpen]);\n\n const handleReply = useCallback(\n (message: { id: string; content: string; sender: { name: string } }) => {\n setReplyingTo({\n id: message.id,\n content: message.content,\n sender: message.sender.name,\n });\n },\n [],\n );\n\n const handleReact = useCallback(\n async (messageId: string, emoji: string) => {\n try {\n await addReaction(messageId, emoji);\n } catch {\n // If already reacted, toggle off\n await removeReaction(messageId, emoji).catch(() => {});\n }\n },\n [addReaction, removeReaction],\n );\n\n const handleEdit = useCallback((message: { id: string; content: string }) => {\n setEditingMessageId(message.id);\n setEditingContent(message.content);\n }, []);\n\n const handleEditSubmit = useCallback(async () => {\n if (!editingMessageId || !editingContent.trim() || editingMessage) return;\n await editMessage(editingMessageId, editingContent.trim());\n setEditingMessageId(null);\n setEditingContent(\"\");\n }, [editingMessageId, editingContent, editingMessage, editMessage]);\n\n const handleEditCancel = useCallback(() => {\n setEditingMessageId(null);\n setEditingContent(\"\");\n }, []);\n\n const handleDelete = useCallback(\n async (messageId: string) => {\n const confirmed = window.confirm(\n tr(\n \"conversations.confirmDeleteMessage\",\n \"Delete this message? This action cannot be undone.\",\n ),\n );\n if (!confirmed) return;\n nativeImpact(\"medium\");\n try {\n await deleteMessage(messageId);\n void refetchMessages();\n nativeNotify(\"success\");\n } catch (error) {\n console.error(\"[microfe-conversations] message delete failed\", error);\n nativeNotify(\"error\");\n }\n },\n [deleteMessage, refetchMessages, tr],\n );\n\n const handleTogglePinMessage = useCallback(\n async (messageId: string, isPinned: boolean) => {\n if (isPinned) {\n await unpinMessage(messageId);\n return;\n }\n await pinMessage(messageId);\n },\n [pinMessage, unpinMessage],\n );\n\n const handlePin = useCallback(async () => {\n if (!conversationId) return;\n if (conversation?.isPinned) {\n await unpinConversation(conversationId);\n } else {\n await pinConversation(conversationId);\n }\n setMoreMenuOpen(false);\n }, [\n conversationId,\n conversation?.isPinned,\n pinConversation,\n unpinConversation,\n ]);\n\n const handleMute = useCallback(async () => {\n if (!conversationId) return;\n if (conversation?.isMuted) {\n await unmuteConversation(conversationId);\n } else {\n await muteConversation(conversationId);\n }\n setMoreMenuOpen(false);\n }, [\n conversationId,\n conversation?.isMuted,\n muteConversation,\n unmuteConversation,\n ]);\n\n const handleArchive = useCallback(async () => {\n if (!conversationId) return;\n try {\n await archiveConversation(conversationId);\n bus.emit(\"conversations.conversation.archived\", { conversationId });\n setMoreMenuOpen(false);\n navigate(basePath || \"/conversations\");\n } catch (error) {\n console.error(\n \"[microfe-conversations] conversation archive failed\",\n error,\n );\n nativeNotify(\"error\");\n }\n }, [archiveConversation, basePath, bus, conversationId, navigate]);\n\n const handleLeave = useCallback(async () => {\n if (!conversationId) return;\n const confirmed = window.confirm(\n tr(\n \"conversations.confirmLeaveConversation\",\n \"Leave this conversation? You will lose access unless someone adds you again.\",\n ),\n );\n if (!confirmed) return;\n try {\n await leaveConversation(conversationId);\n if (currentUserId) {\n bus.emit(\"conversations.member.removed\", {\n conversationId,\n userId: currentUserId,\n });\n }\n setMoreMenuOpen(false);\n navigate(basePath || \"/conversations\");\n } catch (error) {\n console.error(\"[microfe-conversations] conversation leave failed\", error);\n nativeNotify(\"error\");\n }\n }, [\n basePath,\n bus,\n conversationId,\n currentUserId,\n leaveConversation,\n navigate,\n tr,\n ]);\n\n const handleOpenContext = useCallback(() => {\n if (!sourceContext?.contextHref) return;\n try {\n const url = new URL(sourceContext.contextHref);\n if (url.origin === window.location.origin) {\n navigate(`${url.pathname}${url.search}${url.hash}`);\n return;\n }\n } catch {\n // Fall through to full navigation for relative or invalid URLs.\n }\n\n window.location.assign(sourceContext.contextHref);\n }, [navigate, sourceContext?.contextHref]);\n\n // Concrete next actions surfaced on the (complex) detail page so a new user\n // isn't left staring at an unfamiliar surface. Each step is wired to a real\n // handler already present on this page; steps that don't apply to the\n // current conversation type/state are filtered out.\n const detailNextSteps = useMemo<NextStep[]>(() => {\n const steps: NextStep[] = [\n {\n id: \"send-message\",\n label: tr(\n \"conversations.nextSteps.send.label\",\n \"Send your first reply\",\n ),\n description: tr(\n \"conversations.nextSteps.send.description\",\n \"Jump to the composer and start the conversation.\",\n ),\n icon: <SendHorizontal size={14} aria-hidden=\"true\" />,\n onClick: focusComposer,\n done: displayedMessages.length > 0,\n },\n ];\n if (conversation?.isGroup) {\n steps.push({\n id: \"view-participants\",\n label: tr(\n \"conversations.nextSteps.participants.label\",\n \"Review participants\",\n ),\n description: tr(\n \"conversations.nextSteps.participants.description\",\n \"See who can read and post in this conversation.\",\n ),\n icon: <Users size={14} aria-hidden=\"true\" />,\n onClick: () => {\n if (!isParticipantsPanelOpen) toggleParticipantsPanel();\n },\n });\n }\n if (sourceContext?.contextHref) {\n steps.push({\n id: \"open-context\",\n label: tr(\n \"conversations.nextSteps.context.label\",\n \"Open the related item\",\n ),\n description: tr(\n \"conversations.nextSteps.context.description\",\n \"Jump to the page or record this conversation is about.\",\n ),\n icon: <ExternalLink size={14} aria-hidden=\"true\" />,\n onClick: handleOpenContext,\n });\n }\n steps.push({\n id: \"pin-conversation\",\n label: conversation?.isPinned\n ? tr(\"conversations.nextSteps.unpin.label\", \"Unpin this conversation\")\n : tr(\"conversations.nextSteps.pin.label\", \"Pin for quick access\"),\n description: tr(\n \"conversations.nextSteps.pin.description\",\n \"Keep important conversations at the top of your inbox.\",\n ),\n icon: <Pin size={14} aria-hidden=\"true\" />,\n onClick: () => void handlePin(),\n done: conversation?.isPinned,\n });\n return steps;\n }, [\n conversation?.isGroup,\n conversation?.isPinned,\n displayedMessages.length,\n focusComposer,\n handleOpenContext,\n handlePin,\n isParticipantsPanelOpen,\n sourceContext?.contextHref,\n toggleParticipantsPanel,\n tr,\n ]);\n\n if (!conversation && conversationLoading) {\n return (\n <div className=\"flex items-center justify-center h-[calc(100vh-64px)] text-sm text-text-secondary\">\n {tr(\"conversations.loading\", \"Loading conversation...\")}\n </div>\n );\n }\n\n if (!conversation || conversationError) {\n return (\n <div className=\"flex h-[calc(100vh-64px)] items-center justify-center p-8\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={\n conversationError\n ? tr(\"conversations.unavailableTitle\", \"Conversation unavailable\")\n : tr(\"conversations.notFound\", \"Conversation not found\")\n }\n description={\n conversationError\n ? tr(\n \"conversations.unavailable\",\n \"We couldn't load this conversation. Please try again.\",\n )\n : tr(\n \"conversations.notFoundDescription\",\n \"This conversation may have been deleted, archived, or you no longer have access to it.\",\n )\n }\n action={\n <Button\n type=\"button\"\n onClick={() => navigate(basePath || \"/conversations\")}\n >\n {tr(\"conversations.back\", \"Back to conversations\")}\n </Button>\n }\n />\n </div>\n );\n }\n\n return (\n <div className=\"relative flex h-[calc(100vh-64px)] bg-bg-surface\">\n <div className=\"flex-1 flex flex-col min-w-0\">\n <header\n data-tour=\"conversation-detail-header\"\n className=\"flex items-center justify-between px-4 py-3 border-b border-border-seam bg-bg-surface/95 backdrop-blur-sm\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <button\n type=\"button\"\n onClick={() => navigate(basePath || \"/conversations\")}\n className=\"flex min-h-10 min-w-10 items-center justify-center rounded-lg text-text-secondary transition-colors hover:bg-bg-sunken/50 hover:text-text-primary\"\n >\n <ArrowLeft size={18} />\n </button>\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-1.5\">\n {conversation.type === \"CHANNEL\" && (\n <Hash\n size={14}\n className=\"text-text-secondary flex-shrink-0\"\n />\n )}\n <h2 className=\"text-sm font-semibold text-text-primary truncate\">\n {conversation.title}\n </h2>\n </div>\n <p className=\"text-xs text-text-secondary\">\n {conversation.type === \"DIRECT\"\n ? tr(\n \"conversations.directConversation\",\n \"Direct conversation\",\n )\n : conversation.type === \"CHANNEL\"\n ? `${tr(\"conversations.channel\", \"Channel\")} · ${conversation.memberCount} ${tr(\"conversations.membersCountLabel\", \"members\")}`\n : conversation.type === \"ANNOUNCEMENT\"\n ? `${tr(\"conversations.announcement\", \"Announcement\")} · ${conversation.memberCount} ${tr(\"conversations.membersCountLabel\", \"members\")}`\n : `${conversation.memberCount} ${tr(\"conversations.membersCountLabel\", \"members\")}`}\n </p>\n </div>\n </div>\n\n <div className=\"flex shrink-0 items-center gap-1\">\n {sourceContext?.contextHref && (\n <HeaderButton\n dataTour=\"conversation-open-context\"\n icon={<ExternalLink size={16} />}\n label={tr(\"conversations.openContext\", \"Open context\")}\n onClick={handleOpenContext}\n />\n )}\n {conversation.isGroup && (\n <HeaderButton\n dataTour=\"conversation-participants-button\"\n icon={<Users size={16} />}\n label={tr(\"conversations.participants\", \"Participants\")}\n active={isParticipantsPanelOpen}\n onClick={toggleParticipantsPanel}\n />\n )}\n <div className=\"relative\" ref={moreMenuRef}>\n <HeaderButton\n icon={<MoreVertical size={16} />}\n label={tr(\"conversations.moreOptions\", \"More options\")}\n active={moreMenuOpen}\n onClick={() => setMoreMenuOpen((prev) => !prev)}\n />\n {moreMenuOpen && (\n <div className=\"absolute right-0 top-full mt-1 w-48 bg-popover border border-border-subtle rounded-lg shadow-lg z-50 py-1 animate-in fade-in-0 zoom-in-95 duration-100\">\n <DropdownItem\n icon={\n conversation.isPinned ? (\n <PinOff size={14} />\n ) : (\n <Pin size={14} />\n )\n }\n label={\n conversation.isPinned\n ? tr(\n \"conversations.unpinConversation\",\n \"Unpin conversation\",\n )\n : tr(\n \"conversations.pinConversation\",\n \"Pin conversation\",\n )\n }\n onClick={() => void handlePin()}\n />\n <DropdownItem\n icon={\n conversation.isMuted ? (\n <Bell size={14} />\n ) : (\n <BellOff size={14} />\n )\n }\n label={\n conversation.isMuted\n ? tr(\n \"conversations.unmuteConversation\",\n \"Unmute conversation\",\n )\n : tr(\n \"conversations.muteConversation\",\n \"Mute conversation\",\n )\n }\n onClick={() => void handleMute()}\n />\n <div className=\"h-px bg-border my-1\" />\n <DropdownItem\n icon={<Archive size={14} />}\n label={tr(\n \"conversations.archiveConversation\",\n \"Archive conversation\",\n )}\n onClick={() => void handleArchive()}\n destructive\n />\n {conversation.isGroup && (\n <DropdownItem\n icon={<LogOut size={14} />}\n label={tr(\n \"conversations.leaveConversation\",\n \"Leave conversation\",\n )}\n onClick={() => void handleLeave()}\n destructive\n />\n )}\n </div>\n )}\n </div>\n </div>\n </header>\n\n <div className=\"flex-1 overflow-y-auto py-4 space-y-0.5\">\n {sourceContext?.pagePath && (\n <div className=\"mx-4 mb-4 rounded-lg border border-border-subtle bg-bg-sunken/20 px-3 py-2 text-xs text-text-secondary\">\n <span className=\"font-medium text-text-primary\">\n {sourceContext.contextTitle || sourceContext.pagePath}\n </span>\n {sourceContext.entityType && sourceContext.entityId\n ? ` · ${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 <QuotaExhaustedDialog\n open={showMessageQuotaExhausted}\n title={tr(\n \"conversations.quota.messages.title\",\n \"Message Quota Reached\",\n )}\n description={tr(\n \"conversations.quota.messages.description\",\n \"You've reached the message limit for your current plan. View billing to upgrade or add more capacity.\",\n )}\n onClose={() => setShowMessageQuotaExhausted(false)}\n onConfirm={() => {\n setShowMessageQuotaExhausted(false);\n navigate(\"/billing\");\n }}\n />\n </div>\n );\n}\n\nfunction DropdownItem({\n icon,\n label,\n onClick,\n destructive,\n}: {\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n destructive?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className={cn(\n \"flex items-center gap-2.5 w-full px-3 py-2 text-sm transition-colors text-left\",\n destructive\n ? \"text-status-error-text hover:bg-status-error-bg/10\"\n : \"text-text-primary hover:bg-bg-sunken/50\",\n )}\n >\n {icon}\n <span>{label}</span>\n </button>\n );\n}\n\nfunction HeaderButton({\n icon,\n label,\n active,\n dataTour,\n onClick,\n}: {\n icon: React.ReactNode;\n label: string;\n active?: boolean;\n dataTour?: string;\n onClick?: () => void;\n}) {\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <button\n data-tour={dataTour}\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n title={label}\n className={cn(\n \"flex min-h-10 min-w-10 items-center justify-center rounded-lg transition-colors\",\n active\n ? \"bg-action-primary-bg/10 text-primary\"\n : \"text-text-secondary hover:text-text-primary hover:bg-bg-sunken/50\",\n )}\n >\n {icon}\n </button>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConversationsLandingPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationsLandingPage.tsx"],"sourcesContent":["import { Suspense, useCallback, useEffect, useMemo, lazy } from \"react\";\nimport { useNavigate, useParams, useSearchParams } from \"react-router-dom\";\nimport { PageContainer } from \"@burdenoff/fe-libs/ui\";\nimport { Pin, Filter, Loader2 } from \"lucide-react\";\nimport { ConversationListPage } from \"./ConversationListPage\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport {\n InboxLayout,\n type FilterChipDescriptor,\n type InboxTabId,\n} from \"../components/InboxLayout\";\nimport { NewConversationDialog } from \"../components/NewConversationDialog\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { useUserFeatureFlags } from \"../hooks/useUserFeatureFlags\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport { useTr } from \"../utils/i18n\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { joinPath } from \"../utils/navigation\";\n\n// Lazy-load the detail page so the inbox bundle stays small for users who\n// never open a conversation in this session. The detail subtree is the\n// heaviest by far (~19kB gz including MessageBubble).\nconst ConversationDetailPage = lazy(() =>\n import(\"./ConversationDetailPage\").then((m) => ({\n default: m.ConversationDetailPage,\n })),\n);\n\nconst DEFAULT_TAB_ID: InboxTabId = \"all\";\n\n// Maps the inbox tab id to the `forcedFilterParam` prop accepted by\n// ConversationListPage. The \"all\" tab passes undefined so the list shows\n// every conversation type. The archived tab triggers the BE state filter\n// (states: [ARCHIVED]) handled inside ConversationListPage.\nconst TAB_TO_LIST_FILTER: Record<\n InboxTabId,\n \"channel\" | \"direct\" | \"group\" | \"archived\" | undefined\n> = {\n all: undefined,\n direct: \"direct\",\n groups: \"group\",\n channels: \"channel\",\n archived: \"archived\",\n};\n\n/**\n * Landing page for `/conversations`.\n *\n * Renders the canonical inbox: two-pane shell with the conversation list on\n * the left and an empty detail panel on the right. The detail panel is\n * filled in by the router on `/conversations/:conversationId`; this page\n * itself only renders the chrome and the list.\n *\n * Tabs (All / Direct / Groups / Channels / Archived) are stored in the\n * `?tab=` URL search param so refreshing the page keeps the user on the\n * same view.\n */\nexport function ConversationsLandingPage() {\n const [params, setParams] = useSearchParams();\n const { conversationId } = useParams<{ conversationId?: string }>();\n const navigate = useNavigate();\n const { basePath } = useConversations();\n const tr = useTr();\n const { archivedEnabled } = useUserFeatureFlags();\n const {\n searchQuery,\n setSearchQuery,\n conversationFilter,\n updateConversationFilter,\n } = useConversationsStore();\n\n // `?new=true` opens the create-conversation dialog in-place. The dialog\n // is dismissed by either (a) the user closing it, in which case we drop\n // the param via setParams (replace), or (b) a successful create, in\n // which case we strip the param and navigate to the new detail route.\n const isNewDialogOpen = params.get(\"new\") === \"true\";\n\n const closeNewDialog = useCallback(() => {\n setParams(\n (prev) => {\n const next = new URLSearchParams(prev);\n next.delete(\"new\");\n return next;\n },\n { replace: true },\n );\n }, [setParams]);\n\n const handleNewDialogOpenChange = useCallback(\n (open: boolean) => {\n if (!open) closeNewDialog();\n },\n [closeNewDialog],\n );\n\n const handleNewDialogCreated = useCallback(\n (newConversationId: string) => {\n // The dialog closes itself before invoking onCreated; we just need\n // to route to the new conversation detail page (which lives under\n // the conversations basePath) without the lingering ?new=true.\n navigate(joinPath(basePath || \"/conversations\", newConversationId), {\n replace: true,\n });\n },\n [basePath, navigate],\n );\n\n const requestedTab = (params.get(\"tab\") ?? DEFAULT_TAB_ID) as InboxTabId;\n // Strip the archived tab when the feature flag is off — the side effect\n // below also lazily cleans the URL so a deep link doesn't leave the user\n // staring at an unknown tab.\n const activeTab: InboxTabId =\n requestedTab === \"archived\" && !archivedEnabled\n ? DEFAULT_TAB_ID\n : requestedTab;\n\n useEffect(() => {\n if (!archivedEnabled && requestedTab === \"archived\") {\n // Updater form reads the freshest params at flush time to preserve\n // concurrent query-param changes elsewhere in the tree.\n setParams(\n (prev) => {\n const next = new URLSearchParams(prev);\n next.delete(\"tab\");\n return next;\n },\n { replace: true },\n );\n }\n }, [archivedEnabled, requestedTab, setParams]);\n\n const handleTabChange = useCallback(\n (id: InboxTabId) => {\n setParams(\n (prev) => {\n const next = new URLSearchParams(prev);\n if (id === DEFAULT_TAB_ID) {\n next.delete(\"tab\");\n } else {\n next.set(\"tab\", id);\n }\n return next;\n },\n { replace: true },\n );\n },\n [setParams],\n );\n\n // Filter chips are only meaningful on tabs where they can hide items. On\n // the Archived tab the only sane secondary toggle is \"Pinned\" (unread\n // doesn't make sense for archived conversations); on the inbox tabs we\n // expose both unread + pinned.\n const filterChips = useMemo<FilterChipDescriptor[]>(() => {\n if (activeTab === \"archived\") return [];\n const unreadActive = conversationFilter.hasUnread === true;\n const pinnedActive = conversationFilter.isPinned === true;\n return [\n {\n id: \"unread\",\n label: tr(\"conversations.unread\", \"Unread\"),\n active: unreadActive,\n onToggle: () =>\n updateConversationFilter({\n hasUnread: unreadActive ? undefined : true,\n }),\n icon: <Filter size={11} aria-hidden=\"true\" />,\n },\n {\n id: \"pinned\",\n label: tr(\"conversations.pinned\", \"Pinned\"),\n active: pinnedActive,\n onToggle: () =>\n updateConversationFilter({\n isPinned: pinnedActive ? undefined : true,\n }),\n icon: <Pin size={11} aria-hidden=\"true\" />,\n },\n ];\n }, [\n activeTab,\n conversationFilter.hasUnread,\n conversationFilter.isPinned,\n tr,\n updateConversationFilter,\n ]);\n\n const forcedFilter = TAB_TO_LIST_FILTER[activeTab];\n\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <PageContainer\n data-tour=\"conversations-landing\"\n padded={false}\n className=\"h-full min-h-0\"\n >\n <h1 id=\"conversations-page-heading\" className=\"sr-only\">\n {tr(\"conversations.title\", \"Conversations\")}\n </h1>\n <InboxLayout\n activeTab={activeTab}\n onTabChange={handleTabChange}\n archivedEnabled={archivedEnabled}\n searchQuery={searchQuery}\n onSearchChange={setSearchQuery}\n filterChips={filterChips}\n listSlot={\n <ConversationListPage embedded forcedFilterParam={forcedFilter} />\n }\n detailSlot={\n conversationId ? (\n <Suspense\n fallback={\n <div\n role=\"status\"\n aria-live=\"polite\"\n className=\"flex h-full items-center justify-center gap-2 text-sm text-text-secondary\"\n >\n <Loader2\n size={16}\n className=\"animate-spin\"\n aria-hidden=\"true\"\n />\n {tr(\"conversations.detailLoading\", \"Loading conversation…\")}\n </div>\n }\n >\n <ConversationDetailPage />\n </Suspense>\n ) : undefined\n }\n />\n {isNewDialogOpen && (\n <NewConversationDialog\n open\n onOpenChange={handleNewDialogOpenChange}\n onCreated={handleNewDialogCreated}\n />\n )}\n </PageContainer>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ConversationsLandingPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationsLandingPage.tsx"],"sourcesContent":["import { Suspense, useCallback, useEffect, useMemo, lazy } from \"react\";\nimport { useNavigate, useParams, useSearchParams } from \"react-router-dom\";\nimport { PageContainer } from \"@burdenoff/fe-libs/ui\";\nimport { Pin, Filter, Loader2 } from \"lucide-react\";\nimport { ConversationListPage } from \"./ConversationListPage\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport {\n InboxLayout,\n type FilterChipDescriptor,\n type InboxTabId,\n} from \"../components/InboxLayout\";\nimport { NewConversationDialog } from \"../components/NewConversationDialog\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { useUserFeatureFlags } from \"../hooks/useUserFeatureFlags\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport { useTr } from \"../utils/i18n\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { joinPath } from \"../utils/navigation\";\n\n// Lazy-load the detail page so the inbox bundle stays small for users who\n// never open a conversation in this session. The detail subtree is the\n// heaviest by far (~19kB gz including MessageBubble).\nconst ConversationDetailPage = lazy(() =>\n import(\"./ConversationDetailPage\").then((m) => ({\n default: m.ConversationDetailPage,\n })),\n);\n\nconst DEFAULT_TAB_ID: InboxTabId = \"all\";\n\n// Maps the inbox tab id to the `forcedFilterParam` prop accepted by\n// ConversationListPage. The \"all\" tab passes undefined so the list shows\n// every conversation type. The archived tab triggers the BE state filter\n// (states: [ARCHIVED]) handled inside ConversationListPage.\nconst TAB_TO_LIST_FILTER: Record<\n InboxTabId,\n \"channel\" | \"direct\" | \"group\" | \"archived\" | undefined\n> = {\n all: undefined,\n direct: \"direct\",\n groups: \"group\",\n channels: \"channel\",\n archived: \"archived\",\n};\n\n/**\n * Landing page for `/conversations`.\n *\n * Renders the canonical inbox: two-pane shell with the conversation list on\n * the left and an empty detail panel on the right. The detail panel is\n * filled in by the router on `/conversations/:conversationId`; this page\n * itself only renders the chrome and the list.\n *\n * Tabs (All / Direct / Groups / Channels / Archived) are stored in the\n * `?tab=` URL search param so refreshing the page keeps the user on the\n * same view.\n */\nexport function ConversationsLandingPage() {\n const [params, setParams] = useSearchParams();\n const { conversationId } = useParams<{ conversationId?: string }>();\n const navigate = useNavigate();\n const { basePath } = useConversations();\n const tr = useTr();\n const { archivedEnabled } = useUserFeatureFlags();\n const {\n searchQuery,\n setSearchQuery,\n conversationFilter,\n updateConversationFilter,\n } = useConversationsStore();\n\n // `?new=true` opens the create-conversation dialog in-place. The dialog\n // is dismissed by either (a) the user closing it, in which case we drop\n // the param via setParams (replace), or (b) a successful create, in\n // which case we strip the param and navigate to the new detail route.\n const isNewDialogOpen = params.get(\"new\") === \"true\";\n\n const closeNewDialog = useCallback(() => {\n setParams(\n (prev) => {\n const next = new URLSearchParams(prev);\n next.delete(\"new\");\n return next;\n },\n { replace: true },\n );\n }, [setParams]);\n\n const handleNewDialogOpenChange = useCallback(\n (open: boolean) => {\n if (!open) closeNewDialog();\n },\n [closeNewDialog],\n );\n\n const handleNewDialogCreated = useCallback(\n (newConversationId: string) => {\n // The dialog closes itself before invoking onCreated; we just need\n // to route to the new conversation detail page (which lives under\n // the conversations basePath) without the lingering ?new=true.\n navigate(joinPath(basePath || \"/conversations\", newConversationId), {\n replace: true,\n });\n },\n [basePath, navigate],\n );\n\n const requestedTab = (params.get(\"tab\") ?? DEFAULT_TAB_ID) as InboxTabId;\n // Strip the archived tab when the feature flag is off — the side effect\n // below also lazily cleans the URL so a deep link doesn't leave the user\n // staring at an unknown tab.\n const activeTab: InboxTabId =\n requestedTab === \"archived\" && !archivedEnabled\n ? DEFAULT_TAB_ID\n : requestedTab;\n\n useEffect(() => {\n if (!archivedEnabled && requestedTab === \"archived\") {\n // Updater form reads the freshest params at flush time to preserve\n // concurrent query-param changes elsewhere in the tree.\n setParams(\n (prev) => {\n const next = new URLSearchParams(prev);\n next.delete(\"tab\");\n return next;\n },\n { replace: true },\n );\n }\n }, [archivedEnabled, requestedTab, setParams]);\n\n const handleTabChange = useCallback(\n (id: InboxTabId) => {\n setParams(\n (prev) => {\n const next = new URLSearchParams(prev);\n if (id === DEFAULT_TAB_ID) {\n next.delete(\"tab\");\n } else {\n next.set(\"tab\", id);\n }\n return next;\n },\n { replace: true },\n );\n },\n [setParams],\n );\n\n // Filter chips are only meaningful on tabs where they can hide items. On\n // the Archived tab the only sane secondary toggle is \"Pinned\" (unread\n // doesn't make sense for archived conversations); on the inbox tabs we\n // expose both unread + pinned.\n const filterChips = useMemo<FilterChipDescriptor[]>(() => {\n if (activeTab === \"archived\") return [];\n const unreadActive = conversationFilter.hasUnread === true;\n const pinnedActive = conversationFilter.isPinned === true;\n return [\n {\n id: \"unread\",\n label: tr(\"conversations.unread\", \"Unread\"),\n active: unreadActive,\n onToggle: () =>\n updateConversationFilter({\n hasUnread: unreadActive ? undefined : true,\n }),\n icon: <Filter size={11} aria-hidden=\"true\" />,\n },\n {\n id: \"pinned\",\n label: tr(\"conversations.pinned\", \"Pinned\"),\n active: pinnedActive,\n onToggle: () =>\n updateConversationFilter({\n isPinned: pinnedActive ? undefined : true,\n }),\n icon: <Pin size={11} aria-hidden=\"true\" />,\n },\n ];\n }, [\n activeTab,\n conversationFilter.hasUnread,\n conversationFilter.isPinned,\n tr,\n updateConversationFilter,\n ]);\n\n const forcedFilter = TAB_TO_LIST_FILTER[activeTab];\n\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <PageContainer\n data-tour=\"conversations-landing\"\n padded={false}\n className=\"h-full min-h-0\"\n >\n <h1 id=\"conversations-page-heading\" className=\"sr-only\">\n {tr(\"conversations.title\", \"Conversations\")}\n </h1>\n <InboxLayout\n activeTab={activeTab}\n onTabChange={handleTabChange}\n archivedEnabled={archivedEnabled}\n searchQuery={searchQuery}\n onSearchChange={setSearchQuery}\n filterChips={filterChips}\n listSlot={\n <ConversationListPage embedded forcedFilterParam={forcedFilter} />\n }\n detailSlot={\n conversationId ? (\n <Suspense\n fallback={\n <div\n role=\"status\"\n aria-live=\"polite\"\n className=\"flex h-full items-center justify-center gap-2 text-sm text-text-secondary\"\n >\n <Loader2\n size={16}\n className=\"animate-spin\"\n aria-hidden=\"true\"\n />\n {tr(\"conversations.detailLoading\", \"Loading conversation…\")}\n </div>\n }\n >\n <ConversationDetailPage />\n </Suspense>\n ) : undefined\n }\n />\n <NewConversationDialog\n open={isNewDialogOpen}\n onOpenChange={handleNewDialogOpenChange}\n onCreated={handleNewDialogCreated}\n />\n </PageContainer>\n </PermissionGuard>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAsBA,IAAM,IAAyB,QAC7B,OAAO,+BAA4B,MAAM,OAAO,EAC9C,SAAS,EAAE,wBACZ,EAAE,CACJ,EAEK,IAA6B,OAM7B,IAGF;CACF,KAAK,KAAA;CACL,QAAQ;CACR,QAAQ;CACR,UAAU;CACV,UAAU;CACX;AAcD,SAAgB,IAA2B;CACzC,IAAM,CAAC,GAAQ,KAAa,GAAiB,EACvC,EAAE,sBAAmB,GAAwC,EAC7D,IAAW,GAAa,EACxB,EAAE,gBAAa,GAAkB,EACjC,IAAK,GAAO,EACZ,EAAE,uBAAoB,GAAqB,EAC3C,EACJ,gBACA,mBACA,uBACA,gCACE,GAAuB,EAMrB,IAAkB,EAAO,IAAI,MAAM,KAAK,QAExC,IAAiB,QAAkB;AACvC,KACG,MAAS;GACR,IAAM,IAAO,IAAI,gBAAgB,EAAK;AAEtC,UADA,EAAK,OAAO,MAAM,EACX;KAET,EAAE,SAAS,IAAM,CAClB;IACA,CAAC,EAAU,CAAC,EAET,IAA4B,GAC/B,MAAkB;AACjB,EAAK,KAAM,GAAgB;IAE7B,CAAC,EAAe,CACjB,EAEK,IAAyB,GAC5B,MAA8B;AAI7B,IAAS,EAAS,KAAY,kBAAkB,EAAkB,EAAE,EAClE,SAAS,IACV,CAAC;IAEJ,CAAC,GAAU,EAAS,CACrB,EAEK,IAAgB,EAAO,IAAI,MAAM,IAAI,GAIrC,IACJ,MAAiB,cAAc,CAAC,IAC5B,IACA;AAEN,SAAgB;AACd,EAAI,CAAC,KAAmB,MAAiB,cAGvC,GACG,MAAS;GACR,IAAM,IAAO,IAAI,gBAAgB,EAAK;AAEtC,UADA,EAAK,OAAO,MAAM,EACX;KAET,EAAE,SAAS,IAAM,CAClB;IAEF;EAAC;EAAiB;EAAc;EAAU,CAAC;CAE9C,IAAM,IAAkB,GACrB,MAAmB;AAClB,KACG,MAAS;GACR,IAAM,IAAO,IAAI,gBAAgB,EAAK;AAMtC,UALI,MAAO,IACT,EAAK,OAAO,MAAM,GAElB,EAAK,IAAI,OAAO,EAAG,EAEd;KAET,EAAE,SAAS,IAAM,CAClB;IAEH,CAAC,EAAU,CACZ,EAMK,IAAc,QAAsC;AACxD,MAAI,MAAc,WAAY,QAAO,EAAE;EACvC,IAAM,IAAe,EAAmB,cAAc,IAChD,IAAe,EAAmB,aAAa;AACrD,SAAO,CACL;GACE,IAAI;GACJ,OAAO,EAAG,wBAAwB,SAAS;GAC3C,QAAQ;GACR,gBACE,EAAyB,EACvB,WAAW,IAAe,KAAA,IAAY,IACvC,CAAC;GACJ,MAAM,kBAAC,GAAD;IAAQ,MAAM;IAAI,eAAY;IAAS,CAAA;GAC9C,EACD;GACE,IAAI;GACJ,OAAO,EAAG,wBAAwB,SAAS;GAC3C,QAAQ;GACR,gBACE,EAAyB,EACvB,UAAU,IAAe,KAAA,IAAY,IACtC,CAAC;GACJ,MAAM,kBAAC,GAAD;IAAK,MAAM;IAAI,eAAY;IAAS,CAAA;GAC3C,CACF;IACA;EACD;EACA,EAAmB;EACnB,EAAmB;EACnB;EACA;EACD,CAAC,EAEI,IAAe,EAAmB;AAExC,QACE,kBAAC,GAAD;EACE,YAAY,EAA0B;EACtC,6BAAA;YAEA,kBAAC,GAAD;GACE,aAAU;GACV,QAAQ;GACR,WAAU;aAHZ;IAKE,kBAAC,MAAD;KAAI,IAAG;KAA6B,WAAU;eAC3C,EAAG,uBAAuB,gBAAgB;KACxC,CAAA;IACL,kBAAC,GAAD;KACa;KACX,aAAa;KACI;KACJ;KACb,gBAAgB;KACH;KACb,UACE,kBAAC,GAAD;MAAsB,UAAA;MAAS,mBAAmB;MAAgB,CAAA;KAEpE,YACE,IACE,kBAAC,GAAD;MACE,UACE,kBAAC,OAAD;OACE,MAAK;OACL,aAAU;OACV,WAAU;iBAHZ,CAKE,kBAAC,GAAD;QACE,MAAM;QACN,WAAU;QACV,eAAY;QACZ,CAAA,EACD,EAAG,+BAA+B,wBAAwB,CACvD;;gBAGR,kBAAC,GAAD,EAA0B,CAAA;MACjB,CAAA,GACT,KAAA;KAEN,CAAA;IACF,kBAAC,GAAD;KACE,MAAM;KACN,cAAc;KACd,WAAW;KACX,CAAA;IACY;;EACA,CAAA"}
|
|
@@ -3,27 +3,37 @@ import { joinPath as t } from "../utils/navigation.js";
|
|
|
3
3
|
import { PermissionGuard as n } from "../components/PermissionGuard.js";
|
|
4
4
|
import { CONVERSATIONS_PERMISSIONS as r } from "../constants/permissions.js";
|
|
5
5
|
import { NewConversationDialog as i } from "../components/NewConversationDialog.js";
|
|
6
|
-
import { useCallback as a } from "react";
|
|
7
|
-
import { useNavigate as
|
|
8
|
-
import { jsx as
|
|
6
|
+
import { useCallback as a, useState as o } from "react";
|
|
7
|
+
import { useNavigate as s } from "react-router-dom";
|
|
8
|
+
import { jsx as c } from "react/jsx-runtime";
|
|
9
9
|
//#region src/conversations/pages/NewConversationPage.tsx
|
|
10
|
-
function
|
|
11
|
-
let
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
function l() {
|
|
11
|
+
let l = s(), { basePath: u } = e(), d = u || "/conversations", [f, p] = o(!0), m = a((e) => {
|
|
12
|
+
if (e) {
|
|
13
|
+
p(!0);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
l(`${d}?new=true`, { replace: !0 });
|
|
17
|
+
}, [l, d]), h = a(() => {
|
|
18
|
+
p(!1);
|
|
19
|
+
}, []), g = a(() => {
|
|
20
|
+
l(d, { replace: !0 });
|
|
21
|
+
}, [l, d]), _ = a((e) => {
|
|
22
|
+
l(t(d, e));
|
|
23
|
+
}, [l, d]);
|
|
24
|
+
return /* @__PURE__ */ c(n, {
|
|
17
25
|
permission: r.CONVERSATION_CREATE,
|
|
18
26
|
allowUnavailablePermissions: !0,
|
|
19
|
-
children: /* @__PURE__ */
|
|
20
|
-
open:
|
|
21
|
-
onOpenChange:
|
|
22
|
-
|
|
27
|
+
children: /* @__PURE__ */ c(i, {
|
|
28
|
+
open: f,
|
|
29
|
+
onOpenChange: m,
|
|
30
|
+
onQuotaExhausted: h,
|
|
31
|
+
onQuotaDialogClose: g,
|
|
32
|
+
onCreated: _
|
|
23
33
|
})
|
|
24
34
|
});
|
|
25
35
|
}
|
|
26
36
|
//#endregion
|
|
27
|
-
export {
|
|
37
|
+
export { l as NewConversationPage };
|
|
28
38
|
|
|
29
39
|
//# sourceMappingURL=NewConversationPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NewConversationPage.js","names":[],"sources":["../../../src/conversations/pages/NewConversationPage.tsx"],"sourcesContent":["import { useCallback } from \"react\";\nimport { useNavigate } from \"react-router-dom\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { NewConversationDialog } from \"../components/NewConversationDialog\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { joinPath } from \"../utils/navigation\";\n\n/**\n * Legacy `/conversations/new` route.\n *\n * The canonical UX is now the in-place `<NewConversationDialog>` opened via\n * the `?new=true` URL param on the landing page — no full-page navigation.\n * This wrapper preserves the old route for any deep links that still exist\n * in the wild: it mounts the dialog as already-open and routes the user\n * back to the inbox (with `?new=true`) on close, so the dialog stays open\n * and they end up in the canonical surface.\n *\n * On successful create the dialog itself navigates to the new conversation\n * detail page via the `onCreated` callback.\n */\nexport function NewConversationPage() {\n const navigate = useNavigate();\n const { basePath } = useConversations();\n const root = basePath || \"/conversations\";\n\n const handleOpenChange = useCallback(\n (open: boolean) => {\n if (open) return;\n // Closing the dialog from the legacy route lands the user on the\n // inbox; we keep `?new=true` so the dialog re-opens in-place there\n // — this mirrors what would have happened had they clicked the\n // primary CTA on the inbox itself.\n navigate(`${root}?new=true`, { replace: true });\n },\n [navigate, root],\n );\n\n const handleCreated = useCallback(\n (conversationId: string) => {\n navigate(joinPath(root, conversationId));\n },\n [navigate, root],\n );\n\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_CREATE}\n allowUnavailablePermissions\n >\n <NewConversationDialog\n open\n onOpenChange={handleOpenChange}\n onCreated={handleCreated}\n />\n </PermissionGuard>\n );\n}\n"],"mappings":";;;;;;;;;AAqBA,SAAgB,IAAsB;CACpC,IAAM,IAAW,GAAa,EACxB,EAAE,gBAAa,GAAkB,EACjC,IAAO,KAAY,
|
|
1
|
+
{"version":3,"file":"NewConversationPage.js","names":[],"sources":["../../../src/conversations/pages/NewConversationPage.tsx"],"sourcesContent":["import { useCallback, useState } from \"react\";\nimport { useNavigate } from \"react-router-dom\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { NewConversationDialog } from \"../components/NewConversationDialog\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { joinPath } from \"../utils/navigation\";\n\n/**\n * Legacy `/conversations/new` route.\n *\n * The canonical UX is now the in-place `<NewConversationDialog>` opened via\n * the `?new=true` URL param on the landing page — no full-page navigation.\n * This wrapper preserves the old route for any deep links that still exist\n * in the wild: it mounts the dialog as already-open and routes the user\n * back to the inbox (with `?new=true`) on close, so the dialog stays open\n * and they end up in the canonical surface.\n *\n * On successful create the dialog itself navigates to the new conversation\n * detail page via the `onCreated` callback.\n */\nexport function NewConversationPage() {\n const navigate = useNavigate();\n const { basePath } = useConversations();\n const root = basePath || \"/conversations\";\n const [dialogOpen, setDialogOpen] = useState(true);\n\n const handleOpenChange = useCallback(\n (open: boolean) => {\n if (open) {\n setDialogOpen(true);\n return;\n }\n // Closing the dialog from the legacy route lands the user on the\n // inbox; we keep `?new=true` so the dialog re-opens in-place there\n // — this mirrors what would have happened had they clicked the\n // primary CTA on the inbox itself.\n navigate(`${root}?new=true`, { replace: true });\n },\n [navigate, root],\n );\n\n const handleQuotaExhausted = useCallback(() => {\n setDialogOpen(false);\n }, []);\n\n const handleQuotaDialogClose = useCallback(() => {\n navigate(root, { replace: true });\n }, [navigate, root]);\n\n const handleCreated = useCallback(\n (conversationId: string) => {\n navigate(joinPath(root, conversationId));\n },\n [navigate, root],\n );\n\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_CREATE}\n allowUnavailablePermissions\n >\n <NewConversationDialog\n open={dialogOpen}\n onOpenChange={handleOpenChange}\n onQuotaExhausted={handleQuotaExhausted}\n onQuotaDialogClose={handleQuotaDialogClose}\n onCreated={handleCreated}\n />\n </PermissionGuard>\n );\n}\n"],"mappings":";;;;;;;;;AAqBA,SAAgB,IAAsB;CACpC,IAAM,IAAW,GAAa,EACxB,EAAE,gBAAa,GAAkB,EACjC,IAAO,KAAY,kBACnB,CAAC,GAAY,KAAiB,EAAS,GAAK,EAE5C,IAAmB,GACtB,MAAkB;AACjB,MAAI,GAAM;AACR,KAAc,GAAK;AACnB;;AAMF,IAAS,GAAG,EAAK,YAAY,EAAE,SAAS,IAAM,CAAC;IAEjD,CAAC,GAAU,EAAK,CACjB,EAEK,IAAuB,QAAkB;AAC7C,IAAc,GAAM;IACnB,EAAE,CAAC,EAEA,IAAyB,QAAkB;AAC/C,IAAS,GAAM,EAAE,SAAS,IAAM,CAAC;IAChC,CAAC,GAAU,EAAK,CAAC,EAEd,IAAgB,GACnB,MAA2B;AAC1B,IAAS,EAAS,GAAM,EAAe,CAAC;IAE1C,CAAC,GAAU,EAAK,CACjB;AAED,QACE,kBAAC,GAAD;EACE,YAAY,EAA0B;EACtC,6BAAA;YAEA,kBAAC,GAAD;GACE,MAAM;GACN,cAAc;GACd,kBAAkB;GAClB,oBAAoB;GACpB,WAAW;GACX,CAAA;EACc,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burdenoff/microfe-conversations",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.731.1",
|
|
4
4
|
"description": "Burdenoff Conversations Microfrontend - Complete messaging and conversation interface",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
"types": "./dist/index.d.ts",
|
|
23
23
|
"scripts": {
|
|
24
24
|
"dev": "vite",
|
|
25
|
-
"build": "tsc && vite build",
|
|
25
|
+
"build": "tsc && bash scripts/build-limits.sh --gate vite build",
|
|
26
26
|
"lint": "eslint .",
|
|
27
27
|
"lint:sanity": "if [ \"${CI:-}\" = \"true\" ]; then bun run lint; else bun run lint -- --cache --cache-location .eslintcache; fi",
|
|
28
28
|
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
29
|
-
"type:check": "tsc --noEmit",
|
|
30
|
-
"sanity": "bun run codegen && bun run lint:sanity && bun run format && bun run type:check && bun run build",
|
|
29
|
+
"type:check": "bash scripts/build-limits.sh tsc --noEmit",
|
|
30
|
+
"sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run lint:sanity && bun run format && bun run type:check && bun run build'",
|
|
31
31
|
"schema:pull": "graphql-codegen --project schema-wspace && graphql-codegen --project schema-global",
|
|
32
32
|
"codegen": "graphql-codegen --project wspace && graphql-codegen --project global",
|
|
33
33
|
"codegen:all": "bun run schema:pull && bun run codegen",
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"switch:check": "bun scripts/deps-mode.ts check",
|
|
38
38
|
"prepare": "lefthook install || true",
|
|
39
39
|
"format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
|
|
40
|
-
"test": "vitest run",
|
|
40
|
+
"test": "bash scripts/build-limits.sh vitest run",
|
|
41
41
|
"test:watch": "vitest",
|
|
42
|
-
"test:coverage": "vitest run --coverage"
|
|
42
|
+
"test:coverage": "bash scripts/build-limits.sh vitest run --coverage"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@apollo/client": "^4.0.11",
|