@burdenoff/microfe-conversations 2026.924.1 → 2026.930.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/ConversationRow.js +1 -1
- package/dist/conversations/components/ConversationRow.js.map +1 -1
- package/dist/conversations/hooks/useConversationQueries.js +66 -40
- package/dist/conversations/hooks/useConversationQueries.js.map +1 -1
- package/dist/conversations/pages/ConversationDetailPage.js +316 -276
- package/dist/conversations/pages/ConversationDetailPage.js.map +1 -1
- package/dist/conversations/utils/mappers.js +2 -1
- package/dist/conversations/utils/mappers.js.map +1 -1
- package/dist/conversations/utils/readReceiptCache.js +45 -0
- package/dist/conversations/utils/readReceiptCache.js.map +1 -0
- package/dist/generated/wspace-operations.js +83 -63
- package/dist/generated/wspace-operations.js.map +1 -1
- package/package.json +1 -1
|
@@ -66,7 +66,7 @@ var j = r(function({ conversation: r, isActive: l = !1, onClick: d, onLeave: p }
|
|
|
66
66
|
})
|
|
67
67
|
}),
|
|
68
68
|
/* @__PURE__ */ S("div", {
|
|
69
|
-
className: "flex min-w-0 flex-1 flex-col gap-0.5",
|
|
69
|
+
className: "flex min-w-0 flex-1 flex-col gap-0.5 pr-9",
|
|
70
70
|
children: [/* @__PURE__ */ S("div", {
|
|
71
71
|
className: "flex items-center justify-between gap-2",
|
|
72
72
|
children: [/* @__PURE__ */ S("div", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConversationRow.js","names":[],"sources":["../../../src/conversations/components/ConversationRow.tsx"],"sourcesContent":["import { memo, useState } from \"react\";\nimport {\n ArchiveIcon,\n ArchiveRestore,\n Bell,\n BellOff,\n Bot,\n Hash,\n HeadphonesIcon,\n LogOut,\n Megaphone,\n MessageCircle,\n MoreHorizontal,\n Pin,\n PinOff,\n Users,\n VolumeX,\n} from \"lucide-react\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@burdenoff/fe-libs/ui\";\nimport { cn } from \"../utils/cn\";\nimport { useTr } from \"../utils/i18n\";\nimport { useConversationActions } from \"../hooks/useConversationQueries\";\nimport type { Conversation } from \"../types\";\n\nexport interface ConversationRowProps {\n conversation: Conversation;\n isActive?: boolean;\n onClick?: (conversation: Conversation) => void;\n /** Optional leave handler — only the host has the membership mutation. */\n onLeave?: (conversation: Conversation) => void;\n}\n\nfunction ConversationTypeIcon({\n type,\n size = 16,\n className,\n}: {\n type: Conversation[\"type\"];\n size?: number;\n className?: string;\n}) {\n const props = { size, className };\n switch (type) {\n case \"DIRECT\":\n return <MessageCircle {...props} />;\n case \"GROUP\":\n return <Users {...props} />;\n case \"CHANNEL\":\n return <Hash {...props} />;\n case \"BROADCAST\":\n case \"ANNOUNCEMENT\":\n return <Megaphone {...props} />;\n case \"BOT\":\n return <Bot {...props} />;\n case \"SUPPORT\":\n return <HeadphonesIcon {...props} />;\n default:\n return <MessageCircle {...props} />;\n }\n}\n\nfunction formatRelativeTime(\n dateString: string,\n labels: {\n now: string;\n yesterday: string;\n minutesAgo: string;\n hoursAgo: string;\n daysAgo: string;\n },\n): string {\n const now = new Date();\n const date = new Date(dateString);\n const diffMs = now.getTime() - date.getTime();\n const diffMin = Math.floor(diffMs / 1000 / 60);\n const diffHr = Math.floor(diffMin / 60);\n const diffDay = Math.floor(diffHr / 24);\n\n if (diffMin < 1) return labels.now;\n if (diffMin < 60)\n return labels.minutesAgo.replace(\"{{count}}\", String(diffMin));\n if (diffHr < 24) return labels.hoursAgo.replace(\"{{count}}\", String(diffHr));\n if (diffDay === 1) return labels.yesterday;\n if (diffDay < 7) return labels.daysAgo.replace(\"{{count}}\", String(diffDay));\n return date.toLocaleDateString(undefined, { month: \"short\", day: \"numeric\" });\n}\n\nfunction getInitials(title: string): string {\n return title\n .split(\" \")\n .slice(0, 2)\n .map((word) => word[0])\n .join(\"\")\n .toUpperCase();\n}\n\n/**\n * ConversationRow — inbox-style list row.\n *\n * Visual hierarchy:\n * [avatar] Title · 2h\n * last message preview (badge)\n *\n * On hover the kebab (3-dots) menu reveals so the user can pin/mute/archive\n * without opening the conversation first. Wires straight into\n * `useConversationActions` for archive/unarchive/pin/mute.\n */\nexport const ConversationRow = memo(function ConversationRow({\n conversation,\n isActive = false,\n onClick,\n onLeave,\n}: ConversationRowProps) {\n const {\n title,\n type,\n state,\n lastMessage,\n unreadCount,\n isPinned,\n isMuted,\n memberCount,\n updatedAt,\n avatar,\n } = conversation;\n\n const hasUnread = unreadCount > 0;\n const isArchived = state === \"ARCHIVED\";\n const tr = useTr();\n const timeLabels = {\n now: tr(\"conversations.now\", \"now\"),\n yesterday: tr(\"conversations.yesterday\", \"Yesterday\"),\n minutesAgo: tr(\"conversations.time.minutesAgo\", \"{{count}}m\"),\n hoursAgo: tr(\"conversations.time.hoursAgo\", \"{{count}}h\"),\n daysAgo: tr(\"conversations.time.daysAgo\", \"{{count}}d\"),\n };\n\n // Track menu open so the kebab stays visible even when the cursor leaves\n // the row (otherwise the radix portal closes on every hover-out).\n const [menuOpen, setMenuOpen] = useState(false);\n\n const {\n archiveConversation,\n unarchiveConversation,\n pinConversation,\n unpinConversation,\n muteConversation,\n unmuteConversation,\n } = useConversationActions();\n\n // Stop click bubbling from the kebab into the row's click handler — opening\n // the menu must not also open the conversation.\n const stop = (event: React.MouseEvent | React.KeyboardEvent) => {\n event.stopPropagation();\n };\n\n return (\n <div\n data-tour=\"conversation-row\"\n className={cn(\n \"group relative flex w-full min-w-0 items-start gap-3 rounded-md px-3 py-2.5 transition-colors\",\n \"cursor-pointer select-none\",\n isActive\n ? \"bg-bg-sunken border-l-2 border-l-action-primary-bg\"\n : \"border-l-2 border-l-transparent hover:bg-action-ghost-bg-hover\",\n hasUnread && !isActive && \"bg-bg-elevated/40\",\n )}\n role=\"button\"\n tabIndex={0}\n onClick={() => onClick?.(conversation)}\n onKeyDown={(event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onClick?.(conversation);\n }\n }}\n aria-label={\n tr(\"conversations.openConversation\", \"Open conversation\") + \": \" + title\n }\n >\n {/* Avatar / type icon */}\n <div\n className={cn(\n \"flex-shrink-0 flex h-9 w-9 items-center justify-center rounded-full text-xs font-semibold overflow-hidden\",\n type === \"DIRECT\" && \"bg-status-info-bg-subtle text-status-info-text\",\n type === \"CHANNEL\" &&\n \"bg-status-success-bg-subtle text-status-success-text\",\n (type === \"GROUP\" ||\n type === \"BROADCAST\" ||\n type === \"ANNOUNCEMENT\" ||\n type === \"BOT\" ||\n type === \"SUPPORT\") &&\n \"bg-bg-sunken text-text-primary\",\n )}\n aria-hidden=\"true\"\n >\n {avatar ? (\n <img src={avatar} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : type === \"DIRECT\" ? (\n getInitials(title)\n ) : type === \"CHANNEL\" ? (\n <Hash size={16} />\n ) : (\n <ConversationTypeIcon type={type} size={16} />\n )}\n </div>\n\n {/* Body */}\n <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <span\n className={cn(\n \"truncate text-sm\",\n hasUnread\n ? \"font-semibold text-text-primary\"\n : \"font-medium text-text-primary\",\n )}\n >\n {title}\n </span>\n {isPinned && (\n <Pin\n size={11}\n className=\"flex-shrink-0 text-text-secondary\"\n aria-label={tr(\"conversations.pinnedLabel\", \"Pinned\")}\n />\n )}\n {isMuted && (\n <VolumeX\n size={11}\n className=\"flex-shrink-0 text-text-secondary\"\n aria-label={tr(\"conversations.mutedLabel\", \"Muted\")}\n />\n )}\n </div>\n\n <span\n className={cn(\n \"flex-shrink-0 text-[11px]\",\n hasUnread\n ? \"font-medium text-action-primary-bg\"\n : \"text-text-secondary\",\n )}\n >\n {formatRelativeTime(\n lastMessage?.createdAt ?? updatedAt,\n timeLabels,\n )}\n </span>\n </div>\n\n <div className=\"flex items-center justify-between gap-2\">\n <p\n className={cn(\n \"min-w-0 truncate break-all text-xs\",\n hasUnread ? \"text-text-primary/80\" : \"text-text-secondary\",\n )}\n >\n {lastMessage ? (\n <>\n {lastMessage.sender.name && (\n <span className=\"font-medium\">\n {lastMessage.sender.name}\n {\": \"}\n </span>\n )}\n {lastMessage.content ||\n tr(\"conversations.sentAttachment\", \"Sent an attachment\")}\n </>\n ) : (\n <span className=\"italic\">\n {tr(\"conversations.noMessages\", \"No messages yet\")}\n </span>\n )}\n </p>\n\n <div className=\"flex flex-shrink-0 items-center gap-1.5\">\n {type !== \"DIRECT\" && (\n <span\n className=\"inline-flex items-center gap-0.5 text-[10px] text-text-secondary\"\n aria-label={`${memberCount} ${tr(\"conversations.membersLabel\", \"members\")}`}\n >\n <Users size={10} aria-hidden=\"true\" />\n {memberCount}\n </span>\n )}\n {hasUnread && (\n <span\n className=\"inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-action-primary-bg px-1 text-[10px] font-bold text-action-primary-text motion-safe:animate-vc-glow-pulse\"\n aria-label={`${unreadCount} ${tr(\"conversations.unreadCountLabel\", \"unread\")}`}\n >\n {unreadCount > 99 ? \"99+\" : unreadCount}\n </span>\n )}\n </div>\n </div>\n </div>\n\n {/* Kebab menu — hidden until hover or menu open */}\n <DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n onClick={stop}\n onKeyDown={stop}\n aria-label={tr(\n \"conversations.rowMenuLabel\",\n \"Conversation actions\",\n )}\n className={cn(\n \"absolute right-2 top-2 flex h-7 w-7 items-center justify-center rounded-md transition-opacity\",\n \"text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border\",\n menuOpen\n ? \"opacity-100\"\n : \"opacity-0 group-hover:opacity-100 group-focus-within:opacity-100\",\n )}\n >\n <MoreHorizontal size={14} aria-hidden=\"true\" />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n align=\"end\"\n className=\"min-w-[180px]\"\n onClick={stop}\n >\n <DropdownMenuItem\n onSelect={() => {\n void (isPinned\n ? unpinConversation(conversation.id)\n : pinConversation(conversation.id));\n }}\n >\n {isPinned ? (\n <>\n <PinOff size={14} />\n {tr(\"conversations.unpin\", \"Unpin\")}\n </>\n ) : (\n <>\n <Pin size={14} />\n {tr(\"conversations.pin\", \"Pin\")}\n </>\n )}\n </DropdownMenuItem>\n <DropdownMenuItem\n onSelect={() => {\n void (isMuted\n ? unmuteConversation(conversation.id)\n : muteConversation(conversation.id));\n }}\n >\n {isMuted ? (\n <>\n <Bell size={14} />\n {tr(\"conversations.unmute\", \"Unmute\")}\n </>\n ) : (\n <>\n <BellOff size={14} />\n {tr(\"conversations.mute\", \"Mute\")}\n </>\n )}\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onSelect={() => {\n void (isArchived\n ? unarchiveConversation(conversation.id)\n : archiveConversation(conversation.id));\n }}\n >\n {isArchived ? (\n <>\n <ArchiveRestore size={14} />\n {tr(\"conversations.unarchive\", \"Unarchive\")}\n </>\n ) : (\n <>\n <ArchiveIcon size={14} />\n {tr(\"conversations.archive\", \"Archive\")}\n </>\n )}\n </DropdownMenuItem>\n {(type === \"GROUP\" || type === \"CHANNEL\") && onLeave && (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onSelect={() => onLeave(conversation)}\n className=\"text-status-error-text focus:text-status-error-text\"\n >\n <LogOut size={14} />\n {tr(\"conversations.leave\", \"Leave\")}\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n );\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ConversationRow.js","names":[],"sources":["../../../src/conversations/components/ConversationRow.tsx"],"sourcesContent":["import { memo, useState } from \"react\";\nimport {\n ArchiveIcon,\n ArchiveRestore,\n Bell,\n BellOff,\n Bot,\n Hash,\n HeadphonesIcon,\n LogOut,\n Megaphone,\n MessageCircle,\n MoreHorizontal,\n Pin,\n PinOff,\n Users,\n VolumeX,\n} from \"lucide-react\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@burdenoff/fe-libs/ui\";\nimport { cn } from \"../utils/cn\";\nimport { useTr } from \"../utils/i18n\";\nimport { useConversationActions } from \"../hooks/useConversationQueries\";\nimport type { Conversation } from \"../types\";\n\nexport interface ConversationRowProps {\n conversation: Conversation;\n isActive?: boolean;\n onClick?: (conversation: Conversation) => void;\n /** Optional leave handler — only the host has the membership mutation. */\n onLeave?: (conversation: Conversation) => void;\n}\n\nfunction ConversationTypeIcon({\n type,\n size = 16,\n className,\n}: {\n type: Conversation[\"type\"];\n size?: number;\n className?: string;\n}) {\n const props = { size, className };\n switch (type) {\n case \"DIRECT\":\n return <MessageCircle {...props} />;\n case \"GROUP\":\n return <Users {...props} />;\n case \"CHANNEL\":\n return <Hash {...props} />;\n case \"BROADCAST\":\n case \"ANNOUNCEMENT\":\n return <Megaphone {...props} />;\n case \"BOT\":\n return <Bot {...props} />;\n case \"SUPPORT\":\n return <HeadphonesIcon {...props} />;\n default:\n return <MessageCircle {...props} />;\n }\n}\n\nfunction formatRelativeTime(\n dateString: string,\n labels: {\n now: string;\n yesterday: string;\n minutesAgo: string;\n hoursAgo: string;\n daysAgo: string;\n },\n): string {\n const now = new Date();\n const date = new Date(dateString);\n const diffMs = now.getTime() - date.getTime();\n const diffMin = Math.floor(diffMs / 1000 / 60);\n const diffHr = Math.floor(diffMin / 60);\n const diffDay = Math.floor(diffHr / 24);\n\n if (diffMin < 1) return labels.now;\n if (diffMin < 60)\n return labels.minutesAgo.replace(\"{{count}}\", String(diffMin));\n if (diffHr < 24) return labels.hoursAgo.replace(\"{{count}}\", String(diffHr));\n if (diffDay === 1) return labels.yesterday;\n if (diffDay < 7) return labels.daysAgo.replace(\"{{count}}\", String(diffDay));\n return date.toLocaleDateString(undefined, { month: \"short\", day: \"numeric\" });\n}\n\nfunction getInitials(title: string): string {\n return title\n .split(\" \")\n .slice(0, 2)\n .map((word) => word[0])\n .join(\"\")\n .toUpperCase();\n}\n\n/**\n * ConversationRow — inbox-style list row.\n *\n * Visual hierarchy:\n * [avatar] Title · 2h\n * last message preview (badge)\n *\n * On hover the kebab (3-dots) menu reveals so the user can pin/mute/archive\n * without opening the conversation first. Wires straight into\n * `useConversationActions` for archive/unarchive/pin/mute.\n */\nexport const ConversationRow = memo(function ConversationRow({\n conversation,\n isActive = false,\n onClick,\n onLeave,\n}: ConversationRowProps) {\n const {\n title,\n type,\n state,\n lastMessage,\n unreadCount,\n isPinned,\n isMuted,\n memberCount,\n updatedAt,\n avatar,\n } = conversation;\n\n const hasUnread = unreadCount > 0;\n const isArchived = state === \"ARCHIVED\";\n const tr = useTr();\n const timeLabels = {\n now: tr(\"conversations.now\", \"now\"),\n yesterday: tr(\"conversations.yesterday\", \"Yesterday\"),\n minutesAgo: tr(\"conversations.time.minutesAgo\", \"{{count}}m\"),\n hoursAgo: tr(\"conversations.time.hoursAgo\", \"{{count}}h\"),\n daysAgo: tr(\"conversations.time.daysAgo\", \"{{count}}d\"),\n };\n\n // Track menu open so the kebab stays visible even when the cursor leaves\n // the row (otherwise the radix portal closes on every hover-out).\n const [menuOpen, setMenuOpen] = useState(false);\n\n const {\n archiveConversation,\n unarchiveConversation,\n pinConversation,\n unpinConversation,\n muteConversation,\n unmuteConversation,\n } = useConversationActions();\n\n // Stop click bubbling from the kebab into the row's click handler — opening\n // the menu must not also open the conversation.\n const stop = (event: React.MouseEvent | React.KeyboardEvent) => {\n event.stopPropagation();\n };\n\n return (\n <div\n data-tour=\"conversation-row\"\n className={cn(\n \"group relative flex w-full min-w-0 items-start gap-3 rounded-md px-3 py-2.5 transition-colors\",\n \"cursor-pointer select-none\",\n isActive\n ? \"bg-bg-sunken border-l-2 border-l-action-primary-bg\"\n : \"border-l-2 border-l-transparent hover:bg-action-ghost-bg-hover\",\n hasUnread && !isActive && \"bg-bg-elevated/40\",\n )}\n role=\"button\"\n tabIndex={0}\n onClick={() => onClick?.(conversation)}\n onKeyDown={(event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onClick?.(conversation);\n }\n }}\n aria-label={\n tr(\"conversations.openConversation\", \"Open conversation\") + \": \" + title\n }\n >\n {/* Avatar / type icon */}\n <div\n className={cn(\n \"flex-shrink-0 flex h-9 w-9 items-center justify-center rounded-full text-xs font-semibold overflow-hidden\",\n type === \"DIRECT\" && \"bg-status-info-bg-subtle text-status-info-text\",\n type === \"CHANNEL\" &&\n \"bg-status-success-bg-subtle text-status-success-text\",\n (type === \"GROUP\" ||\n type === \"BROADCAST\" ||\n type === \"ANNOUNCEMENT\" ||\n type === \"BOT\" ||\n type === \"SUPPORT\") &&\n \"bg-bg-sunken text-text-primary\",\n )}\n aria-hidden=\"true\"\n >\n {avatar ? (\n <img src={avatar} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : type === \"DIRECT\" ? (\n getInitials(title)\n ) : type === \"CHANNEL\" ? (\n <Hash size={16} />\n ) : (\n <ConversationTypeIcon type={type} size={16} />\n )}\n </div>\n\n {/* Body */}\n {/*\n * ★ `pr-9` (36 px) reserves the kebab's footprint up front so the\n * timestamp and unread/member badges never sit under the absolutely\n * positioned 28 px kebab (right-2 top-2 h-7 w-7). The row's two\n * rows are `flex justify-between`; without this padding the kebab\n * collides with the timestamp on hover / focus / menu-open, and a\n * long title plus a wide timestamp would let the timestamp and the\n * kebab overlap the preview row's unread count.\n *\n * `pr-8` would align with the kebab's right edge (28 + 8 = 36 px\n * matches the kebab's outer footprint). We use `pr-9` so the\n * timestamp sits a full kebab-width to the left of the row's right\n * edge — even if the kebab grows or the row gains a right-side\n * adornment later — and so the badge in the preview row never\n * pushes into the kebab area at narrow widths.\n */}\n <div className=\"flex min-w-0 flex-1 flex-col gap-0.5 pr-9\">\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <span\n className={cn(\n \"truncate text-sm\",\n hasUnread\n ? \"font-semibold text-text-primary\"\n : \"font-medium text-text-primary\",\n )}\n >\n {title}\n </span>\n {isPinned && (\n <Pin\n size={11}\n className=\"flex-shrink-0 text-text-secondary\"\n aria-label={tr(\"conversations.pinnedLabel\", \"Pinned\")}\n />\n )}\n {isMuted && (\n <VolumeX\n size={11}\n className=\"flex-shrink-0 text-text-secondary\"\n aria-label={tr(\"conversations.mutedLabel\", \"Muted\")}\n />\n )}\n </div>\n\n <span\n className={cn(\n \"flex-shrink-0 text-[11px]\",\n hasUnread\n ? \"font-medium text-action-primary-bg\"\n : \"text-text-secondary\",\n )}\n >\n {formatRelativeTime(\n lastMessage?.createdAt ?? updatedAt,\n timeLabels,\n )}\n </span>\n </div>\n\n <div className=\"flex items-center justify-between gap-2\">\n <p\n className={cn(\n \"min-w-0 truncate break-all text-xs\",\n hasUnread ? \"text-text-primary/80\" : \"text-text-secondary\",\n )}\n >\n {lastMessage ? (\n <>\n {lastMessage.sender.name && (\n <span className=\"font-medium\">\n {lastMessage.sender.name}\n {\": \"}\n </span>\n )}\n {lastMessage.content ||\n tr(\"conversations.sentAttachment\", \"Sent an attachment\")}\n </>\n ) : (\n <span className=\"italic\">\n {tr(\"conversations.noMessages\", \"No messages yet\")}\n </span>\n )}\n </p>\n\n <div className=\"flex flex-shrink-0 items-center gap-1.5\">\n {type !== \"DIRECT\" && (\n <span\n className=\"inline-flex items-center gap-0.5 text-[10px] text-text-secondary\"\n aria-label={`${memberCount} ${tr(\"conversations.membersLabel\", \"members\")}`}\n >\n <Users size={10} aria-hidden=\"true\" />\n {memberCount}\n </span>\n )}\n {hasUnread && (\n <span\n className=\"inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-action-primary-bg px-1 text-[10px] font-bold text-action-primary-text motion-safe:animate-vc-glow-pulse\"\n aria-label={`${unreadCount} ${tr(\"conversations.unreadCountLabel\", \"unread\")}`}\n >\n {unreadCount > 99 ? \"99+\" : unreadCount}\n </span>\n )}\n </div>\n </div>\n </div>\n\n {/* Kebab menu — hidden until hover or menu open */}\n <DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n onClick={stop}\n onKeyDown={stop}\n aria-label={tr(\n \"conversations.rowMenuLabel\",\n \"Conversation actions\",\n )}\n className={cn(\n \"absolute right-2 top-2 flex h-7 w-7 items-center justify-center rounded-md transition-opacity\",\n \"text-text-secondary hover:bg-action-ghost-bg-hover hover:text-text-primary\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border\",\n menuOpen\n ? \"opacity-100\"\n : \"opacity-0 group-hover:opacity-100 group-focus-within:opacity-100\",\n )}\n >\n <MoreHorizontal size={14} aria-hidden=\"true\" />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n align=\"end\"\n className=\"min-w-[180px]\"\n onClick={stop}\n >\n <DropdownMenuItem\n onSelect={() => {\n void (isPinned\n ? unpinConversation(conversation.id)\n : pinConversation(conversation.id));\n }}\n >\n {isPinned ? (\n <>\n <PinOff size={14} />\n {tr(\"conversations.unpin\", \"Unpin\")}\n </>\n ) : (\n <>\n <Pin size={14} />\n {tr(\"conversations.pin\", \"Pin\")}\n </>\n )}\n </DropdownMenuItem>\n <DropdownMenuItem\n onSelect={() => {\n void (isMuted\n ? unmuteConversation(conversation.id)\n : muteConversation(conversation.id));\n }}\n >\n {isMuted ? (\n <>\n <Bell size={14} />\n {tr(\"conversations.unmute\", \"Unmute\")}\n </>\n ) : (\n <>\n <BellOff size={14} />\n {tr(\"conversations.mute\", \"Mute\")}\n </>\n )}\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onSelect={() => {\n void (isArchived\n ? unarchiveConversation(conversation.id)\n : archiveConversation(conversation.id));\n }}\n >\n {isArchived ? (\n <>\n <ArchiveRestore size={14} />\n {tr(\"conversations.unarchive\", \"Unarchive\")}\n </>\n ) : (\n <>\n <ArchiveIcon size={14} />\n {tr(\"conversations.archive\", \"Archive\")}\n </>\n )}\n </DropdownMenuItem>\n {(type === \"GROUP\" || type === \"CHANNEL\") && onLeave && (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onSelect={() => onLeave(conversation)}\n className=\"text-status-error-text focus:text-status-error-text\"\n >\n <LogOut size={14} />\n {tr(\"conversations.leave\", \"Leave\")}\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n );\n});\n"],"mappings":"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"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useConversations as e } from "../providers/ConversationsProvider.js";
|
|
2
|
-
import { useArchiveConversationMutation as t, useCreateConversationMutation as n, useGetConversationQuery as r, useGetConversationsQuery as i, useGetWorkspaceDirectoryQuery as a, useLeaveConversationMutation as o,
|
|
3
|
-
import { mapConversation as
|
|
4
|
-
import {
|
|
2
|
+
import { useArchiveConversationMutation as t, useCreateConversationMutation as n, useGetConversationQuery as r, useGetConversationsQuery as i, useGetWorkspaceDirectoryQuery as a, useLeaveConversationMutation as o, useMarkConversationAsReadMutation as s, useMuteConversationMutation as c, usePinConversationMutation as l, useSearchConversationsQuery as u, useUnarchiveConversationMutation as d, useUnmuteConversationMutation as f, useUnpinConversationMutation as p, useUpdateConversationByHookMutation as m } from "../../generated/wspace-operations.js";
|
|
3
|
+
import { mapConversation as h } from "../utils/mappers.js";
|
|
4
|
+
import { applyMarkConversationAsReadToCache as g } from "../utils/readReceiptCache.js";
|
|
5
|
+
import { useCallback as _, useEffect as v, useMemo as y, useState as b } from "react";
|
|
5
6
|
//#region src/conversations/hooks/useConversationQueries.ts
|
|
6
|
-
function
|
|
7
|
+
function x(t) {
|
|
7
8
|
let { workspaceId: n } = e(), r = i({
|
|
8
9
|
variables: {
|
|
9
10
|
first: t?.first ?? 25,
|
|
@@ -16,7 +17,7 @@ function y(t) {
|
|
|
16
17
|
}
|
|
17
18
|
},
|
|
18
19
|
skip: !n
|
|
19
|
-
}), a =
|
|
20
|
+
}), a = y(() => r.data?.conversations.edges.map((e) => h(e.node)) ?? [], [r.data]), o = r.data?.conversations.pageInfo.hasNextPage ?? !1;
|
|
20
21
|
return {
|
|
21
22
|
conversations: a,
|
|
22
23
|
loading: r.loading,
|
|
@@ -40,13 +41,13 @@ function y(t) {
|
|
|
40
41
|
}
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
|
-
function
|
|
44
|
+
function S(t) {
|
|
44
45
|
let { workspaceId: n } = e(), i = r({
|
|
45
46
|
variables: { id: t },
|
|
46
47
|
skip: !n || !t
|
|
47
48
|
});
|
|
48
49
|
return {
|
|
49
|
-
conversation: i.data?.conversation ?
|
|
50
|
+
conversation: i.data?.conversation ? h(i.data.conversation) : null,
|
|
50
51
|
members: i.data?.conversation?.members.edges.map((e) => e.node).filter((e) => !!e) ?? [],
|
|
51
52
|
membersTotalCount: i.data?.conversation?.members.totalCount ?? 0,
|
|
52
53
|
loading: i.loading,
|
|
@@ -56,8 +57,8 @@ function b(t) {
|
|
|
56
57
|
}
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
|
-
function
|
|
60
|
-
let { workspaceId: n } = e(), r = t.trim(), i =
|
|
60
|
+
function C(t) {
|
|
61
|
+
let { workspaceId: n } = e(), r = t.trim(), i = u({
|
|
61
62
|
variables: {
|
|
62
63
|
query: r,
|
|
63
64
|
first: 25
|
|
@@ -65,25 +66,25 @@ function x(t) {
|
|
|
65
66
|
skip: !n || r.length < 2
|
|
66
67
|
});
|
|
67
68
|
return {
|
|
68
|
-
conversations: i.data?.searchConversations.edges.map((e) =>
|
|
69
|
+
conversations: i.data?.searchConversations.edges.map((e) => h(e.node)) ?? [],
|
|
69
70
|
loading: i.loading,
|
|
70
71
|
error: i.error ?? null
|
|
71
72
|
};
|
|
72
73
|
}
|
|
73
|
-
var
|
|
74
|
-
function
|
|
75
|
-
let { workspaceId: n } = e(), [r, i] =
|
|
76
|
-
|
|
74
|
+
var w = 25, T = 250;
|
|
75
|
+
function E(t) {
|
|
76
|
+
let { workspaceId: n } = e(), [r, i] = b(t.trim());
|
|
77
|
+
v(() => {
|
|
77
78
|
let e = t.trim(), n = setTimeout(() => {
|
|
78
79
|
i(e);
|
|
79
|
-
},
|
|
80
|
+
}, T);
|
|
80
81
|
return () => clearTimeout(n);
|
|
81
82
|
}, [t]);
|
|
82
83
|
let o = a({
|
|
83
84
|
variables: {
|
|
84
85
|
filter: r ? { search: r } : void 0,
|
|
85
86
|
pagination: {
|
|
86
|
-
limit:
|
|
87
|
+
limit: w,
|
|
87
88
|
offset: 0
|
|
88
89
|
}
|
|
89
90
|
},
|
|
@@ -91,16 +92,16 @@ function w(t) {
|
|
|
91
92
|
fetchPolicy: "cache-and-network"
|
|
92
93
|
});
|
|
93
94
|
return {
|
|
94
|
-
members:
|
|
95
|
+
members: y(() => o.data?.workspaceDirectory.edges ?? [], [o.data]),
|
|
95
96
|
totalCount: o.data?.workspaceDirectory.totalCount ?? 0,
|
|
96
97
|
loading: o.loading,
|
|
97
98
|
error: o.error ?? null
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
|
-
function
|
|
101
|
+
function D() {
|
|
101
102
|
let { workspaceId: t } = e(), [r, i] = n();
|
|
102
103
|
return {
|
|
103
|
-
createConversation:
|
|
104
|
+
createConversation: _(async (e) => {
|
|
104
105
|
if (!t) throw Error("Workspace context is required to create a conversation");
|
|
105
106
|
try {
|
|
106
107
|
let { data: n, error: i } = await r({ variables: { input: {
|
|
@@ -112,7 +113,7 @@ function T() {
|
|
|
112
113
|
} } });
|
|
113
114
|
if (i) throw i;
|
|
114
115
|
let a = n?.createConversation.conversation;
|
|
115
|
-
return a ?
|
|
116
|
+
return a ? h(a) : null;
|
|
116
117
|
} catch (e) {
|
|
117
118
|
throw e instanceof Error ? e : /* @__PURE__ */ Error("Failed to create conversation");
|
|
118
119
|
}
|
|
@@ -121,10 +122,35 @@ function T() {
|
|
|
121
122
|
error: i.error ?? null
|
|
122
123
|
};
|
|
123
124
|
}
|
|
124
|
-
function
|
|
125
|
-
let [e,
|
|
125
|
+
function O() {
|
|
126
|
+
let [e, t] = s();
|
|
126
127
|
return {
|
|
127
|
-
|
|
128
|
+
markConversationAsRead: _(async (t) => {
|
|
129
|
+
try {
|
|
130
|
+
let { data: n, error: r } = await e({
|
|
131
|
+
variables: {
|
|
132
|
+
conversationId: t.conversationId,
|
|
133
|
+
messageId: t.messageId
|
|
134
|
+
},
|
|
135
|
+
update: (e, t) => {
|
|
136
|
+
let n = t.data?.markConversationAsRead;
|
|
137
|
+
n && g(e, n);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
if (r) throw r;
|
|
141
|
+
return !!n?.markConversationAsRead;
|
|
142
|
+
} catch (e) {
|
|
143
|
+
throw e instanceof Error ? e : /* @__PURE__ */ Error("Failed to mark conversation as read");
|
|
144
|
+
}
|
|
145
|
+
}, [e]),
|
|
146
|
+
loading: t.loading,
|
|
147
|
+
error: t.error ?? null
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function k() {
|
|
151
|
+
let [e, n] = t(), [r, i] = d(), [a, s] = l(), [u, m] = p(), [h, g] = c(), [v, y] = f(), [b, x] = o();
|
|
152
|
+
return {
|
|
153
|
+
archiveConversation: _(async (t) => {
|
|
128
154
|
let { data: n } = await e({
|
|
129
155
|
variables: { id: t },
|
|
130
156
|
refetchQueries: ["GetConversations"],
|
|
@@ -132,7 +158,7 @@ function E() {
|
|
|
132
158
|
});
|
|
133
159
|
return n?.archiveConversation.id === t;
|
|
134
160
|
}, [e]),
|
|
135
|
-
unarchiveConversation:
|
|
161
|
+
unarchiveConversation: _(async (e) => {
|
|
136
162
|
let { data: t } = await r({
|
|
137
163
|
variables: { id: e },
|
|
138
164
|
refetchQueries: ["GetConversations"],
|
|
@@ -140,23 +166,23 @@ function E() {
|
|
|
140
166
|
});
|
|
141
167
|
return t?.unarchiveConversation.id === e;
|
|
142
168
|
}, [r]),
|
|
143
|
-
pinConversation:
|
|
169
|
+
pinConversation: _(async (e) => {
|
|
144
170
|
let { data: t } = await a({ variables: { conversationId: e } });
|
|
145
171
|
return !!t?.pinConversation?.id;
|
|
146
172
|
}, [a]),
|
|
147
|
-
unpinConversation:
|
|
148
|
-
let { data: t } = await
|
|
173
|
+
unpinConversation: _(async (e) => {
|
|
174
|
+
let { data: t } = await u({ variables: { conversationId: e } });
|
|
149
175
|
return !!t?.unpinConversation?.id;
|
|
150
|
-
}, [
|
|
151
|
-
muteConversation:
|
|
152
|
-
let { data: t } = await
|
|
176
|
+
}, [u]),
|
|
177
|
+
muteConversation: _(async (e) => {
|
|
178
|
+
let { data: t } = await h({ variables: { conversationId: e } });
|
|
153
179
|
return !!t?.muteConversation?.id;
|
|
154
|
-
}, [
|
|
155
|
-
unmuteConversation:
|
|
180
|
+
}, [h]),
|
|
181
|
+
unmuteConversation: _(async (e) => {
|
|
156
182
|
let { data: t } = await v({ variables: { conversationId: e } });
|
|
157
183
|
return !!t?.unmuteConversation?.id;
|
|
158
184
|
}, [v]),
|
|
159
|
-
leaveConversation:
|
|
185
|
+
leaveConversation: _(async (e) => {
|
|
160
186
|
let { data: t } = await b({
|
|
161
187
|
variables: { conversationId: e },
|
|
162
188
|
refetchQueries: ["GetConversations"],
|
|
@@ -164,17 +190,17 @@ function E() {
|
|
|
164
190
|
});
|
|
165
191
|
return t?.leaveConversation.success ?? !1;
|
|
166
192
|
}, [b]),
|
|
167
|
-
loading: n.loading || i.loading ||
|
|
168
|
-
error: n.error ?? i.error ??
|
|
193
|
+
loading: n.loading || i.loading || s.loading || m.loading || g.loading || y.loading || x.loading,
|
|
194
|
+
error: n.error ?? i.error ?? s.error ?? m.error ?? g.error ?? y.error ?? x.error ?? null
|
|
169
195
|
};
|
|
170
196
|
}
|
|
171
|
-
function
|
|
172
|
-
let [e, t] =
|
|
197
|
+
function A() {
|
|
198
|
+
let [e, t] = m();
|
|
173
199
|
return {
|
|
174
|
-
updateConversation:
|
|
200
|
+
updateConversation: _(async (t) => {
|
|
175
201
|
try {
|
|
176
202
|
let { data: n } = await e({ variables: { input: t } }), r = n?.updateConversation?.conversation;
|
|
177
|
-
return r ?
|
|
203
|
+
return r ? h(r) : null;
|
|
178
204
|
} catch (e) {
|
|
179
205
|
throw e instanceof Error ? e : /* @__PURE__ */ Error("Failed to update conversation");
|
|
180
206
|
}
|
|
@@ -184,6 +210,6 @@ function D() {
|
|
|
184
210
|
};
|
|
185
211
|
}
|
|
186
212
|
//#endregion
|
|
187
|
-
export {
|
|
213
|
+
export { k as useConversationActions, D as useCreateConversation, S as useGetConversation, x as useGetConversations, O as useMarkConversationAsRead, C as useSearchConversations, A as useUpdateConversation, E as useWorkspaceDirectory };
|
|
188
214
|
|
|
189
215
|
//# sourceMappingURL=useConversationQueries.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useConversationQueries.js","names":[],"sources":["../../../src/conversations/hooks/useConversationQueries.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useState } from \"react\";\nimport {\n type GetConversationQuery,\n type GetWorkspaceDirectoryQuery,\n useArchiveConversationMutation,\n useCreateConversationMutation,\n useGetConversationQuery,\n useGetConversationsQuery,\n useGetWorkspaceDirectoryQuery,\n useMuteConversationMutation,\n useLeaveConversationMutation,\n usePinConversationMutation,\n useSearchConversationsQuery,\n useUnarchiveConversationMutation,\n useUnmuteConversationMutation,\n useUnpinConversationMutation,\n useUpdateConversationByHookMutation,\n} from \"../../generated/wspace-operations\";\nimport type * as WspaceTypes from \"../../generated/wspace-types\";\nimport type {\n Conversation,\n ConversationState,\n ConversationType,\n ConversationVisibility,\n} from \"../types\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { mapConversation } from \"../utils/mappers\";\n\ninterface UseGetConversationsOptions {\n first?: number;\n after?: string;\n types?: ConversationType[];\n /**\n * Conversation lifecycle states to include. When omitted the backend\n * returns ACTIVE conversations by default (filtering is union, not\n * exclusion, so `[\"ACTIVE\"]` and `undefined` are interchangeable for the\n * default inbox). Pass `[\"ARCHIVED\"]` to drive the archived tab.\n */\n states?: ConversationState[];\n hasUnread?: boolean;\n isPinned?: boolean;\n}\n\ninterface UseGetConversationsResult {\n conversations: Conversation[];\n loading: boolean;\n error: Error | null;\n hasNextPage: boolean;\n fetchMore: () => void;\n refetch: () => void;\n}\n\n/**\n * Hook: Fetch a paginated list of conversations.\n */\nexport function useGetConversations(\n options?: UseGetConversationsOptions,\n): UseGetConversationsResult {\n const { workspaceId } = useConversations();\n const queryResult = useGetConversationsQuery({\n variables: {\n first: options?.first ?? 25,\n after: options?.after,\n filter: {\n types: options?.types as WspaceTypes.InputMaybe<\n WspaceTypes.ConversationType[]\n >,\n states: options?.states as WspaceTypes.InputMaybe<\n WspaceTypes.ConversationState[]\n >,\n hasUnread: options?.hasUnread,\n isPinned: options?.isPinned,\n },\n },\n skip: !workspaceId,\n });\n\n const conversations = useMemo(\n () =>\n queryResult.data?.conversations.edges.map(\n (edge: { node: Record<string, unknown> }) => mapConversation(edge.node),\n ) ?? [],\n [queryResult.data],\n );\n\n const hasNextPage =\n queryResult.data?.conversations.pageInfo.hasNextPage ?? false;\n\n return {\n conversations,\n loading: queryResult.loading,\n error: queryResult.error ?? null,\n hasNextPage,\n fetchMore: () => {\n const endCursor = queryResult.data?.conversations.pageInfo.endCursor;\n if (!endCursor || !hasNextPage) return;\n void queryResult.fetchMore({\n variables: {\n first: options?.first ?? 25,\n after: endCursor,\n filter: {\n types: options?.types as WspaceTypes.InputMaybe<\n WspaceTypes.ConversationType[]\n >,\n states: options?.states as WspaceTypes.InputMaybe<\n WspaceTypes.ConversationState[]\n >,\n hasUnread: options?.hasUnread,\n isPinned: options?.isPinned,\n },\n },\n });\n },\n refetch: () => {\n void queryResult.refetch();\n },\n };\n}\n\ninterface UseGetConversationResult {\n conversation: Conversation | null;\n members: ConversationMember[];\n membersTotalCount: number;\n loading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\nexport type ConversationMember = NonNullable<\n GetConversationQuery[\"conversation\"]\n>[\"members\"][\"edges\"][number][\"node\"];\ntype ConversationMemberEdge = NonNullable<\n GetConversationQuery[\"conversation\"]\n>[\"members\"][\"edges\"][number];\n\n/**\n * Hook: Fetch a single conversation by ID.\n */\nexport function useGetConversation(\n conversationId: string,\n): UseGetConversationResult {\n const { workspaceId } = useConversations();\n const queryResult = useGetConversationQuery({\n variables: { id: conversationId },\n skip: !workspaceId || !conversationId,\n });\n\n return {\n conversation: queryResult.data?.conversation\n ? mapConversation(queryResult.data.conversation)\n : null,\n members:\n queryResult.data?.conversation?.members.edges\n .map((edge: ConversationMemberEdge) => edge.node)\n .filter(\n (member: ConversationMember | null): member is ConversationMember =>\n Boolean(member),\n ) ?? [],\n membersTotalCount: queryResult.data?.conversation?.members.totalCount ?? 0,\n loading: queryResult.loading,\n error: queryResult.error ?? null,\n refetch: () => {\n void queryResult.refetch();\n },\n };\n}\n\ninterface UseSearchConversationsResult {\n conversations: Conversation[];\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * Hook: Search conversations by query string.\n */\nexport function useSearchConversations(\n query: string,\n): UseSearchConversationsResult {\n const { workspaceId } = useConversations();\n const trimmedQuery = query.trim();\n const queryResult = useSearchConversationsQuery({\n variables: { query: trimmedQuery, first: 25 },\n skip: !workspaceId || trimmedQuery.length < 2,\n });\n\n return {\n conversations:\n queryResult.data?.searchConversations.edges.map(\n (edge: { node: Record<string, unknown> }) => mapConversation(edge.node),\n ) ?? [],\n loading: queryResult.loading,\n error: queryResult.error ?? null,\n };\n}\n\n/**\n * A workspace member surfaced by the directory, resolved to a friendly\n * identity. `id` is the member's user id (used for `participantIds`).\n */\nexport type WorkspaceDirectoryMember =\n GetWorkspaceDirectoryQuery[\"workspaceDirectory\"][\"edges\"][number];\n\ninterface UseWorkspaceDirectoryResult {\n members: WorkspaceDirectoryMember[];\n totalCount: number;\n loading: boolean;\n error: Error | null;\n}\n\nconst DIRECTORY_PAGE_SIZE = 25;\nconst DIRECTORY_SEARCH_DEBOUNCE_MS = 250;\n\n/**\n * Hook: Search the current workspace's member directory (server-side,\n * debounced). Backed by the conversations `workspaceDirectory` query, which is\n * a thin, identity-resolved wrapper over wspace-workspace-svc. An empty search\n * lists the first page of members so the picker shows suggestions before the\n * user types.\n */\nexport function useWorkspaceDirectory(\n search: string,\n): UseWorkspaceDirectoryResult {\n const { workspaceId } = useConversations();\n const [debouncedSearch, setDebouncedSearch] = useState(search.trim());\n\n useEffect(() => {\n const trimmed = search.trim();\n const handle = setTimeout(() => {\n setDebouncedSearch(trimmed);\n }, DIRECTORY_SEARCH_DEBOUNCE_MS);\n return () => clearTimeout(handle);\n }, [search]);\n\n const queryResult = useGetWorkspaceDirectoryQuery({\n variables: {\n filter: debouncedSearch ? { search: debouncedSearch } : undefined,\n pagination: { limit: DIRECTORY_PAGE_SIZE, offset: 0 },\n },\n skip: !workspaceId,\n fetchPolicy: \"cache-and-network\",\n });\n\n const members = useMemo(\n () => queryResult.data?.workspaceDirectory.edges ?? [],\n [queryResult.data],\n );\n\n return {\n members,\n totalCount: queryResult.data?.workspaceDirectory.totalCount ?? 0,\n loading: queryResult.loading,\n error: queryResult.error ?? null,\n };\n}\n\ninterface CreateConversationInput {\n type: ConversationType;\n title?: string;\n description?: string;\n participantIds: string[];\n visibility?: ConversationVisibility;\n}\n\ninterface UseCreateConversationResult {\n createConversation: (\n input: CreateConversationInput,\n ) => Promise<Conversation | null>;\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * Hook: Create a new conversation.\n */\nexport function useCreateConversation(): UseCreateConversationResult {\n const { workspaceId } = useConversations();\n const [createConversationMutation, mutationState] =\n useCreateConversationMutation();\n\n const createConversation = useCallback(\n async (input: CreateConversationInput): Promise<Conversation | null> => {\n if (!workspaceId) {\n throw new Error(\n \"Workspace context is required to create a conversation\",\n );\n }\n\n try {\n const { data, error } = await createConversationMutation({\n variables: {\n input: {\n workspaceId,\n type: input.type as WspaceTypes.ConversationType,\n title: input.title,\n description: input.description,\n participantIds: input.participantIds,\n },\n },\n });\n if (error) throw error;\n\n const conversation = data?.createConversation.conversation;\n return conversation ? mapConversation(conversation) : null;\n } catch (err) {\n throw err instanceof Error\n ? err\n : new Error(\"Failed to create conversation\");\n }\n },\n [createConversationMutation, workspaceId],\n );\n\n return {\n createConversation,\n loading: mutationState.loading,\n error: mutationState.error ?? null,\n };\n}\n\ninterface UseUpdateConversationResult {\n archiveConversation: (id: string) => Promise<boolean>;\n unarchiveConversation: (id: string) => Promise<boolean>;\n pinConversation: (id: string) => Promise<boolean>;\n unpinConversation: (id: string) => Promise<boolean>;\n muteConversation: (id: string) => Promise<boolean>;\n unmuteConversation: (id: string) => Promise<boolean>;\n leaveConversation: (id: string) => Promise<boolean>;\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * Hook: Conversation mutation operations.\n */\nexport function useConversationActions(): UseUpdateConversationResult {\n const [archiveConversationMutation, archiveState] =\n useArchiveConversationMutation();\n const [unarchiveConversationMutation, unarchiveState] =\n useUnarchiveConversationMutation();\n const [pinConversationMutation, pinState] = usePinConversationMutation();\n const [unpinConversationMutation, unpinState] =\n useUnpinConversationMutation();\n const [muteConversationMutation, muteState] = useMuteConversationMutation();\n const [unmuteConversationMutation, unmuteState] =\n useUnmuteConversationMutation();\n const [leaveConversationMutation, leaveState] =\n useLeaveConversationMutation();\n\n const archiveConversation = useCallback(\n async (id: string): Promise<boolean> => {\n // refetch GetConversations so the row drops out of the inbox (and the\n // Archived tab gains it) without waiting for a manual refresh.\n const { data } = await archiveConversationMutation({\n variables: { id },\n refetchQueries: [\"GetConversations\"],\n awaitRefetchQueries: true,\n });\n return data?.archiveConversation.id === id;\n },\n [archiveConversationMutation],\n );\n\n const unarchiveConversation = useCallback(\n async (id: string): Promise<boolean> => {\n const { data } = await unarchiveConversationMutation({\n variables: { id },\n refetchQueries: [\"GetConversations\"],\n awaitRefetchQueries: true,\n });\n return data?.unarchiveConversation.id === id;\n },\n [unarchiveConversationMutation],\n );\n\n const pinConversation = useCallback(\n async (id: string): Promise<boolean> => {\n const { data } = await pinConversationMutation({\n variables: { conversationId: id },\n });\n return Boolean(data?.pinConversation?.id);\n },\n [pinConversationMutation],\n );\n\n const unpinConversation = useCallback(\n async (id: string): Promise<boolean> => {\n const { data } = await unpinConversationMutation({\n variables: { conversationId: id },\n });\n return Boolean(data?.unpinConversation?.id);\n },\n [unpinConversationMutation],\n );\n\n const muteConversation = useCallback(\n async (id: string): Promise<boolean> => {\n const { data } = await muteConversationMutation({\n variables: { conversationId: id },\n });\n return Boolean(data?.muteConversation?.id);\n },\n [muteConversationMutation],\n );\n\n const unmuteConversation = useCallback(\n async (id: string): Promise<boolean> => {\n const { data } = await unmuteConversationMutation({\n variables: { conversationId: id },\n });\n return Boolean(data?.unmuteConversation?.id);\n },\n [unmuteConversationMutation],\n );\n\n const leaveConversation = useCallback(\n async (id: string): Promise<boolean> => {\n const { data } = await leaveConversationMutation({\n variables: { conversationId: id },\n refetchQueries: [\"GetConversations\"],\n awaitRefetchQueries: true,\n });\n return data?.leaveConversation.success ?? false;\n },\n [leaveConversationMutation],\n );\n\n return {\n archiveConversation,\n unarchiveConversation,\n pinConversation,\n unpinConversation,\n muteConversation,\n unmuteConversation,\n leaveConversation,\n loading:\n archiveState.loading ||\n unarchiveState.loading ||\n pinState.loading ||\n unpinState.loading ||\n muteState.loading ||\n unmuteState.loading ||\n leaveState.loading,\n error:\n (archiveState.error as Error | undefined) ??\n (unarchiveState.error as Error | undefined) ??\n (pinState.error as Error | undefined) ??\n (unpinState.error as Error | undefined) ??\n (muteState.error as Error | undefined) ??\n (unmuteState.error as Error | undefined) ??\n (leaveState.error as Error | undefined) ??\n null,\n };\n}\n\ninterface UseUpdateConversationOptions {\n updateConversation: (input: {\n id: string;\n title?: string;\n description?: string;\n avatarUrl?: string;\n settings?: Record<string, unknown>;\n }) => Promise<Conversation | null>;\n loading: boolean;\n error: Error | null;\n}\n\nexport function useUpdateConversation(): UseUpdateConversationOptions {\n const [updateConversationMutation, mutationState] =\n useUpdateConversationByHookMutation();\n\n const updateConversation = useCallback(\n async (input: {\n id: string;\n title?: string;\n description?: string;\n avatarUrl?: string;\n settings?: Record<string, unknown>;\n }): Promise<Conversation | null> => {\n try {\n const { data } = await updateConversationMutation({\n variables: { input },\n });\n const typedData = data as\n | {\n updateConversation?: {\n conversation?: Record<string, unknown> | null;\n };\n }\n | undefined;\n const conversation = typedData?.updateConversation?.conversation;\n return conversation\n ? mapConversation(conversation as Record<string, unknown>)\n : null;\n } catch (err) {\n throw err instanceof Error\n ? err\n : new Error(\"Failed to update conversation\");\n }\n },\n [updateConversationMutation],\n );\n\n return {\n updateConversation,\n loading: mutationState.loading,\n error: mutationState.error ?? null,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useConversationQueries.js","names":[],"sources":["../../../src/conversations/hooks/useConversationQueries.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { type ApolloCache, type FetchResult } from \"@apollo/client\";\nimport {\n type GetConversationQuery,\n type GetWorkspaceDirectoryQuery,\n type MarkConversationAsReadMutation,\n useArchiveConversationMutation,\n useCreateConversationMutation,\n useGetConversationQuery,\n useGetConversationsQuery,\n useGetWorkspaceDirectoryQuery,\n useMarkConversationAsReadMutation,\n useMuteConversationMutation,\n useLeaveConversationMutation,\n usePinConversationMutation,\n useSearchConversationsQuery,\n useUnarchiveConversationMutation,\n useUnmuteConversationMutation,\n useUnpinConversationMutation,\n useUpdateConversationByHookMutation,\n} from \"../../generated/wspace-operations\";\nimport type * as WspaceTypes from \"../../generated/wspace-types\";\nimport type {\n Conversation,\n ConversationState,\n ConversationType,\n ConversationVisibility,\n} from \"../types\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { mapConversation } from \"../utils/mappers\";\nimport { applyMarkConversationAsReadToCache } from \"../utils/readReceiptCache\";\n\ninterface UseGetConversationsOptions {\n first?: number;\n after?: string;\n types?: ConversationType[];\n /**\n * Conversation lifecycle states to include. When omitted the backend\n * returns ACTIVE conversations by default (filtering is union, not\n * exclusion, so `[\"ACTIVE\"]` and `undefined` are interchangeable for the\n * default inbox). Pass `[\"ARCHIVED\"]` to drive the archived tab.\n */\n states?: ConversationState[];\n hasUnread?: boolean;\n isPinned?: boolean;\n}\n\ninterface UseGetConversationsResult {\n conversations: Conversation[];\n loading: boolean;\n error: Error | null;\n hasNextPage: boolean;\n fetchMore: () => void;\n refetch: () => void;\n}\n\n/**\n * Hook: Fetch a paginated list of conversations.\n */\nexport function useGetConversations(\n options?: UseGetConversationsOptions,\n): UseGetConversationsResult {\n const { workspaceId } = useConversations();\n const queryResult = useGetConversationsQuery({\n variables: {\n first: options?.first ?? 25,\n after: options?.after,\n filter: {\n types: options?.types as WspaceTypes.InputMaybe<\n WspaceTypes.ConversationType[]\n >,\n states: options?.states as WspaceTypes.InputMaybe<\n WspaceTypes.ConversationState[]\n >,\n hasUnread: options?.hasUnread,\n isPinned: options?.isPinned,\n },\n },\n skip: !workspaceId,\n });\n\n const conversations = useMemo(\n () =>\n queryResult.data?.conversations.edges.map(\n (edge: { node: Record<string, unknown> }) => mapConversation(edge.node),\n ) ?? [],\n [queryResult.data],\n );\n\n const hasNextPage =\n queryResult.data?.conversations.pageInfo.hasNextPage ?? false;\n\n return {\n conversations,\n loading: queryResult.loading,\n error: queryResult.error ?? null,\n hasNextPage,\n fetchMore: () => {\n const endCursor = queryResult.data?.conversations.pageInfo.endCursor;\n if (!endCursor || !hasNextPage) return;\n void queryResult.fetchMore({\n variables: {\n first: options?.first ?? 25,\n after: endCursor,\n filter: {\n types: options?.types as WspaceTypes.InputMaybe<\n WspaceTypes.ConversationType[]\n >,\n states: options?.states as WspaceTypes.InputMaybe<\n WspaceTypes.ConversationState[]\n >,\n hasUnread: options?.hasUnread,\n isPinned: options?.isPinned,\n },\n },\n });\n },\n refetch: () => {\n void queryResult.refetch();\n },\n };\n}\n\ninterface UseGetConversationResult {\n conversation: Conversation | null;\n members: ConversationMember[];\n membersTotalCount: number;\n loading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\nexport type ConversationMember = NonNullable<\n GetConversationQuery[\"conversation\"]\n>[\"members\"][\"edges\"][number][\"node\"];\ntype ConversationMemberEdge = NonNullable<\n GetConversationQuery[\"conversation\"]\n>[\"members\"][\"edges\"][number];\n\n/**\n * Hook: Fetch a single conversation by ID.\n */\nexport function useGetConversation(\n conversationId: string,\n): UseGetConversationResult {\n const { workspaceId } = useConversations();\n const queryResult = useGetConversationQuery({\n variables: { id: conversationId },\n skip: !workspaceId || !conversationId,\n });\n\n return {\n conversation: queryResult.data?.conversation\n ? mapConversation(queryResult.data.conversation)\n : null,\n members:\n queryResult.data?.conversation?.members.edges\n .map((edge: ConversationMemberEdge) => edge.node)\n .filter(\n (member: ConversationMember | null): member is ConversationMember =>\n Boolean(member),\n ) ?? [],\n membersTotalCount: queryResult.data?.conversation?.members.totalCount ?? 0,\n loading: queryResult.loading,\n error: queryResult.error ?? null,\n refetch: () => {\n void queryResult.refetch();\n },\n };\n}\n\ninterface UseSearchConversationsResult {\n conversations: Conversation[];\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * Hook: Search conversations by query string.\n */\nexport function useSearchConversations(\n query: string,\n): UseSearchConversationsResult {\n const { workspaceId } = useConversations();\n const trimmedQuery = query.trim();\n const queryResult = useSearchConversationsQuery({\n variables: { query: trimmedQuery, first: 25 },\n skip: !workspaceId || trimmedQuery.length < 2,\n });\n\n return {\n conversations:\n queryResult.data?.searchConversations.edges.map(\n (edge: { node: Record<string, unknown> }) => mapConversation(edge.node),\n ) ?? [],\n loading: queryResult.loading,\n error: queryResult.error ?? null,\n };\n}\n\n/**\n * A workspace member surfaced by the directory, resolved to a friendly\n * identity. `id` is the member's user id (used for `participantIds`).\n */\nexport type WorkspaceDirectoryMember =\n GetWorkspaceDirectoryQuery[\"workspaceDirectory\"][\"edges\"][number];\n\ninterface UseWorkspaceDirectoryResult {\n members: WorkspaceDirectoryMember[];\n totalCount: number;\n loading: boolean;\n error: Error | null;\n}\n\nconst DIRECTORY_PAGE_SIZE = 25;\nconst DIRECTORY_SEARCH_DEBOUNCE_MS = 250;\n\n/**\n * Hook: Search the current workspace's member directory (server-side,\n * debounced). Backed by the conversations `workspaceDirectory` query, which is\n * a thin, identity-resolved wrapper over wspace-workspace-svc. An empty search\n * lists the first page of members so the picker shows suggestions before the\n * user types.\n */\nexport function useWorkspaceDirectory(\n search: string,\n): UseWorkspaceDirectoryResult {\n const { workspaceId } = useConversations();\n const [debouncedSearch, setDebouncedSearch] = useState(search.trim());\n\n useEffect(() => {\n const trimmed = search.trim();\n const handle = setTimeout(() => {\n setDebouncedSearch(trimmed);\n }, DIRECTORY_SEARCH_DEBOUNCE_MS);\n return () => clearTimeout(handle);\n }, [search]);\n\n const queryResult = useGetWorkspaceDirectoryQuery({\n variables: {\n filter: debouncedSearch ? { search: debouncedSearch } : undefined,\n pagination: { limit: DIRECTORY_PAGE_SIZE, offset: 0 },\n },\n skip: !workspaceId,\n fetchPolicy: \"cache-and-network\",\n });\n\n const members = useMemo(\n () => queryResult.data?.workspaceDirectory.edges ?? [],\n [queryResult.data],\n );\n\n return {\n members,\n totalCount: queryResult.data?.workspaceDirectory.totalCount ?? 0,\n loading: queryResult.loading,\n error: queryResult.error ?? null,\n };\n}\n\ninterface CreateConversationInput {\n type: ConversationType;\n title?: string;\n description?: string;\n participantIds: string[];\n visibility?: ConversationVisibility;\n}\n\ninterface UseCreateConversationResult {\n createConversation: (\n input: CreateConversationInput,\n ) => Promise<Conversation | null>;\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * Hook: Create a new conversation.\n */\nexport function useCreateConversation(): UseCreateConversationResult {\n const { workspaceId } = useConversations();\n const [createConversationMutation, mutationState] =\n useCreateConversationMutation();\n\n const createConversation = useCallback(\n async (input: CreateConversationInput): Promise<Conversation | null> => {\n if (!workspaceId) {\n throw new Error(\n \"Workspace context is required to create a conversation\",\n );\n }\n\n try {\n const { data, error } = await createConversationMutation({\n variables: {\n input: {\n workspaceId,\n type: input.type as WspaceTypes.ConversationType,\n title: input.title,\n description: input.description,\n participantIds: input.participantIds,\n },\n },\n });\n if (error) throw error;\n\n const conversation = data?.createConversation.conversation;\n return conversation ? mapConversation(conversation) : null;\n } catch (err) {\n throw err instanceof Error\n ? err\n : new Error(\"Failed to create conversation\");\n }\n },\n [createConversationMutation, workspaceId],\n );\n\n return {\n createConversation,\n loading: mutationState.loading,\n error: mutationState.error ?? null,\n };\n}\n\ninterface UseMarkConversationAsReadResult {\n markConversationAsRead: (input: {\n conversationId: string;\n messageId?: string;\n }) => Promise<boolean>;\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * Hook: Mark a conversation as read for the current viewer.\n *\n * Pass the newest message the viewer has seen as `messageId`; when omitted\n * the backend marks the whole conversation read. On success the cache update\n * zeroes the viewer-scoped `unreadCount`/`mentionCount` fields on the cached\n * Conversation entity — the list badge reads those top-level fields, which\n * membership normalization alone would leave stale — and drops the\n * conversation from any cached `hasUnread: true` list (see\n * `applyMarkConversationAsReadToCache`).\n */\nexport function useMarkConversationAsRead(): UseMarkConversationAsReadResult {\n const [markConversationAsReadMutation, mutationState] =\n useMarkConversationAsReadMutation();\n\n const markConversationAsRead = useCallback(\n async (input: {\n conversationId: string;\n messageId?: string;\n }): Promise<boolean> => {\n try {\n const { data, error } = await markConversationAsReadMutation({\n variables: {\n conversationId: input.conversationId,\n messageId: input.messageId,\n },\n update: (\n cache: ApolloCache,\n result: FetchResult<MarkConversationAsReadMutation>,\n ) => {\n const membership = result.data?.markConversationAsRead;\n if (!membership) return;\n applyMarkConversationAsReadToCache(cache, membership);\n },\n });\n if (error) throw error;\n return Boolean(data?.markConversationAsRead);\n } catch (err) {\n throw err instanceof Error\n ? err\n : new Error(\"Failed to mark conversation as read\");\n }\n },\n [markConversationAsReadMutation],\n );\n\n return {\n markConversationAsRead,\n loading: mutationState.loading,\n error: mutationState.error ?? null,\n };\n}\n\ninterface UseUpdateConversationResult {\n archiveConversation: (id: string) => Promise<boolean>;\n unarchiveConversation: (id: string) => Promise<boolean>;\n pinConversation: (id: string) => Promise<boolean>;\n unpinConversation: (id: string) => Promise<boolean>;\n muteConversation: (id: string) => Promise<boolean>;\n unmuteConversation: (id: string) => Promise<boolean>;\n leaveConversation: (id: string) => Promise<boolean>;\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * Hook: Conversation mutation operations.\n */\nexport function useConversationActions(): UseUpdateConversationResult {\n const [archiveConversationMutation, archiveState] =\n useArchiveConversationMutation();\n const [unarchiveConversationMutation, unarchiveState] =\n useUnarchiveConversationMutation();\n const [pinConversationMutation, pinState] = usePinConversationMutation();\n const [unpinConversationMutation, unpinState] =\n useUnpinConversationMutation();\n const [muteConversationMutation, muteState] = useMuteConversationMutation();\n const [unmuteConversationMutation, unmuteState] =\n useUnmuteConversationMutation();\n const [leaveConversationMutation, leaveState] =\n useLeaveConversationMutation();\n\n const archiveConversation = useCallback(\n async (id: string): Promise<boolean> => {\n // refetch GetConversations so the row drops out of the inbox (and the\n // Archived tab gains it) without waiting for a manual refresh.\n const { data } = await archiveConversationMutation({\n variables: { id },\n refetchQueries: [\"GetConversations\"],\n awaitRefetchQueries: true,\n });\n return data?.archiveConversation.id === id;\n },\n [archiveConversationMutation],\n );\n\n const unarchiveConversation = useCallback(\n async (id: string): Promise<boolean> => {\n const { data } = await unarchiveConversationMutation({\n variables: { id },\n refetchQueries: [\"GetConversations\"],\n awaitRefetchQueries: true,\n });\n return data?.unarchiveConversation.id === id;\n },\n [unarchiveConversationMutation],\n );\n\n const pinConversation = useCallback(\n async (id: string): Promise<boolean> => {\n const { data } = await pinConversationMutation({\n variables: { conversationId: id },\n });\n return Boolean(data?.pinConversation?.id);\n },\n [pinConversationMutation],\n );\n\n const unpinConversation = useCallback(\n async (id: string): Promise<boolean> => {\n const { data } = await unpinConversationMutation({\n variables: { conversationId: id },\n });\n return Boolean(data?.unpinConversation?.id);\n },\n [unpinConversationMutation],\n );\n\n const muteConversation = useCallback(\n async (id: string): Promise<boolean> => {\n const { data } = await muteConversationMutation({\n variables: { conversationId: id },\n });\n return Boolean(data?.muteConversation?.id);\n },\n [muteConversationMutation],\n );\n\n const unmuteConversation = useCallback(\n async (id: string): Promise<boolean> => {\n const { data } = await unmuteConversationMutation({\n variables: { conversationId: id },\n });\n return Boolean(data?.unmuteConversation?.id);\n },\n [unmuteConversationMutation],\n );\n\n const leaveConversation = useCallback(\n async (id: string): Promise<boolean> => {\n const { data } = await leaveConversationMutation({\n variables: { conversationId: id },\n refetchQueries: [\"GetConversations\"],\n awaitRefetchQueries: true,\n });\n return data?.leaveConversation.success ?? false;\n },\n [leaveConversationMutation],\n );\n\n return {\n archiveConversation,\n unarchiveConversation,\n pinConversation,\n unpinConversation,\n muteConversation,\n unmuteConversation,\n leaveConversation,\n loading:\n archiveState.loading ||\n unarchiveState.loading ||\n pinState.loading ||\n unpinState.loading ||\n muteState.loading ||\n unmuteState.loading ||\n leaveState.loading,\n error:\n (archiveState.error as Error | undefined) ??\n (unarchiveState.error as Error | undefined) ??\n (pinState.error as Error | undefined) ??\n (unpinState.error as Error | undefined) ??\n (muteState.error as Error | undefined) ??\n (unmuteState.error as Error | undefined) ??\n (leaveState.error as Error | undefined) ??\n null,\n };\n}\n\ninterface UseUpdateConversationOptions {\n updateConversation: (input: {\n id: string;\n title?: string;\n description?: string;\n avatarUrl?: string;\n settings?: Record<string, unknown>;\n }) => Promise<Conversation | null>;\n loading: boolean;\n error: Error | null;\n}\n\nexport function useUpdateConversation(): UseUpdateConversationOptions {\n const [updateConversationMutation, mutationState] =\n useUpdateConversationByHookMutation();\n\n const updateConversation = useCallback(\n async (input: {\n id: string;\n title?: string;\n description?: string;\n avatarUrl?: string;\n settings?: Record<string, unknown>;\n }): Promise<Conversation | null> => {\n try {\n const { data } = await updateConversationMutation({\n variables: { input },\n });\n const typedData = data as\n | {\n updateConversation?: {\n conversation?: Record<string, unknown> | null;\n };\n }\n | undefined;\n const conversation = typedData?.updateConversation?.conversation;\n return conversation\n ? mapConversation(conversation as Record<string, unknown>)\n : null;\n } catch (err) {\n throw err instanceof Error\n ? err\n : new Error(\"Failed to update conversation\");\n }\n },\n [updateConversationMutation],\n );\n\n return {\n updateConversation,\n loading: mutationState.loading,\n error: mutationState.error ?? null,\n };\n}\n"],"mappings":"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"}
|