@burdenoff/microfe-conversations 2026.522.1 → 2026.523.2

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.
Files changed (47) hide show
  1. package/dist/conversations/ConversationsUserRoutes.d.ts.map +1 -1
  2. package/dist/conversations/ConversationsUserRoutes.js +17 -19
  3. package/dist/conversations/ConversationsUserRoutes.js.map +1 -1
  4. package/dist/conversations/components/ConversationCard.d.ts.map +1 -1
  5. package/dist/conversations/components/ConversationCard.js +2 -1
  6. package/dist/conversations/components/ConversationCard.js.map +1 -1
  7. package/dist/conversations/components/LazyPageBoundary.d.ts +3 -3
  8. package/dist/conversations/components/LazyPageBoundary.d.ts.map +1 -1
  9. package/dist/conversations/components/LazyPageBoundary.js.map +1 -1
  10. package/dist/conversations/components/MessageBubble.d.ts.map +1 -1
  11. package/dist/conversations/components/MessageBubble.js +22 -16
  12. package/dist/conversations/components/MessageBubble.js.map +1 -1
  13. package/dist/conversations/components/PageLayout.d.ts +3 -1
  14. package/dist/conversations/components/PageLayout.d.ts.map +1 -1
  15. package/dist/conversations/components/PageLayout.js +2 -1
  16. package/dist/conversations/components/PageLayout.js.map +1 -1
  17. package/dist/conversations/hooks/useConversationQueries.js +1 -1
  18. package/dist/conversations/hooks/useConversationQueries.js.map +1 -1
  19. package/dist/conversations/hooks/useMessageQueries.d.ts +1 -0
  20. package/dist/conversations/hooks/useMessageQueries.d.ts.map +1 -1
  21. package/dist/conversations/hooks/useMessageQueries.js +48 -42
  22. package/dist/conversations/hooks/useMessageQueries.js.map +1 -1
  23. package/dist/conversations/index.js +1 -1
  24. package/dist/conversations/pages/ConversationAdminPages.d.ts.map +1 -1
  25. package/dist/conversations/pages/ConversationAdminPages.js +139 -138
  26. package/dist/conversations/pages/ConversationAdminPages.js.map +1 -1
  27. package/dist/conversations/pages/ConversationDetailPage.d.ts.map +1 -1
  28. package/dist/conversations/pages/ConversationDetailPage.js +256 -230
  29. package/dist/conversations/pages/ConversationDetailPage.js.map +1 -1
  30. package/dist/conversations/pages/ConversationListPage.d.ts +1 -2
  31. package/dist/conversations/pages/ConversationListPage.d.ts.map +1 -1
  32. package/dist/conversations/pages/ConversationListPage.js +138 -125
  33. package/dist/conversations/pages/ConversationListPage.js.map +1 -1
  34. package/dist/conversations/pages/ConversationsLandingPage.d.ts.map +1 -1
  35. package/dist/conversations/pages/ConversationsLandingPage.js +3 -1
  36. package/dist/conversations/pages/ConversationsLandingPage.js.map +1 -1
  37. package/dist/conversations/pages/ConversationsSettingsPage.d.ts.map +1 -1
  38. package/dist/conversations/pages/ConversationsSettingsPage.js +5 -4
  39. package/dist/conversations/pages/ConversationsSettingsPage.js.map +1 -1
  40. package/dist/conversations/pages/NewConversationPage.d.ts.map +1 -1
  41. package/dist/conversations/pages/NewConversationPage.js +4 -0
  42. package/dist/conversations/pages/NewConversationPage.js.map +1 -1
  43. package/dist/conversations/providers/ConversationsProvider.d.ts.map +1 -1
  44. package/dist/conversations/providers/ConversationsProvider.js +10 -8
  45. package/dist/conversations/providers/ConversationsProvider.js.map +1 -1
  46. package/dist/index.js +5 -5
  47. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationListPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationListPage.tsx"],"sourcesContent":["import { useMemo, useState, useCallback, useEffect } from \"react\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport { useNavigate, useSearchParams } from \"react-router-dom\";\nimport {\n Archive,\n Filter,\n LayoutGrid,\n LayoutList,\n MessageCircle,\n Pin,\n Plus,\n Rows3,\n Search,\n SlidersHorizontal,\n X,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport { ConversationCard } from \"../components/ConversationCard\";\nimport { EmptyState, PageLayout } from \"../components/PageLayout\";\nimport {\n useSearchConversations,\n useGetConversations,\n} from \"../hooks/useConversationQueries\";\nimport { useConversationStream } from \"../hooks/useMessageSubscriptions\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport type { Conversation, ConversationFilter } from \"../types\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { useTr } from \"../utils/i18n\";\nimport { joinPath } from \"../utils/navigation\";\n\n// Default filter shape applied whenever the `?filter=` URL param is absent\n// or unrecognised. Covers every dimension on `ConversationFilter` except\n// `searchQuery` — the text search is independent of URL-driven filters and\n// must not be wiped when the URL flips between filter values.\n//\n// The boolean toggles (hasUnread / isPinned / isMuted) are explicitly set\n// to `undefined` here (rather than omitted). Zustand's `set` merges\n// shallowly — the store's `updateConversationFilter` does\n// `{ ...currentFilter, ...partial }` — so an omitted key would preserve the\n// previous value, leaving a toggle like `hasUnread: true` sticky across a\n// URL that drops the filter param. Typing the constant as\n// `Partial<ConversationFilter>` widens the type so the explicit\n// `undefined` entries are accepted as `boolean | undefined` rather than\n// narrowed to the literal `undefined` under `as const`.\nconst FILTER_DEFAULTS: Partial<ConversationFilter> = {\n state: \"all\",\n type: \"all\",\n visibility: \"all\",\n hasUnread: undefined,\n isPinned: undefined,\n isMuted: undefined,\n};\n\ninterface ConversationListPageProps {\n forcedFilterParam?: \"channel\" | \"direct\" | \"group\" | \"archived\";\n}\n\nexport function ConversationListPage({\n forcedFilterParam,\n}: ConversationListPageProps = {}) {\n const navigate = useNavigate();\n const { basePath, onConversationSelect } = useConversations();\n const bus = useEventBus();\n const tr = useTr();\n const {\n searchQuery,\n setSearchQuery,\n viewMode,\n setViewMode,\n conversationFilter,\n updateConversationFilter,\n activeConversationId,\n setActiveConversationId,\n } = useConversationsStore();\n const [showFilters, setShowFilters] = useState(false);\n const [searchParams] = useSearchParams();\n\n // Sync URL filter param (?filter=channel|direct|group|archived) to the store\n // on mount / URL change. When the param is absent or unrecognised, reset\n // every filter dimension to its module-level default (`FILTER_DEFAULTS`) —\n // but deliberately do NOT touch `searchQuery`. The list's text search is\n // independent of URL-driven filters; zeroing it here would wipe in-progress\n // searches every time the URL flips between filter values or drops the\n // param entirely. Using `updateConversationFilter` with an explicit\n // full-defaults payload (instead of the store's `resetConversationFilter`)\n // keeps the merge exhaustive for filter dimensions while leaving\n // `searchQuery` alone.\n useEffect(() => {\n const filterParam = forcedFilterParam ?? searchParams.get(\"filter\");\n\n if (!filterParam) {\n updateConversationFilter(FILTER_DEFAULTS);\n setShowFilters(false);\n return;\n }\n\n const filterMap: Record<string, string> = {\n channel: \"CHANNEL\",\n direct: \"DIRECT\",\n group: \"GROUP\",\n archived: \"ARCHIVED\",\n };\n\n const mapped = filterMap[filterParam.toLowerCase()];\n if (!mapped) {\n updateConversationFilter(FILTER_DEFAULTS);\n setShowFilters(false);\n return;\n }\n\n // Start from a clean filter baseline (still preserving searchQuery) then\n // apply the URL-driven dimension, so switching between ?filter=direct and\n // ?filter=archived can't mix the boolean toggles from the previous URL.\n if (mapped === \"ARCHIVED\") {\n updateConversationFilter({ ...FILTER_DEFAULTS, state: \"ARCHIVED\" });\n } else {\n updateConversationFilter({\n ...FILTER_DEFAULTS,\n type: mapped as \"CHANNEL\" | \"DIRECT\" | \"GROUP\",\n });\n }\n setShowFilters(true);\n }, [forcedFilterParam, searchParams, updateConversationFilter]);\n\n const {\n conversations: baseConversations,\n loading,\n error,\n } = useGetConversations({\n first: 50,\n types:\n conversationFilter.type === \"all\" ? undefined : [conversationFilter.type],\n hasUnread: conversationFilter.hasUnread,\n isPinned: conversationFilter.isPinned,\n });\n\n // Live conversation lifecycle stream — keeps the list current without polling.\n // Apollo cache updates are handled inside the hook (append on add, evict on\n // delete, normalisation handles updates).\n useConversationStream();\n\n const {\n conversations: searchedConversations,\n loading: searchLoading,\n error: searchError,\n } = useSearchConversations(searchQuery);\n\n const conversations = useMemo(\n () => (searchQuery.trim() ? searchedConversations : baseConversations),\n [searchQuery, searchedConversations, baseConversations],\n );\n\n const filteredConversations = useMemo(() => {\n if (conversationFilter.state === \"all\") return conversations;\n return conversations.filter(\n (conversation) => conversation.state === conversationFilter.state,\n );\n }, [conversations, conversationFilter.state]);\n\n const pinnedConversations = useMemo(\n () => filteredConversations.filter((conversation) => conversation.isPinned),\n [filteredConversations],\n );\n\n const unpinnedConversations = useMemo(\n () =>\n filteredConversations.filter((conversation) => !conversation.isPinned),\n [filteredConversations],\n );\n\n const totalUnread = useMemo(\n () =>\n filteredConversations.reduce(\n (count, conversation) => count + conversation.unreadCount,\n 0,\n ),\n [filteredConversations],\n );\n\n const handleConversationClick = useCallback(\n (conversation: Conversation) => {\n setActiveConversationId(conversation.id);\n onConversationSelect?.(conversation);\n bus.emit(\"conversations.conversation.opened\", {\n conversationId: conversation.id,\n conversationType: conversation.type,\n });\n\n if (conversation.sourceContext?.contextHref) {\n try {\n const url = new URL(conversation.sourceContext.contextHref);\n if (url.origin === window.location.origin) {\n navigate(`${url.pathname}${url.search}${url.hash}`);\n return;\n }\n } catch {\n // Fall back to conversation detail below.\n }\n }\n\n navigate(joinPath(basePath || \"/conversations\", conversation.id));\n },\n [basePath, bus, navigate, onConversationSelect, setActiveConversationId],\n );\n\n const isLoading = loading || searchLoading;\n const activeError = error ?? searchError;\n const subtitle = [\n `${filteredConversations.length} ${tr(\"conversations.countLabel\", \"conversations\")}`,\n totalUnread > 0\n ? `${totalUnread} ${tr(\"conversations.unreadCountLabel\", \"unread\")}`\n : null,\n ]\n .filter(Boolean)\n .join(\" · \");\n\n return (\n <PageLayout\n title={tr(\"conversations.title\", \"Conversations\")}\n subtitle={subtitle}\n icon={<MessageCircle size={24} />}\n maxWidth=\"2xl\"\n headerActions={\n <button\n type=\"button\"\n onClick={() =>\n navigate(joinPath(basePath || \"/conversations\", \"new\"))\n }\n aria-label={tr(\n \"conversations.createNewConversation\",\n \"Create a new conversation\",\n )}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors shadow-sm\"\n >\n <Plus size={16} />\n {tr(\"conversations.newConversation\", \"New Conversation\")}\n </button>\n }\n >\n <div className=\"space-y-3\">\n <div className=\"flex items-center gap-2\">\n <div className=\"relative flex-1\">\n <Search\n size={16}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n />\n <input\n aria-label={tr(\"conversations.search\", \"Search conversations\")}\n type=\"text\"\n placeholder={tr(\n \"conversations.search\",\n \"Search conversations...\",\n )}\n value={searchQuery}\n onChange={(event) => setSearchQuery(event.target.value)}\n className=\"w-full pl-9 pr-8 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring\"\n />\n {searchQuery && (\n <button\n type=\"button\"\n onClick={() => setSearchQuery(\"\")}\n aria-label={tr(\n \"conversations.clearSearch\",\n \"Clear conversation search\",\n )}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 p-0.5 rounded text-muted-foreground hover:text-foreground\"\n >\n <X size={14} />\n </button>\n )}\n </div>\n\n <button\n type=\"button\"\n onClick={() => setShowFilters((state) => !state)}\n aria-label={\n showFilters\n ? tr(\"conversations.hideFilters\", \"Hide conversation filters\")\n : tr(\"conversations.showFilters\", \"Show conversation filters\")\n }\n aria-pressed={showFilters}\n className={cn(\n \"p-2 rounded-lg border transition-colors\",\n showFilters\n ? \"bg-primary/10 border-primary/30 text-primary\"\n : \"border-border text-muted-foreground hover:text-foreground hover:bg-muted/50\",\n )}\n >\n <SlidersHorizontal size={16} />\n </button>\n\n <div className=\"flex items-center border border-border rounded-lg overflow-hidden\">\n <ViewModeButton\n active={viewMode === \"list\"}\n icon={<LayoutList size={14} />}\n label={tr(\"conversations.listView\", \"List view\")}\n onClick={() => setViewMode(\"list\")}\n />\n <ViewModeButton\n active={viewMode === \"grid\"}\n icon={<LayoutGrid size={14} />}\n label={tr(\"conversations.gridView\", \"Grid view\")}\n onClick={() => setViewMode(\"grid\")}\n />\n <ViewModeButton\n active={viewMode === \"compact\"}\n icon={<Rows3 size={14} />}\n label={tr(\"conversations.compactView\", \"Compact view\")}\n onClick={() => setViewMode(\"compact\")}\n />\n </div>\n </div>\n\n {showFilters && (\n <div className=\"flex flex-wrap gap-2 animate-in fade-in-0 slide-in-from-top-2 duration-200\">\n <FilterChip\n label={tr(\"conversations.all\", \"All\")}\n active={conversationFilter.type === \"all\"}\n onClick={() => updateConversationFilter({ type: \"all\" })}\n />\n <FilterChip\n label={tr(\"conversations.direct\", \"Direct\")}\n active={conversationFilter.type === \"DIRECT\"}\n onClick={() => updateConversationFilter({ type: \"DIRECT\" })}\n />\n <FilterChip\n label={tr(\"conversations.groups\", \"Groups\")}\n active={conversationFilter.type === \"GROUP\"}\n onClick={() => updateConversationFilter({ type: \"GROUP\" })}\n />\n <FilterChip\n label={tr(\"conversations.channels\", \"Channels\")}\n active={conversationFilter.type === \"CHANNEL\"}\n onClick={() => updateConversationFilter({ type: \"CHANNEL\" })}\n />\n <div className=\"w-px h-6 bg-border self-center\" />\n <FilterChip\n label={tr(\"conversations.unread\", \"Unread\")}\n icon={<Filter size={12} />}\n active={conversationFilter.hasUnread === true}\n onClick={() =>\n updateConversationFilter({\n hasUnread: conversationFilter.hasUnread ? undefined : true,\n })\n }\n />\n <FilterChip\n label={tr(\"conversations.pinned\", \"Pinned\")}\n icon={<Pin size={12} />}\n active={conversationFilter.isPinned === true}\n onClick={() =>\n updateConversationFilter({\n isPinned: conversationFilter.isPinned ? undefined : true,\n })\n }\n />\n <FilterChip\n label={tr(\"conversations.archived\", \"Archived\")}\n icon={<Archive size={12} />}\n active={conversationFilter.state === \"ARCHIVED\"}\n onClick={() =>\n updateConversationFilter({\n state:\n conversationFilter.state === \"ARCHIVED\"\n ? \"all\"\n : \"ARCHIVED\",\n })\n }\n />\n </div>\n )}\n </div>\n\n {isLoading ? (\n <div className=\"px-4 py-8 text-sm text-muted-foreground\">\n {tr(\"conversations.loading\", \"Loading conversations...\")}\n </div>\n ) : activeError ? (\n <EmptyState\n icon={<MessageCircle size={32} />}\n title={tr(\n \"conversations.couldNotLoad\",\n \"Could not load conversations\",\n )}\n description={activeError.message}\n />\n ) : filteredConversations.length === 0 ? (\n <EmptyState\n icon={<MessageCircle size={32} />}\n title={tr(\"conversations.noConversations\", \"No conversations found\")}\n description={\n searchQuery\n ? `${tr(\"conversations.noMatchingConversations\", \"No conversations matching\")} \"${searchQuery}\"`\n : tr(\n \"conversations.startConversationPrompt\",\n \"Start a new conversation to get going\",\n )\n }\n />\n ) : (\n <div className=\"space-y-1\">\n {pinnedConversations.length > 0 && (\n <div>\n <div className=\"flex items-center gap-1.5 px-4 py-2\">\n <Pin size={12} className=\"text-muted-foreground\" />\n <span className=\"text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr(\"conversations.pinned\", \"Pinned\")}\n </span>\n </div>\n {pinnedConversations.map((conversation) => (\n <ConversationCard\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n </div>\n )}\n\n {unpinnedConversations.length > 0 && (\n <div>\n {pinnedConversations.length > 0 && (\n <div className=\"flex items-center gap-1.5 px-4 py-2 mt-2\">\n <span className=\"text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr(\"conversations.allConversations\", \"All Conversations\")}\n </span>\n </div>\n )}\n {unpinnedConversations.map((conversation) => (\n <ConversationCard\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n </div>\n )}\n </div>\n )}\n </PageLayout>\n );\n}\n\nfunction ViewModeButton({\n active,\n icon,\n label,\n onClick,\n}: {\n active: boolean;\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n aria-pressed={active}\n className={cn(\n \"p-1.5 transition-colors\",\n active\n ? \"bg-primary/10 text-primary\"\n : \"text-muted-foreground hover:text-foreground\",\n )}\n >\n {icon}\n </button>\n );\n}\n\nfunction FilterChip({\n label,\n icon,\n active,\n onClick,\n}: {\n label: string;\n icon?: React.ReactNode;\n active: boolean;\n onClick: () => void;\n}) {\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_LIST}\n allowUnavailablePermissions\n >\n <button\n type=\"button\"\n onClick={onClick}\n aria-pressed={active}\n className={cn(\n \"inline-flex items-center gap-1 px-3 py-1 text-xs font-medium rounded-full border transition-colors\",\n active\n ? \"bg-primary/10 border-primary/30 text-primary\"\n : \"bg-background border-border text-muted-foreground hover:text-foreground hover:bg-muted/50\",\n )}\n >\n {icon}\n {label}\n </button>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ConversationListPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationListPage.tsx"],"sourcesContent":["import {\n useMemo,\n useState,\n useCallback,\n useDeferredValue,\n useEffect,\n} from \"react\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport { useNavigate, useSearchParams } from \"react-router-dom\";\nimport {\n Archive,\n Filter,\n LayoutGrid,\n LayoutList,\n MessageCircle,\n Pin,\n Plus,\n Rows3,\n Search,\n SlidersHorizontal,\n X,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport { ConversationCard } from \"../components/ConversationCard\";\nimport { EmptyState, PageLayout } from \"../components/PageLayout\";\nimport {\n useSearchConversations,\n useGetConversations,\n} from \"../hooks/useConversationQueries\";\nimport { useConversationStream } from \"../hooks/useMessageSubscriptions\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport type { Conversation, ConversationFilter } from \"../types\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { useTr } from \"../utils/i18n\";\nimport { joinPath } from \"../utils/navigation\";\n\n// Default filter shape applied whenever the `?filter=` URL param is absent\n// or unrecognised. Covers every dimension on `ConversationFilter` except\n// `searchQuery` — the text search is independent of URL-driven filters and\n// must not be wiped when the URL flips between filter values.\n//\n// The boolean toggles (hasUnread / isPinned / isMuted) are explicitly set\n// to `undefined` here (rather than omitted). Zustand's `set` merges\n// shallowly — the store's `updateConversationFilter` does\n// `{ ...currentFilter, ...partial }` — so an omitted key would preserve the\n// previous value, leaving a toggle like `hasUnread: true` sticky across a\n// URL that drops the filter param. Typing the constant as\n// `Partial<ConversationFilter>` widens the type so the explicit\n// `undefined` entries are accepted as `boolean | undefined` rather than\n// narrowed to the literal `undefined` under `as const`.\nconst FILTER_DEFAULTS: Partial<ConversationFilter> = {\n state: \"all\",\n type: \"all\",\n visibility: \"all\",\n hasUnread: undefined,\n isPinned: undefined,\n isMuted: undefined,\n};\n\nfunction useDebouncedValue<T>(value: T, delayMs: number): T {\n const [debouncedValue, setDebouncedValue] = useState(value);\n\n useEffect(() => {\n const timeoutId = window.setTimeout(\n () => setDebouncedValue(value),\n delayMs,\n );\n return () => window.clearTimeout(timeoutId);\n }, [delayMs, value]);\n\n return debouncedValue;\n}\n\nexport interface ConversationListPageProps {\n forcedFilterParam?: \"channel\" | \"direct\" | \"group\" | \"archived\";\n}\n\nexport function ConversationListPage({\n forcedFilterParam,\n}: ConversationListPageProps = {}) {\n const navigate = useNavigate();\n const { basePath, onConversationSelect } = useConversations();\n const bus = useEventBus();\n const tr = useTr();\n const {\n searchQuery,\n setSearchQuery,\n viewMode,\n setViewMode,\n conversationFilter,\n updateConversationFilter,\n activeConversationId,\n setActiveConversationId,\n } = useConversationsStore();\n const [showFilters, setShowFilters] = useState(false);\n const [searchParams] = useSearchParams();\n const debouncedSearchQuery = useDebouncedValue(searchQuery, 250);\n const deferredSearchQuery = useDeferredValue(debouncedSearchQuery);\n const trimmedSearchQuery = deferredSearchQuery.trim();\n const isSearchActive = trimmedSearchQuery.length >= 2;\n\n // Sync URL filter param (?filter=channel|direct|group|archived) to the store\n // on mount / URL change. When the param is absent or unrecognised, reset\n // every filter dimension to its module-level default (`FILTER_DEFAULTS`) —\n // but deliberately do NOT touch `searchQuery`. The list's text search is\n // independent of URL-driven filters; zeroing it here would wipe in-progress\n // searches every time the URL flips between filter values or drops the\n // param entirely. Using `updateConversationFilter` with an explicit\n // full-defaults payload (instead of the store's `resetConversationFilter`)\n // keeps the merge exhaustive for filter dimensions while leaving\n // `searchQuery` alone.\n useEffect(() => {\n const filterParam = forcedFilterParam ?? searchParams.get(\"filter\");\n\n if (!filterParam) {\n updateConversationFilter(FILTER_DEFAULTS);\n setShowFilters(false);\n return;\n }\n\n const filterMap: Record<string, string> = {\n channel: \"CHANNEL\",\n direct: \"DIRECT\",\n group: \"GROUP\",\n archived: \"ARCHIVED\",\n };\n\n const mapped = filterMap[filterParam.toLowerCase()];\n if (!mapped) {\n updateConversationFilter(FILTER_DEFAULTS);\n setShowFilters(false);\n return;\n }\n\n // Start from a clean filter baseline (still preserving searchQuery) then\n // apply the URL-driven dimension, so switching between ?filter=direct and\n // ?filter=archived can't mix the boolean toggles from the previous URL.\n if (mapped === \"ARCHIVED\") {\n updateConversationFilter({ ...FILTER_DEFAULTS, state: \"ARCHIVED\" });\n } else {\n updateConversationFilter({\n ...FILTER_DEFAULTS,\n type: mapped as \"CHANNEL\" | \"DIRECT\" | \"GROUP\",\n });\n }\n setShowFilters(true);\n }, [forcedFilterParam, searchParams, updateConversationFilter]);\n\n const {\n conversations: baseConversations,\n loading,\n error,\n } = useGetConversations({\n first: 50,\n types:\n conversationFilter.type === \"all\" ? undefined : [conversationFilter.type],\n hasUnread: conversationFilter.hasUnread,\n isPinned: conversationFilter.isPinned,\n });\n\n // Live conversation lifecycle stream — keeps the list current without polling.\n // Apollo cache updates are handled inside the hook (append on add, evict on\n // delete, normalisation handles updates).\n useConversationStream();\n\n const {\n conversations: searchedConversations,\n loading: searchLoading,\n error: searchError,\n } = useSearchConversations(deferredSearchQuery);\n\n const conversations = useMemo(\n () => (isSearchActive ? searchedConversations : baseConversations),\n [isSearchActive, searchedConversations, baseConversations],\n );\n\n const filteredConversations = useMemo(() => {\n if (conversationFilter.state === \"all\") return conversations;\n return conversations.filter(\n (conversation) => conversation.state === conversationFilter.state,\n );\n }, [conversations, conversationFilter.state]);\n\n const pinnedConversations = useMemo(\n () => filteredConversations.filter((conversation) => conversation.isPinned),\n [filteredConversations],\n );\n\n const unpinnedConversations = useMemo(\n () =>\n filteredConversations.filter((conversation) => !conversation.isPinned),\n [filteredConversations],\n );\n\n const totalUnread = useMemo(\n () =>\n filteredConversations.reduce(\n (count, conversation) => count + conversation.unreadCount,\n 0,\n ),\n [filteredConversations],\n );\n\n const handleConversationClick = useCallback(\n (conversation: Conversation) => {\n setActiveConversationId(conversation.id);\n onConversationSelect?.(conversation);\n bus.emit(\"conversations.conversation.opened\", {\n conversationId: conversation.id,\n conversationType: conversation.type,\n });\n\n if (conversation.sourceContext?.contextHref) {\n try {\n const url = new URL(conversation.sourceContext.contextHref);\n if (url.origin === window.location.origin) {\n navigate(`${url.pathname}${url.search}${url.hash}`);\n return;\n }\n } catch {\n // Fall back to conversation detail below.\n }\n }\n\n navigate(joinPath(basePath || \"/conversations\", conversation.id));\n },\n [basePath, bus, navigate, onConversationSelect, setActiveConversationId],\n );\n\n const isLoading = loading || searchLoading;\n const activeError = error ?? searchError;\n const subtitle = [\n `${filteredConversations.length} ${tr(\"conversations.countLabel\", \"conversations\")}`,\n totalUnread > 0\n ? `${totalUnread} ${tr(\"conversations.unreadCountLabel\", \"unread\")}`\n : null,\n ]\n .filter(Boolean)\n .join(\" · \");\n\n return (\n <PageLayout\n title={tr(\"conversations.title\", \"Conversations\")}\n subtitle={subtitle}\n icon={<MessageCircle size={24} />}\n maxWidth=\"2xl\"\n headerActions={\n <button\n data-tour=\"conversation-new-button\"\n type=\"button\"\n onClick={() =>\n navigate(joinPath(basePath || \"/conversations\", \"new\"))\n }\n aria-label={tr(\n \"conversations.createNewConversation\",\n \"Create a new conversation\",\n )}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors shadow-sm\"\n >\n <Plus size={16} />\n {tr(\"conversations.newConversation\", \"New Conversation\")}\n </button>\n }\n >\n <div data-tour=\"conversation-list-header\" className=\"space-y-3\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <div className=\"relative min-w-[14rem] flex-1\">\n <Search\n size={16}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n />\n <input\n data-tour=\"conversation-search\"\n aria-label={tr(\"conversations.search\", \"Search conversations\")}\n type=\"text\"\n placeholder={tr(\n \"conversations.search\",\n \"Search conversations...\",\n )}\n value={searchQuery}\n onChange={(event) => setSearchQuery(event.target.value)}\n className=\"w-full pl-9 pr-8 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring\"\n />\n {searchQuery && (\n <button\n type=\"button\"\n onClick={() => setSearchQuery(\"\")}\n aria-label={tr(\n \"conversations.clearSearch\",\n \"Clear conversation search\",\n )}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 p-0.5 rounded text-muted-foreground hover:text-foreground\"\n >\n <X size={14} />\n </button>\n )}\n </div>\n\n <button\n data-tour=\"conversation-filters\"\n type=\"button\"\n onClick={() => setShowFilters((state) => !state)}\n aria-label={\n showFilters\n ? tr(\"conversations.hideFilters\", \"Hide conversation filters\")\n : tr(\"conversations.showFilters\", \"Show conversation filters\")\n }\n aria-pressed={showFilters}\n className={cn(\n \"p-2 rounded-lg border transition-colors\",\n showFilters\n ? \"bg-primary/10 border-primary/30 text-primary\"\n : \"border-border text-muted-foreground hover:text-foreground hover:bg-muted/50\",\n )}\n >\n <SlidersHorizontal size={16} />\n </button>\n\n <div\n data-tour=\"conversation-view-modes\"\n className=\"flex items-center border border-border rounded-lg overflow-hidden\"\n >\n <ViewModeButton\n active={viewMode === \"list\"}\n icon={<LayoutList size={14} />}\n label={tr(\"conversations.listView\", \"List view\")}\n onClick={() => setViewMode(\"list\")}\n />\n <ViewModeButton\n active={viewMode === \"grid\"}\n icon={<LayoutGrid size={14} />}\n label={tr(\"conversations.gridView\", \"Grid view\")}\n onClick={() => setViewMode(\"grid\")}\n />\n <ViewModeButton\n active={viewMode === \"compact\"}\n icon={<Rows3 size={14} />}\n label={tr(\"conversations.compactView\", \"Compact view\")}\n onClick={() => setViewMode(\"compact\")}\n />\n </div>\n </div>\n\n {showFilters && (\n <div className=\"flex flex-wrap gap-2 animate-in fade-in-0 slide-in-from-top-2 duration-200\">\n <FilterChip\n label={tr(\"conversations.all\", \"All\")}\n active={conversationFilter.type === \"all\"}\n onClick={() => updateConversationFilter({ type: \"all\" })}\n />\n <FilterChip\n label={tr(\"conversations.direct\", \"Direct\")}\n active={conversationFilter.type === \"DIRECT\"}\n onClick={() => updateConversationFilter({ type: \"DIRECT\" })}\n />\n <FilterChip\n label={tr(\"conversations.groups\", \"Groups\")}\n active={conversationFilter.type === \"GROUP\"}\n onClick={() => updateConversationFilter({ type: \"GROUP\" })}\n />\n <FilterChip\n label={tr(\"conversations.channels\", \"Channels\")}\n active={conversationFilter.type === \"CHANNEL\"}\n onClick={() => updateConversationFilter({ type: \"CHANNEL\" })}\n />\n <div className=\"w-px h-6 bg-border self-center\" />\n <FilterChip\n label={tr(\"conversations.unread\", \"Unread\")}\n icon={<Filter size={12} />}\n active={conversationFilter.hasUnread === true}\n onClick={() =>\n updateConversationFilter({\n hasUnread: conversationFilter.hasUnread ? undefined : true,\n })\n }\n />\n <FilterChip\n label={tr(\"conversations.pinned\", \"Pinned\")}\n icon={<Pin size={12} />}\n active={conversationFilter.isPinned === true}\n onClick={() =>\n updateConversationFilter({\n isPinned: conversationFilter.isPinned ? undefined : true,\n })\n }\n />\n <FilterChip\n label={tr(\"conversations.archived\", \"Archived\")}\n icon={<Archive size={12} />}\n active={conversationFilter.state === \"ARCHIVED\"}\n onClick={() =>\n updateConversationFilter({\n state:\n conversationFilter.state === \"ARCHIVED\"\n ? \"all\"\n : \"ARCHIVED\",\n })\n }\n />\n </div>\n )}\n </div>\n\n {isLoading ? (\n <div className=\"px-4 py-8 text-sm text-muted-foreground\">\n {tr(\"conversations.loading\", \"Loading conversations...\")}\n </div>\n ) : activeError ? (\n <EmptyState\n icon={<MessageCircle size={32} />}\n title={tr(\n \"conversations.couldNotLoad\",\n \"Could not load conversations\",\n )}\n description={activeError.message}\n />\n ) : filteredConversations.length === 0 ? (\n <EmptyState\n icon={<MessageCircle size={32} />}\n title={tr(\"conversations.noConversations\", \"No conversations found\")}\n description={\n isSearchActive\n ? `${tr(\"conversations.noMatchingConversations\", \"No conversations matching\")} \"${searchQuery}\"`\n : tr(\n \"conversations.startConversationPrompt\",\n \"Start a new conversation to get going\",\n )\n }\n />\n ) : (\n <div data-tour=\"conversation-list-results\" className=\"space-y-1\">\n {pinnedConversations.length > 0 && (\n <div>\n <div className=\"flex items-center gap-1.5 px-4 py-2\">\n <Pin size={12} className=\"text-muted-foreground\" />\n <span className=\"text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr(\"conversations.pinned\", \"Pinned\")}\n </span>\n </div>\n {pinnedConversations.map((conversation) => (\n <ConversationCard\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n </div>\n )}\n\n {unpinnedConversations.length > 0 && (\n <div>\n {pinnedConversations.length > 0 && (\n <div className=\"flex items-center gap-1.5 px-4 py-2 mt-2\">\n <span className=\"text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr(\"conversations.allConversations\", \"All Conversations\")}\n </span>\n </div>\n )}\n {unpinnedConversations.map((conversation) => (\n <ConversationCard\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n </div>\n )}\n </div>\n )}\n </PageLayout>\n );\n}\n\nfunction ViewModeButton({\n active,\n icon,\n label,\n onClick,\n}: {\n active: boolean;\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n aria-pressed={active}\n className={cn(\n \"flex min-h-10 min-w-10 items-center justify-center p-2 transition-colors\",\n active\n ? \"bg-primary/10 text-primary\"\n : \"text-muted-foreground hover:text-foreground\",\n )}\n >\n {icon}\n </button>\n );\n}\n\nfunction FilterChip({\n label,\n icon,\n active,\n onClick,\n}: {\n label: string;\n icon?: React.ReactNode;\n active: boolean;\n onClick: () => void;\n}) {\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <button\n type=\"button\"\n onClick={onClick}\n aria-pressed={active}\n className={cn(\n \"inline-flex min-h-9 items-center gap-1 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors\",\n active\n ? \"bg-primary/10 border-primary/30 text-primary\"\n : \"bg-background border-border text-muted-foreground hover:text-foreground hover:bg-muted/50\",\n )}\n >\n {icon}\n {label}\n </button>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationsLandingPage.d.ts","sourceRoot":"","sources":["../../../src/conversations/pages/ConversationsLandingPage.tsx"],"names":[],"mappings":"AAqCA;;;;;;GAMG;AACH,wBAAgB,wBAAwB,4CA0HvC"}
1
+ {"version":3,"file":"ConversationsLandingPage.d.ts","sourceRoot":"","sources":["../../../src/conversations/pages/ConversationsLandingPage.tsx"],"names":[],"mappings":"AAqCA;;;;;;GAMG;AACH,wBAAgB,wBAAwB,4CA8HvC"}
@@ -74,9 +74,10 @@ function h() {
74
74
  x,
75
75
  g
76
76
  ]), /* @__PURE__ */ l(e, {
77
- permission: t.CONVERSATION_LIST,
77
+ permission: t.CONVERSATION_READ,
78
78
  allowUnavailablePermissions: !0,
79
79
  children: /* @__PURE__ */ u("main", {
80
+ "data-tour": "conversations-landing",
80
81
  className: "flex flex-col h-full",
81
82
  "aria-labelledby": "conversations-page-heading",
82
83
  children: [
@@ -86,6 +87,7 @@ function h() {
86
87
  children: v("conversations.title", "Conversations")
87
88
  }),
88
89
  /* @__PURE__ */ l("div", {
90
+ "data-tour": "conversations-tabs",
89
91
  className: "border-b border-border-subtle px-4 pt-3",
90
92
  children: /* @__PURE__ */ l(f, {
91
93
  items: b,
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationsLandingPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationsLandingPage.tsx"],"sourcesContent":["import { useEffect, useMemo } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\nimport { Tabs, type TabItem } from \"@burdenoff/fe-libs/chrome\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\n\nimport { ConversationListPage } from \"./ConversationListPage\";\nimport { NewConversationPage } from \"./NewConversationPage\";\nimport { ConversationsSettingsPage } from \"./ConversationsSettingsPage\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { useUserFeatureFlags } from \"../hooks/useUserFeatureFlags\";\n\nconst DEFAULT_TAB_ID = \"all\";\n\n/**\n * Coming-soon placeholder shown for tabs whose dedicated pages\n * are not yet implemented in this microfrontend.\n */\nfunction ComingSoon({ title }: { title: string }) {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n return (\n <div className=\"flex h-full items-center justify-center p-8 text-center\">\n <div className=\"space-y-2\">\n <h3 className=\"text-lg font-semibold text-foreground\">{title}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {tr(\"conversations.comingSoon\", \"Coming soon.\")}\n </p>\n </div>\n </div>\n );\n}\n\n/**\n * Tabbed landing page for the Conversations microfrontend.\n *\n * Renders an underline tab bar at the top and swaps the active panel\n * based on the `?tab=...` URL search param. The first tab (\"all\") uses\n * a clean URL with no `tab` param.\n */\nexport function ConversationsLandingPage() {\n const [params, setParams] = useSearchParams();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const { archivedEnabled } = useUserFeatureFlags();\n\n const tabs: TabItem[] = useMemo(\n () => [\n {\n id: DEFAULT_TAB_ID,\n label: tr(\"conversations.allConversations\", \"All Conversations\"),\n },\n { id: \"channels\", label: tr(\"conversations.channels\", \"Channels\") },\n {\n id: \"direct\",\n label: tr(\"conversations.directMessages\", \"Direct Messages\"),\n },\n { id: \"groups\", label: tr(\"conversations.groups\", \"Groups\") },\n ...(archivedEnabled\n ? [{ id: \"archived\", label: tr(\"conversations.archived\", \"Archived\") }]\n : []),\n { id: \"new\", label: tr(\"conversations.new\", \"New\") },\n { id: \"settings\", label: tr(\"conversations.settings\", \"Settings\") },\n ],\n // Suppression is for `tr`, which closes over `t`; `archivedEnabled` IS listed.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [t, archivedEnabled],\n );\n\n const currentTab = params.get(\"tab\");\n // Render `activeId` correctly for the archived-off case; the effect below cleans the URL lazily.\n const activeId =\n currentTab === \"archived\" && !archivedEnabled\n ? DEFAULT_TAB_ID\n : (currentTab ?? DEFAULT_TAB_ID);\n\n useEffect(() => {\n if (!archivedEnabled && currentTab === \"archived\") {\n // Updater form reads the freshest params at flush time to preserve concurrent query-param changes.\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, currentTab, setParams]);\n\n const handleTabChange = (id: string) => {\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\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_LIST}\n allowUnavailablePermissions\n >\n <main\n className=\"flex flex-col h-full\"\n aria-labelledby=\"conversations-page-heading\"\n >\n <h1 id=\"conversations-page-heading\" className=\"sr-only\">\n {tr(\"conversations.title\", \"Conversations\")}\n </h1>\n <div className=\"border-b border-border-subtle px-4 pt-3\">\n <Tabs\n items={tabs}\n activeTabId={activeId}\n onTabChange={handleTabChange}\n variant=\"underline\"\n />\n </div>\n <div\n className=\"flex-1 min-h-0 overflow-auto\"\n role=\"region\"\n aria-live=\"polite\"\n aria-label={tr(\n \"conversations.activePanel\",\n \"Active conversations panel\",\n )}\n >\n {activeId === \"all\" && <ConversationListPage />}\n {activeId === \"channels\" && (\n <ConversationListPage forcedFilterParam=\"channel\" />\n )}\n {activeId === \"direct\" && (\n <ConversationListPage forcedFilterParam=\"direct\" />\n )}\n {activeId === \"groups\" && (\n <ConversationListPage forcedFilterParam=\"group\" />\n )}\n {activeId === \"archived\" && (\n <ComingSoon\n title={tr(\n \"conversations.archivedTitle\",\n \"Archived Conversations\",\n )}\n />\n )}\n {activeId === \"new\" && <NewConversationPage />}\n {activeId === \"settings\" && <ConversationsSettingsPage />}\n </div>\n </main>\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 { useEffect, useMemo } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\nimport { Tabs, type TabItem } from \"@burdenoff/fe-libs/chrome\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\n\nimport { ConversationListPage } from \"./ConversationListPage\";\nimport { NewConversationPage } from \"./NewConversationPage\";\nimport { ConversationsSettingsPage } from \"./ConversationsSettingsPage\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { useUserFeatureFlags } from \"../hooks/useUserFeatureFlags\";\n\nconst DEFAULT_TAB_ID = \"all\";\n\n/**\n * Coming-soon placeholder shown for tabs whose dedicated pages\n * are not yet implemented in this microfrontend.\n */\nfunction ComingSoon({ title }: { title: string }) {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n return (\n <div className=\"flex h-full items-center justify-center p-8 text-center\">\n <div className=\"space-y-2\">\n <h3 className=\"text-lg font-semibold text-foreground\">{title}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {tr(\"conversations.comingSoon\", \"Coming soon.\")}\n </p>\n </div>\n </div>\n );\n}\n\n/**\n * Tabbed landing page for the Conversations microfrontend.\n *\n * Renders an underline tab bar at the top and swaps the active panel\n * based on the `?tab=...` URL search param. The first tab (\"all\") uses\n * a clean URL with no `tab` param.\n */\nexport function ConversationsLandingPage() {\n const [params, setParams] = useSearchParams();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const { archivedEnabled } = useUserFeatureFlags();\n\n const tabs: TabItem[] = useMemo(\n () => [\n {\n id: DEFAULT_TAB_ID,\n label: tr(\"conversations.allConversations\", \"All Conversations\"),\n },\n { id: \"channels\", label: tr(\"conversations.channels\", \"Channels\") },\n {\n id: \"direct\",\n label: tr(\"conversations.directMessages\", \"Direct Messages\"),\n },\n { id: \"groups\", label: tr(\"conversations.groups\", \"Groups\") },\n ...(archivedEnabled\n ? [{ id: \"archived\", label: tr(\"conversations.archived\", \"Archived\") }]\n : []),\n { id: \"new\", label: tr(\"conversations.new\", \"New\") },\n { id: \"settings\", label: tr(\"conversations.settings\", \"Settings\") },\n ],\n // Suppression is for `tr`, which closes over `t`; `archivedEnabled` IS listed.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [t, archivedEnabled],\n );\n\n const currentTab = params.get(\"tab\");\n // Render `activeId` correctly for the archived-off case; the effect below cleans the URL lazily.\n const activeId =\n currentTab === \"archived\" && !archivedEnabled\n ? DEFAULT_TAB_ID\n : (currentTab ?? DEFAULT_TAB_ID);\n\n useEffect(() => {\n if (!archivedEnabled && currentTab === \"archived\") {\n // Updater form reads the freshest params at flush time to preserve concurrent query-param changes.\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, currentTab, setParams]);\n\n const handleTabChange = (id: string) => {\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\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <main\n data-tour=\"conversations-landing\"\n className=\"flex flex-col h-full\"\n aria-labelledby=\"conversations-page-heading\"\n >\n <h1 id=\"conversations-page-heading\" className=\"sr-only\">\n {tr(\"conversations.title\", \"Conversations\")}\n </h1>\n <div\n data-tour=\"conversations-tabs\"\n className=\"border-b border-border-subtle px-4 pt-3\"\n >\n <Tabs\n items={tabs}\n activeTabId={activeId}\n onTabChange={handleTabChange}\n variant=\"underline\"\n />\n </div>\n <div\n className=\"flex-1 min-h-0 overflow-auto\"\n role=\"region\"\n aria-live=\"polite\"\n aria-label={tr(\n \"conversations.activePanel\",\n \"Active conversations panel\",\n )}\n >\n {activeId === \"all\" && <ConversationListPage />}\n {activeId === \"channels\" && (\n <ConversationListPage forcedFilterParam=\"channel\" />\n )}\n {activeId === \"direct\" && (\n <ConversationListPage forcedFilterParam=\"direct\" />\n )}\n {activeId === \"groups\" && (\n <ConversationListPage forcedFilterParam=\"group\" />\n )}\n {activeId === \"archived\" && (\n <ComingSoon\n title={tr(\n \"conversations.archivedTitle\",\n \"Archived Conversations\",\n )}\n />\n )}\n {activeId === \"new\" && <NewConversationPage />}\n {activeId === \"settings\" && <ConversationsSettingsPage />}\n </div>\n </main>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationsSettingsPage.d.ts","sourceRoot":"","sources":["../../../src/conversations/pages/ConversationsSettingsPage.tsx"],"names":[],"mappings":"AAgBA,wBAAgB,yBAAyB,4CA8QxC"}
1
+ {"version":3,"file":"ConversationsSettingsPage.d.ts","sourceRoot":"","sources":["../../../src/conversations/pages/ConversationsSettingsPage.tsx"],"names":[],"mappings":"AAgBA,wBAAgB,yBAAyB,4CA+QxC"}
@@ -1,7 +1,7 @@
1
1
  import { useConversations as e } from "../providers/ConversationsProvider.js";
2
2
  import { PageLayout as t, PageSection as n } from "../components/PageLayout.js";
3
- import { useGetConversation as r, useUpdateConversation as i } from "../hooks/useConversationQueries.js";
4
- import { useConversationsStore as a } from "../store/conversationsStore.js";
3
+ import { useConversationsStore as r } from "../store/conversationsStore.js";
4
+ import { useGetConversation as i, useUpdateConversation as a } from "../hooks/useConversationQueries.js";
5
5
  import { PermissionGuard as o } from "../components/PermissionGuard.js";
6
6
  import { CONVERSATIONS_PERMISSIONS as s } from "../constants/permissions.js";
7
7
  import { nativeImpact as c, nativeNotify as l } from "../../utils/nativeBridge.js";
@@ -13,10 +13,10 @@ import { Fragment as x, jsx as S, jsxs as C } from "react/jsx-runtime";
13
13
  import { useI18n as w } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
14
14
  //#region src/conversations/pages/ConversationsSettingsPage.tsx
15
15
  function T() {
16
- let T = b(), { basePath: D } = e(), { activeConversationId: O } = a(), { t: k } = w(), A = (e, t) => {
16
+ let T = b(), { basePath: D } = e(), { activeConversationId: O } = r(), { t: k } = w(), A = (e, t) => {
17
17
  let n = k(e);
18
18
  return n === e ? t : n;
19
- }, { conversation: j, loading: M } = r(O ?? ""), { updateConversation: N, loading: P, error: F } = i(), [I, L] = p(!0), [R, z] = p(!0), [B, V] = p("ALL"), [H, U] = p(!1), W = f(null), { exportComments: G, loading: K, downloadUrl: q } = u();
19
+ }, { conversation: j, loading: M } = i(O ?? ""), { updateConversation: N, loading: P, error: F } = a(), [I, L] = p(!0), [R, z] = p(!0), [B, V] = p("ALL"), [H, U] = p(!1), W = f(null), { exportComments: G, loading: K, downloadUrl: q } = u();
20
20
  d(() => {
21
21
  let e = j?.settings && typeof j.settings == "object" ? j.settings : null;
22
22
  e && (typeof e.showTypingIndicators == "boolean" && L(e.showTypingIndicators), typeof e.showReadReceipts == "boolean" && z(e.showReadReceipts), typeof e.defaultNotificationLevel == "string" && V(e.defaultNotificationLevel));
@@ -40,6 +40,7 @@ function T() {
40
40
  }
41
41
  };
42
42
  return /* @__PURE__ */ S(t, {
43
+ dataTour: "conversation-settings-page",
43
44
  title: A("conversations.settings", "Settings"),
44
45
  subtitle: j ? `${A("conversations.settingsFor", "Conversation settings for")} ${j.title}` : A("conversations.selectConversationForSettings", "Select a conversation to edit settings"),
45
46
  icon: /* @__PURE__ */ S(y, { size: 24 }),
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationsSettingsPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationsSettingsPage.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { useNavigate } from \"react-router-dom\";\nimport { ArrowLeft, Bell, Download, Eye, Save, Settings } from \"lucide-react\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport {\n useGetConversation,\n useUpdateConversation,\n} from \"../hooks/useConversationQueries\";\nimport { useExportConversation } from \"../hooks/useExportConversation\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { nativeImpact, nativeNotify } from \"../../utils/nativeBridge\";\n\nexport function ConversationsSettingsPage() {\n const navigate = useNavigate();\n const { basePath } = useConversations();\n const { activeConversationId } = useConversationsStore();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const { conversation, loading: conversationLoading } = useGetConversation(\n activeConversationId ?? \"\",\n );\n const {\n updateConversation,\n loading: saving,\n error: saveError,\n } = useUpdateConversation();\n\n const [showTypingIndicators, setShowTypingIndicators] = useState(true);\n const [showReadReceipts, setShowReadReceipts] = useState(true);\n const [defaultNotificationLevel, setDefaultNotificationLevel] =\n useState(\"ALL\");\n const [saved, setSaved] = useState(false);\n const downloadLinkRef = useRef<HTMLAnchorElement>(null);\n const {\n exportComments,\n loading: exporting,\n downloadUrl,\n } = useExportConversation();\n\n useEffect(() => {\n const settings =\n conversation?.settings && typeof conversation.settings === \"object\"\n ? (conversation.settings as Record<string, unknown>)\n : null;\n if (!settings) return;\n if (typeof settings.showTypingIndicators === \"boolean\") {\n setShowTypingIndicators(settings.showTypingIndicators);\n }\n if (typeof settings.showReadReceipts === \"boolean\") {\n setShowReadReceipts(settings.showReadReceipts);\n }\n if (typeof settings.defaultNotificationLevel === \"string\") {\n setDefaultNotificationLevel(settings.defaultNotificationLevel);\n }\n }, [conversation]);\n\n const handleSave = async () => {\n if (!conversation?.id) return;\n nativeImpact(\"light\");\n try {\n const updated = await updateConversation({\n id: conversation.id,\n settings: {\n showTypingIndicators,\n showReadReceipts,\n defaultNotificationLevel,\n },\n });\n const ok = Boolean(updated);\n nativeNotify(ok ? \"success\" : \"error\");\n setSaved(ok);\n } catch (error) {\n nativeNotify(\"error\");\n throw error;\n }\n };\n\n return (\n <PageLayout\n title={tr(\"conversations.settings\", \"Settings\")}\n subtitle={\n conversation\n ? `${tr(\"conversations.settingsFor\", \"Conversation settings for\")} ${conversation.title}`\n : tr(\n \"conversations.selectConversationForSettings\",\n \"Select a conversation to edit settings\",\n )\n }\n icon={<Settings size={24} />}\n maxWidth=\"lg\"\n headerActions={\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigate(basePath || \"/conversations\")}\n className=\"inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors\"\n >\n <ArrowLeft size={16} />\n {tr(\"conversations.back\", \"Back\")}\n </button>\n <button\n type=\"button\"\n onClick={() => void handleSave()}\n disabled={saving || !conversation}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors shadow-sm disabled:opacity-50\"\n >\n <Save size={16} />\n {saving\n ? tr(\"conversations.saving\", \"Saving...\")\n : tr(\"conversations.saveChanges\", \"Save Changes\")}\n </button>\n </div>\n }\n >\n {conversationLoading ? (\n <div className=\"text-sm text-muted-foreground\">\n {tr(\"conversations.loadingSettings\", \"Loading settings...\")}\n </div>\n ) : !conversation ? (\n <div className=\"text-sm text-muted-foreground\">\n {tr(\n \"conversations.openConversationFirst\",\n \"Open a conversation first, then come back to settings.\",\n )}\n </div>\n ) : (\n <>\n <PageSection\n title={tr(\"conversations.notifications\", \"Notifications\")}\n description={tr(\n \"conversations.notificationsDescription\",\n \"Configure delivery behavior for this conversation\",\n )}\n >\n <div className=\"space-y-4 rounded-xl border border-border p-4\">\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex items-start gap-3\">\n <div className=\"p-2 rounded-lg bg-status-info-bg-subtle text-status-info-text\">\n <Bell size={16} />\n </div>\n <div>\n <p className=\"text-sm font-medium text-foreground\">\n {tr(\n \"conversations.defaultNotificationLevel\",\n \"Default Notification Level\",\n )}\n </p>\n <p className=\"text-xs text-muted-foreground mt-0.5\">\n {tr(\n \"conversations.defaultNotificationLevelHint\",\n \"This updates conversation settings JSON via GraphQL.\",\n )}\n </p>\n </div>\n </div>\n <select\n value={defaultNotificationLevel}\n onChange={(event) =>\n setDefaultNotificationLevel(event.target.value)\n }\n className=\"px-3 py-1.5 text-sm rounded-lg border border-border bg-background focus:outline-none focus:ring-2 focus:ring-ring\"\n >\n <option value=\"ALL\">\n {tr(\"conversations.allMessages\", \"All Messages\")}\n </option>\n <option value=\"MENTIONS\">\n {tr(\"conversations.mentionsOnly\", \"Mentions Only\")}\n </option>\n <option value=\"DIRECT\">\n {tr(\"conversations.directOnly\", \"Direct Only\")}\n </option>\n <option value=\"NONE\">\n {tr(\"conversations.none\", \"None\")}\n </option>\n </select>\n </div>\n </div>\n </PageSection>\n\n <PageSection\n title={tr(\"conversations.messageBehavior\", \"Message Behavior\")}\n description={tr(\n \"conversations.messageBehaviorDescription\",\n \"Tune read and typing experience\",\n )}\n >\n <div className=\"space-y-4 rounded-xl border border-border p-4\">\n <ToggleSetting\n icon={<Eye size={16} />}\n label={tr(\n \"conversations.typingIndicators\",\n \"Typing Indicators\",\n )}\n description={tr(\n \"conversations.typingIndicatorsDescription\",\n \"Show others when someone is typing\",\n )}\n checked={showTypingIndicators}\n onChange={setShowTypingIndicators}\n />\n <ToggleSetting\n icon={<Eye size={16} />}\n label={tr(\"conversations.readReceipts\", \"Read Receipts\")}\n description={tr(\n \"conversations.readReceiptsDescription\",\n \"Display read status for conversation participants\",\n )}\n checked={showReadReceipts}\n onChange={setShowReadReceipts}\n />\n </div>\n </PageSection>\n\n <PageSection\n title={tr(\"conversations.exportData\", \"Export Data\")}\n description={tr(\n \"conversations.exportDataDescription\",\n \"Download conversation comments in your preferred format\",\n )}\n >\n <div className=\"space-y-3 rounded-xl border border-border p-4\">\n <div className=\"flex flex-wrap gap-2\">\n {([\"JSON\", \"CSV\", \"MARKDOWN\"] as const).map((format) => (\n <PermissionGuard\n key={format}\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n >\n <button\n type=\"button\"\n disabled={exporting || !conversation}\n onClick={async () => {\n const url = await exportComments(\n \"conversation\",\n conversation!.id,\n format,\n );\n if (url && downloadLinkRef.current) {\n downloadLinkRef.current.href = url;\n downloadLinkRef.current.download = `conversation-${conversation!.id}.${format.toLowerCase()}`;\n downloadLinkRef.current.click();\n }\n }}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors disabled:opacity-50\"\n >\n <Download size={12} />\n {exporting\n ? tr(\"conversations.exporting\", \"Exporting…\")\n : format}\n </button>\n </PermissionGuard>\n ))}\n </div>\n {downloadUrl && (\n <p className=\"text-xs text-status-success-text\">\n {tr(\n \"conversations.exportReady\",\n \"Export ready — downloading…\",\n )}\n </p>\n )}\n {/* Hidden anchor driven programmatically for download trigger */}\n <a ref={downloadLinkRef} className=\"hidden\" aria-hidden=\"true\" />\n </div>\n </PageSection>\n\n {saveError && (\n <div className=\"text-xs text-destructive rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2\">\n {saveError.message}\n </div>\n )}\n {saved && !saveError && (\n <div className=\"text-xs text-status-success-text rounded-lg border border-border-default bg-status-success-bg-subtle px-3 py-2\">\n {tr(\"conversations.settingsSaved\", \"Settings saved.\")}\n </div>\n )}\n </>\n )}\n </PageLayout>\n );\n}\n\nfunction ToggleSetting({\n icon,\n label,\n description,\n checked,\n onChange,\n}: {\n icon: React.ReactNode;\n label: string;\n description: string;\n checked: boolean;\n onChange: (value: boolean) => void;\n}) {\n return (\n <PermissionGuard permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_UPDATE}>\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex items-start gap-3\">\n <div className=\"p-2 rounded-lg bg-muted text-muted-foreground\">\n {icon}\n </div>\n <div>\n <p className=\"text-sm font-medium text-foreground\">{label}</p>\n <p className=\"text-xs text-muted-foreground mt-0.5\">\n {description}\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n aria-label={label}\n onClick={() => onChange(!checked)}\n className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors ${checked ? \"bg-primary\" : \"bg-muted\"}`}\n >\n <span\n className={`pointer-events-none inline-block h-5 w-5 rounded-full bg-background shadow transition ${checked ? \"translate-x-5\" : \"translate-x-0\"}`}\n />\n </button>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ConversationsSettingsPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationsSettingsPage.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { useNavigate } from \"react-router-dom\";\nimport { ArrowLeft, Bell, Download, Eye, Save, Settings } from \"lucide-react\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport {\n useGetConversation,\n useUpdateConversation,\n} from \"../hooks/useConversationQueries\";\nimport { useExportConversation } from \"../hooks/useExportConversation\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { nativeImpact, nativeNotify } from \"../../utils/nativeBridge\";\n\nexport function ConversationsSettingsPage() {\n const navigate = useNavigate();\n const { basePath } = useConversations();\n const { activeConversationId } = useConversationsStore();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const { conversation, loading: conversationLoading } = useGetConversation(\n activeConversationId ?? \"\",\n );\n const {\n updateConversation,\n loading: saving,\n error: saveError,\n } = useUpdateConversation();\n\n const [showTypingIndicators, setShowTypingIndicators] = useState(true);\n const [showReadReceipts, setShowReadReceipts] = useState(true);\n const [defaultNotificationLevel, setDefaultNotificationLevel] =\n useState(\"ALL\");\n const [saved, setSaved] = useState(false);\n const downloadLinkRef = useRef<HTMLAnchorElement>(null);\n const {\n exportComments,\n loading: exporting,\n downloadUrl,\n } = useExportConversation();\n\n useEffect(() => {\n const settings =\n conversation?.settings && typeof conversation.settings === \"object\"\n ? (conversation.settings as Record<string, unknown>)\n : null;\n if (!settings) return;\n if (typeof settings.showTypingIndicators === \"boolean\") {\n setShowTypingIndicators(settings.showTypingIndicators);\n }\n if (typeof settings.showReadReceipts === \"boolean\") {\n setShowReadReceipts(settings.showReadReceipts);\n }\n if (typeof settings.defaultNotificationLevel === \"string\") {\n setDefaultNotificationLevel(settings.defaultNotificationLevel);\n }\n }, [conversation]);\n\n const handleSave = async () => {\n if (!conversation?.id) return;\n nativeImpact(\"light\");\n try {\n const updated = await updateConversation({\n id: conversation.id,\n settings: {\n showTypingIndicators,\n showReadReceipts,\n defaultNotificationLevel,\n },\n });\n const ok = Boolean(updated);\n nativeNotify(ok ? \"success\" : \"error\");\n setSaved(ok);\n } catch (error) {\n nativeNotify(\"error\");\n throw error;\n }\n };\n\n return (\n <PageLayout\n dataTour=\"conversation-settings-page\"\n title={tr(\"conversations.settings\", \"Settings\")}\n subtitle={\n conversation\n ? `${tr(\"conversations.settingsFor\", \"Conversation settings for\")} ${conversation.title}`\n : tr(\n \"conversations.selectConversationForSettings\",\n \"Select a conversation to edit settings\",\n )\n }\n icon={<Settings size={24} />}\n maxWidth=\"lg\"\n headerActions={\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigate(basePath || \"/conversations\")}\n className=\"inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors\"\n >\n <ArrowLeft size={16} />\n {tr(\"conversations.back\", \"Back\")}\n </button>\n <button\n type=\"button\"\n onClick={() => void handleSave()}\n disabled={saving || !conversation}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors shadow-sm disabled:opacity-50\"\n >\n <Save size={16} />\n {saving\n ? tr(\"conversations.saving\", \"Saving...\")\n : tr(\"conversations.saveChanges\", \"Save Changes\")}\n </button>\n </div>\n }\n >\n {conversationLoading ? (\n <div className=\"text-sm text-muted-foreground\">\n {tr(\"conversations.loadingSettings\", \"Loading settings...\")}\n </div>\n ) : !conversation ? (\n <div className=\"text-sm text-muted-foreground\">\n {tr(\n \"conversations.openConversationFirst\",\n \"Open a conversation first, then come back to settings.\",\n )}\n </div>\n ) : (\n <>\n <PageSection\n title={tr(\"conversations.notifications\", \"Notifications\")}\n description={tr(\n \"conversations.notificationsDescription\",\n \"Configure delivery behavior for this conversation\",\n )}\n >\n <div className=\"space-y-4 rounded-xl border border-border p-4\">\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex items-start gap-3\">\n <div className=\"p-2 rounded-lg bg-status-info-bg-subtle text-status-info-text\">\n <Bell size={16} />\n </div>\n <div>\n <p className=\"text-sm font-medium text-foreground\">\n {tr(\n \"conversations.defaultNotificationLevel\",\n \"Default Notification Level\",\n )}\n </p>\n <p className=\"text-xs text-muted-foreground mt-0.5\">\n {tr(\n \"conversations.defaultNotificationLevelHint\",\n \"This updates conversation settings JSON via GraphQL.\",\n )}\n </p>\n </div>\n </div>\n <select\n value={defaultNotificationLevel}\n onChange={(event) =>\n setDefaultNotificationLevel(event.target.value)\n }\n className=\"px-3 py-1.5 text-sm rounded-lg border border-border bg-background focus:outline-none focus:ring-2 focus:ring-ring\"\n >\n <option value=\"ALL\">\n {tr(\"conversations.allMessages\", \"All Messages\")}\n </option>\n <option value=\"MENTIONS\">\n {tr(\"conversations.mentionsOnly\", \"Mentions Only\")}\n </option>\n <option value=\"DIRECT\">\n {tr(\"conversations.directOnly\", \"Direct Only\")}\n </option>\n <option value=\"NONE\">\n {tr(\"conversations.none\", \"None\")}\n </option>\n </select>\n </div>\n </div>\n </PageSection>\n\n <PageSection\n title={tr(\"conversations.messageBehavior\", \"Message Behavior\")}\n description={tr(\n \"conversations.messageBehaviorDescription\",\n \"Tune read and typing experience\",\n )}\n >\n <div className=\"space-y-4 rounded-xl border border-border p-4\">\n <ToggleSetting\n icon={<Eye size={16} />}\n label={tr(\n \"conversations.typingIndicators\",\n \"Typing Indicators\",\n )}\n description={tr(\n \"conversations.typingIndicatorsDescription\",\n \"Show others when someone is typing\",\n )}\n checked={showTypingIndicators}\n onChange={setShowTypingIndicators}\n />\n <ToggleSetting\n icon={<Eye size={16} />}\n label={tr(\"conversations.readReceipts\", \"Read Receipts\")}\n description={tr(\n \"conversations.readReceiptsDescription\",\n \"Display read status for conversation participants\",\n )}\n checked={showReadReceipts}\n onChange={setShowReadReceipts}\n />\n </div>\n </PageSection>\n\n <PageSection\n title={tr(\"conversations.exportData\", \"Export Data\")}\n description={tr(\n \"conversations.exportDataDescription\",\n \"Download conversation comments in your preferred format\",\n )}\n >\n <div className=\"space-y-3 rounded-xl border border-border p-4\">\n <div className=\"flex flex-wrap gap-2\">\n {([\"JSON\", \"CSV\", \"MARKDOWN\"] as const).map((format) => (\n <PermissionGuard\n key={format}\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n >\n <button\n type=\"button\"\n disabled={exporting || !conversation}\n onClick={async () => {\n const url = await exportComments(\n \"conversation\",\n conversation!.id,\n format,\n );\n if (url && downloadLinkRef.current) {\n downloadLinkRef.current.href = url;\n downloadLinkRef.current.download = `conversation-${conversation!.id}.${format.toLowerCase()}`;\n downloadLinkRef.current.click();\n }\n }}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors disabled:opacity-50\"\n >\n <Download size={12} />\n {exporting\n ? tr(\"conversations.exporting\", \"Exporting…\")\n : format}\n </button>\n </PermissionGuard>\n ))}\n </div>\n {downloadUrl && (\n <p className=\"text-xs text-status-success-text\">\n {tr(\n \"conversations.exportReady\",\n \"Export ready — downloading…\",\n )}\n </p>\n )}\n {/* Hidden anchor driven programmatically for download trigger */}\n <a ref={downloadLinkRef} className=\"hidden\" aria-hidden=\"true\" />\n </div>\n </PageSection>\n\n {saveError && (\n <div className=\"text-xs text-destructive rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2\">\n {saveError.message}\n </div>\n )}\n {saved && !saveError && (\n <div className=\"text-xs text-status-success-text rounded-lg border border-border-default bg-status-success-bg-subtle px-3 py-2\">\n {tr(\"conversations.settingsSaved\", \"Settings saved.\")}\n </div>\n )}\n </>\n )}\n </PageLayout>\n );\n}\n\nfunction ToggleSetting({\n icon,\n label,\n description,\n checked,\n onChange,\n}: {\n icon: React.ReactNode;\n label: string;\n description: string;\n checked: boolean;\n onChange: (value: boolean) => void;\n}) {\n return (\n <PermissionGuard permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_UPDATE}>\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex items-start gap-3\">\n <div className=\"p-2 rounded-lg bg-muted text-muted-foreground\">\n {icon}\n </div>\n <div>\n <p className=\"text-sm font-medium text-foreground\">{label}</p>\n <p className=\"text-xs text-muted-foreground mt-0.5\">\n {description}\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n aria-label={label}\n onClick={() => onChange(!checked)}\n className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors ${checked ? \"bg-primary\" : \"bg-muted\"}`}\n >\n <span\n className={`pointer-events-none inline-block h-5 w-5 rounded-full bg-background shadow transition ${checked ? \"translate-x-5\" : \"translate-x-0\"}`}\n />\n </button>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"NewConversationPage.d.ts","sourceRoot":"","sources":["../../../src/conversations/pages/NewConversationPage.tsx"],"names":[],"mappings":"AA6CA,wBAAgB,mBAAmB,4CA8ZlC"}
1
+ {"version":3,"file":"NewConversationPage.d.ts","sourceRoot":"","sources":["../../../src/conversations/pages/NewConversationPage.tsx"],"names":[],"mappings":"AA6CA,wBAAgB,mBAAmB,4CAoalC"}
@@ -130,6 +130,7 @@ function E() {
130
130
  }),
131
131
  children: [
132
132
  /* @__PURE__ */ w("section", {
133
+ "data-tour": "conversation-type-selector",
133
134
  className: "space-y-3",
134
135
  children: [/* @__PURE__ */ C("h3", {
135
136
  className: "text-sm font-semibold text-foreground",
@@ -156,6 +157,7 @@ function E() {
156
157
  })]
157
158
  }),
158
159
  !J && /* @__PURE__ */ w("section", {
160
+ "data-tour": "conversation-visibility-selector",
159
161
  className: "space-y-3",
160
162
  children: [
161
163
  /* @__PURE__ */ C("h3", {
@@ -178,6 +180,7 @@ function E() {
178
180
  ]
179
181
  }),
180
182
  Y && /* @__PURE__ */ w("section", {
183
+ "data-tour": "conversation-create-details",
181
184
  className: "space-y-3",
182
185
  children: [/* @__PURE__ */ w("div", { children: [/* @__PURE__ */ w("label", {
183
186
  htmlFor: "conv-title",
@@ -213,6 +216,7 @@ function E() {
213
216
  })] })]
214
217
  }),
215
218
  /* @__PURE__ */ w("section", {
219
+ "data-tour": "conversation-participants",
216
220
  className: "space-y-3",
217
221
  children: [
218
222
  /* @__PURE__ */ w("h3", {
@@ -1 +1 @@
1
- {"version":3,"file":"NewConversationPage.js","names":[],"sources":["../../../src/conversations/pages/NewConversationPage.tsx"],"sourcesContent":["import { useState, useCallback, useMemo } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport { useNavigate } from \"react-router-dom\";\nimport {\n ArrowLeft,\n Globe,\n Hash,\n Lock,\n Megaphone,\n MessageCircle,\n Plus,\n Shield,\n Users,\n X,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { useCreateConversation } from \"../hooks/useConversationQueries\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport type { ConversationType, ConversationVisibility } from \"../types\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { joinPath } from \"../utils/navigation\";\nimport { nativeImpact, nativeNotify } from \"../../utils/nativeBridge\";\n\n/** Extract the first meaningful GraphQL error message from an ApolloError. */\nfunction getErrorMessage(error: Error): string {\n if (\"graphQLErrors\" in error) {\n const gqlErrors = (error as { graphQLErrors?: Array<{ message?: string }> })\n .graphQLErrors;\n if (gqlErrors && gqlErrors.length > 0 && gqlErrors[0]?.message) {\n return gqlErrors[0].message;\n }\n }\n return error.message;\n}\n\ninterface ConversationTypeOption {\n type: ConversationType;\n label: string;\n description: string;\n icon: React.ReactNode;\n}\n\nexport function NewConversationPage() {\n const navigate = useNavigate();\n const { basePath } = useConversations();\n const { createConversation, loading, error } = useCreateConversation();\n const bus = useEventBus();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const CONVERSATION_TYPES: ConversationTypeOption[] = useMemo(\n () => [\n {\n type: \"DIRECT\" as ConversationType,\n label: tr(\"conversations.directMessage\", \"Direct Message\"),\n description: tr(\n \"conversations.directMessageDescription\",\n \"One-on-one private conversation\",\n ),\n icon: <MessageCircle size={20} />,\n },\n {\n type: \"GROUP\" as ConversationType,\n label: tr(\"conversations.groupChat\", \"Group Chat\"),\n description: tr(\n \"conversations.groupChatDescription\",\n \"Invite multiple people to a group conversation\",\n ),\n icon: <Users size={20} />,\n },\n {\n type: \"CHANNEL\" as ConversationType,\n label: tr(\"conversations.channel\", \"Channel\"),\n description: tr(\n \"conversations.channelDescription\",\n \"A topic-based channel that anyone can join\",\n ),\n icon: <Hash size={20} />,\n },\n {\n type: \"ANNOUNCEMENT\" as ConversationType,\n label: tr(\"conversations.announcement\", \"Announcement\"),\n description: tr(\n \"conversations.announcementDescription\",\n \"Broadcast messages to a large audience\",\n ),\n icon: <Megaphone size={20} />,\n },\n ],\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [t],\n );\n\n const VISIBILITY_OPTIONS: Array<{\n value: ConversationVisibility;\n label: string;\n description: string;\n icon: React.ReactNode;\n }> = useMemo(\n () => [\n {\n value: \"PUBLIC\" as ConversationVisibility,\n label: tr(\"conversations.public\", \"Public\"),\n description: tr(\n \"conversations.publicDescription\",\n \"Anyone in the workspace can discover and join\",\n ),\n icon: <Globe size={16} />,\n },\n {\n value: \"PRIVATE\" as ConversationVisibility,\n label: tr(\"conversations.private\", \"Private\"),\n description: tr(\n \"conversations.privateDescription\",\n \"Only invited members can access\",\n ),\n icon: <Lock size={16} />,\n },\n {\n value: \"PROTECTED\" as ConversationVisibility,\n label: tr(\"conversations.protected\", \"Protected\"),\n description: tr(\n \"conversations.protectedDescription\",\n \"Discoverable but requires approval to join\",\n ),\n icon: <Shield size={16} />,\n },\n ],\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [t],\n );\n\n const [selectedType, setSelectedType] = useState<ConversationType>(\"GROUP\");\n const [title, setTitle] = useState(\"\");\n const [description, setDescription] = useState(\"\");\n const [visibility, setVisibility] =\n useState<ConversationVisibility>(\"PRIVATE\");\n const [participantInput, setParticipantInput] = useState(\"\");\n const [participantIds, setParticipantIds] = useState<string[]>([]);\n\n const isDirectMessage = selectedType === \"DIRECT\";\n const needsTitle = !isDirectMessage;\n\n const addParticipant = useCallback(() => {\n const value = participantInput.trim();\n if (!value || participantIds.includes(value)) return;\n if (isDirectMessage) {\n setParticipantIds([value]);\n } else {\n setParticipantIds((prev) => [...prev, value]);\n }\n setParticipantInput(\"\");\n }, [participantInput, participantIds, isDirectMessage]);\n\n const removeParticipant = useCallback((participantId: string) => {\n setParticipantIds((prev) => prev.filter((id) => id !== participantId));\n }, []);\n\n const requiresParticipants =\n selectedType === \"DIRECT\" || selectedType === \"GROUP\";\n\n const handleCreate = useCallback(async () => {\n if (requiresParticipants && participantIds.length === 0) return;\n if (needsTitle && !title.trim()) return;\n\n nativeImpact(\"light\");\n try {\n const conversation = await createConversation({\n type: selectedType,\n title: needsTitle ? title.trim() : undefined,\n description: description.trim() || undefined,\n participantIds,\n visibility,\n });\n\n if (!conversation) {\n nativeNotify(\"error\");\n return;\n }\n nativeNotify(\"success\");\n bus.emit(\"conversations.conversation.created\", {\n conversationId: conversation.id,\n conversationType: selectedType,\n source: \"new-conversation-page\",\n });\n navigate(joinPath(basePath || \"/conversations\", conversation.id));\n } catch (error) {\n nativeNotify(\"error\");\n throw error;\n }\n }, [\n basePath,\n bus,\n requiresParticipants,\n participantIds,\n needsTitle,\n title,\n createConversation,\n selectedType,\n description,\n visibility,\n navigate,\n ]);\n\n const canCreate =\n (!requiresParticipants || participantIds.length > 0) &&\n (!needsTitle || title.trim().length > 0);\n\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_CREATE}\n allowUnavailablePermissions\n >\n <PageLayout\n title={tr(\"conversations.newConversation\", \"New Conversation\")}\n subtitle={tr(\n \"conversations.newConversationSubtitle\",\n \"Start a new conversation with your team\",\n )}\n icon={<Plus size={24} />}\n maxWidth=\"lg\"\n headerActions={\n <button\n type=\"button\"\n onClick={() => navigate(basePath || \"/conversations\")}\n className=\"inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors\"\n >\n <ArrowLeft size={16} />\n {tr(\"conversations.back\", \"Back\")}\n </button>\n }\n >\n <section className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n {tr(\"conversations.conversationType\", \"Conversation Type\")}\n </h3>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-3\">\n {CONVERSATION_TYPES.map((option) => (\n <button\n key={option.type}\n type=\"button\"\n onClick={() => {\n setSelectedType(option.type);\n if (option.type === \"DIRECT\") {\n setParticipantIds((prev) => prev.slice(0, 1));\n }\n }}\n className={cn(\n \"flex items-start gap-3 p-4 rounded-xl border text-left transition-all duration-150\",\n selectedType === option.type\n ? \"border-primary bg-primary/5 ring-1 ring-primary/20\"\n : \"border-border hover:border-muted-foreground/30 hover:bg-muted/30\",\n )}\n >\n <div\n className={cn(\n \"p-2 rounded-lg\",\n selectedType === option.type\n ? \"bg-primary/10 text-primary\"\n : \"bg-muted text-muted-foreground\",\n )}\n >\n {option.icon}\n </div>\n <div>\n <p className=\"text-sm font-medium text-foreground\">\n {option.label}\n </p>\n <p className=\"text-xs text-muted-foreground mt-0.5\">\n {option.description}\n </p>\n </div>\n </button>\n ))}\n </div>\n </section>\n\n {!isDirectMessage && (\n <section className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n {tr(\"conversations.visibility\", \"Visibility\")}\n </h3>\n <div className=\"flex gap-2\">\n {VISIBILITY_OPTIONS.map((option) => (\n <button\n key={option.value}\n type=\"button\"\n onClick={() => setVisibility(option.value)}\n className={cn(\n \"flex items-center gap-2 px-4 py-2.5 rounded-lg border text-sm transition-all\",\n visibility === option.value\n ? \"border-primary bg-primary/5 text-primary font-medium\"\n : \"border-border text-muted-foreground hover:text-foreground hover:bg-muted/30\",\n )}\n >\n {option.icon}\n {option.label}\n </button>\n ))}\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {\n VISIBILITY_OPTIONS.find((option) => option.value === visibility)\n ?.description\n }\n </p>\n </section>\n )}\n\n {needsTitle && (\n <section className=\"space-y-3\">\n <div>\n <label\n htmlFor=\"conv-title\"\n className=\"text-sm font-semibold text-foreground\"\n >\n {tr(\"conversations.name\", \"Name\")}{\" \"}\n <span className=\"text-destructive\">*</span>\n </label>\n <input\n id=\"conv-title\"\n type=\"text\"\n value={title}\n onChange={(event) => setTitle(event.target.value)}\n placeholder={\n selectedType === \"CHANNEL\"\n ? tr(\n \"conversations.channelNamePlaceholder\",\n \"e.g., engineering-general\",\n )\n : tr(\n \"conversations.conversationNamePlaceholder\",\n \"e.g., Product Launch Q1\",\n )\n }\n maxLength={200}\n className=\"mt-1.5 w-full px-3 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring\"\n />\n </div>\n <div>\n <label\n htmlFor=\"conv-desc\"\n className=\"text-sm font-semibold text-foreground\"\n >\n {tr(\"conversations.description\", \"Description\")}\n </label>\n <textarea\n id=\"conv-desc\"\n value={description}\n onChange={(event) => setDescription(event.target.value)}\n placeholder={tr(\n \"conversations.descriptionPlaceholder\",\n \"What is this conversation about?\",\n )}\n rows={2}\n maxLength={1000}\n className=\"mt-1.5 w-full px-3 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring resize-none\"\n />\n </div>\n </section>\n )}\n\n <section className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n {isDirectMessage\n ? tr(\"conversations.participantId\", \"Participant ID\")\n : tr(\"conversations.participantIds\", \"Participant IDs\")}\n {!requiresParticipants && (\n <span className=\"ml-1.5 text-xs font-normal text-muted-foreground\">\n ({tr(\"conversations.optional\", \"optional\")})\n </span>\n )}\n </h3>\n\n {participantIds.length > 0 && (\n <div className=\"flex flex-wrap gap-2\">\n {participantIds.map((participantId) => (\n <span\n key={participantId}\n className=\"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-primary/10 text-primary text-xs font-medium\"\n >\n {participantId}\n <button\n type=\"button\"\n onClick={() => removeParticipant(participantId)}\n className=\"p-0.5 rounded-full hover:bg-primary/20\"\n >\n <X size={12} />\n </button>\n </span>\n ))}\n </div>\n )}\n\n <div className=\"flex items-center gap-2\">\n <input\n type=\"text\"\n value={participantInput}\n onChange={(event) => setParticipantInput(event.target.value)}\n onKeyDown={(event) => {\n if (event.key !== \"Enter\") return;\n event.preventDefault();\n addParticipant();\n }}\n placeholder={tr(\n \"conversations.participantInputPlaceholder\",\n \"Enter user ID and press Enter\",\n )}\n className=\"w-full px-3 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring\"\n />\n <button\n type=\"button\"\n onClick={addParticipant}\n className=\"px-3 py-2 text-sm rounded-lg border border-border hover:bg-muted/50\"\n >\n {tr(\"conversations.add\", \"Add\")}\n </button>\n </div>\n </section>\n\n {error && (\n <div className=\"text-xs text-destructive rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2\">\n {getErrorMessage(error)}\n </div>\n )}\n\n <div className=\"flex justify-end gap-3 pt-4 border-t border-border\">\n <button\n type=\"button\"\n onClick={() => navigate(\"..\")}\n className=\"px-4 py-2 text-sm font-medium rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors\"\n >\n {tr(\"conversations.cancel\", \"Cancel\")}\n </button>\n <button\n type=\"button\"\n onClick={() => void handleCreate()}\n disabled={!canCreate || loading}\n className={cn(\n \"inline-flex items-center gap-2 px-5 py-2 text-sm font-medium rounded-lg transition-all\",\n canCreate && !loading\n ? \"bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm\"\n : \"bg-muted text-muted-foreground cursor-not-allowed\",\n )}\n >\n {loading\n ? tr(\"conversations.creating\", \"Creating...\")\n : tr(\"conversations.createConversation\", \"Create Conversation\")}\n </button>\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"NewConversationPage.js","names":[],"sources":["../../../src/conversations/pages/NewConversationPage.tsx"],"sourcesContent":["import { useState, useCallback, useMemo } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport { useNavigate } from \"react-router-dom\";\nimport {\n ArrowLeft,\n Globe,\n Hash,\n Lock,\n Megaphone,\n MessageCircle,\n Plus,\n Shield,\n Users,\n X,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { useCreateConversation } from \"../hooks/useConversationQueries\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport type { ConversationType, ConversationVisibility } from \"../types\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { joinPath } from \"../utils/navigation\";\nimport { nativeImpact, nativeNotify } from \"../../utils/nativeBridge\";\n\n/** Extract the first meaningful GraphQL error message from an ApolloError. */\nfunction getErrorMessage(error: Error): string {\n if (\"graphQLErrors\" in error) {\n const gqlErrors = (error as { graphQLErrors?: Array<{ message?: string }> })\n .graphQLErrors;\n if (gqlErrors && gqlErrors.length > 0 && gqlErrors[0]?.message) {\n return gqlErrors[0].message;\n }\n }\n return error.message;\n}\n\ninterface ConversationTypeOption {\n type: ConversationType;\n label: string;\n description: string;\n icon: React.ReactNode;\n}\n\nexport function NewConversationPage() {\n const navigate = useNavigate();\n const { basePath } = useConversations();\n const { createConversation, loading, error } = useCreateConversation();\n const bus = useEventBus();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const CONVERSATION_TYPES: ConversationTypeOption[] = useMemo(\n () => [\n {\n type: \"DIRECT\" as ConversationType,\n label: tr(\"conversations.directMessage\", \"Direct Message\"),\n description: tr(\n \"conversations.directMessageDescription\",\n \"One-on-one private conversation\",\n ),\n icon: <MessageCircle size={20} />,\n },\n {\n type: \"GROUP\" as ConversationType,\n label: tr(\"conversations.groupChat\", \"Group Chat\"),\n description: tr(\n \"conversations.groupChatDescription\",\n \"Invite multiple people to a group conversation\",\n ),\n icon: <Users size={20} />,\n },\n {\n type: \"CHANNEL\" as ConversationType,\n label: tr(\"conversations.channel\", \"Channel\"),\n description: tr(\n \"conversations.channelDescription\",\n \"A topic-based channel that anyone can join\",\n ),\n icon: <Hash size={20} />,\n },\n {\n type: \"ANNOUNCEMENT\" as ConversationType,\n label: tr(\"conversations.announcement\", \"Announcement\"),\n description: tr(\n \"conversations.announcementDescription\",\n \"Broadcast messages to a large audience\",\n ),\n icon: <Megaphone size={20} />,\n },\n ],\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [t],\n );\n\n const VISIBILITY_OPTIONS: Array<{\n value: ConversationVisibility;\n label: string;\n description: string;\n icon: React.ReactNode;\n }> = useMemo(\n () => [\n {\n value: \"PUBLIC\" as ConversationVisibility,\n label: tr(\"conversations.public\", \"Public\"),\n description: tr(\n \"conversations.publicDescription\",\n \"Anyone in the workspace can discover and join\",\n ),\n icon: <Globe size={16} />,\n },\n {\n value: \"PRIVATE\" as ConversationVisibility,\n label: tr(\"conversations.private\", \"Private\"),\n description: tr(\n \"conversations.privateDescription\",\n \"Only invited members can access\",\n ),\n icon: <Lock size={16} />,\n },\n {\n value: \"PROTECTED\" as ConversationVisibility,\n label: tr(\"conversations.protected\", \"Protected\"),\n description: tr(\n \"conversations.protectedDescription\",\n \"Discoverable but requires approval to join\",\n ),\n icon: <Shield size={16} />,\n },\n ],\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [t],\n );\n\n const [selectedType, setSelectedType] = useState<ConversationType>(\"GROUP\");\n const [title, setTitle] = useState(\"\");\n const [description, setDescription] = useState(\"\");\n const [visibility, setVisibility] =\n useState<ConversationVisibility>(\"PRIVATE\");\n const [participantInput, setParticipantInput] = useState(\"\");\n const [participantIds, setParticipantIds] = useState<string[]>([]);\n\n const isDirectMessage = selectedType === \"DIRECT\";\n const needsTitle = !isDirectMessage;\n\n const addParticipant = useCallback(() => {\n const value = participantInput.trim();\n if (!value || participantIds.includes(value)) return;\n if (isDirectMessage) {\n setParticipantIds([value]);\n } else {\n setParticipantIds((prev) => [...prev, value]);\n }\n setParticipantInput(\"\");\n }, [participantInput, participantIds, isDirectMessage]);\n\n const removeParticipant = useCallback((participantId: string) => {\n setParticipantIds((prev) => prev.filter((id) => id !== participantId));\n }, []);\n\n const requiresParticipants =\n selectedType === \"DIRECT\" || selectedType === \"GROUP\";\n\n const handleCreate = useCallback(async () => {\n if (requiresParticipants && participantIds.length === 0) return;\n if (needsTitle && !title.trim()) return;\n\n nativeImpact(\"light\");\n try {\n const conversation = await createConversation({\n type: selectedType,\n title: needsTitle ? title.trim() : undefined,\n description: description.trim() || undefined,\n participantIds,\n visibility,\n });\n\n if (!conversation) {\n nativeNotify(\"error\");\n return;\n }\n nativeNotify(\"success\");\n bus.emit(\"conversations.conversation.created\", {\n conversationId: conversation.id,\n conversationType: selectedType,\n source: \"new-conversation-page\",\n });\n navigate(joinPath(basePath || \"/conversations\", conversation.id));\n } catch (error) {\n nativeNotify(\"error\");\n throw error;\n }\n }, [\n basePath,\n bus,\n requiresParticipants,\n participantIds,\n needsTitle,\n title,\n createConversation,\n selectedType,\n description,\n visibility,\n navigate,\n ]);\n\n const canCreate =\n (!requiresParticipants || participantIds.length > 0) &&\n (!needsTitle || title.trim().length > 0);\n\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_CREATE}\n allowUnavailablePermissions\n >\n <PageLayout\n title={tr(\"conversations.newConversation\", \"New Conversation\")}\n subtitle={tr(\n \"conversations.newConversationSubtitle\",\n \"Start a new conversation with your team\",\n )}\n icon={<Plus size={24} />}\n maxWidth=\"lg\"\n headerActions={\n <button\n type=\"button\"\n onClick={() => navigate(basePath || \"/conversations\")}\n className=\"inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors\"\n >\n <ArrowLeft size={16} />\n {tr(\"conversations.back\", \"Back\")}\n </button>\n }\n >\n <section data-tour=\"conversation-type-selector\" className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n {tr(\"conversations.conversationType\", \"Conversation Type\")}\n </h3>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-3\">\n {CONVERSATION_TYPES.map((option) => (\n <button\n key={option.type}\n type=\"button\"\n onClick={() => {\n setSelectedType(option.type);\n if (option.type === \"DIRECT\") {\n setParticipantIds((prev) => prev.slice(0, 1));\n }\n }}\n className={cn(\n \"flex items-start gap-3 p-4 rounded-xl border text-left transition-all duration-150\",\n selectedType === option.type\n ? \"border-primary bg-primary/5 ring-1 ring-primary/20\"\n : \"border-border hover:border-muted-foreground/30 hover:bg-muted/30\",\n )}\n >\n <div\n className={cn(\n \"p-2 rounded-lg\",\n selectedType === option.type\n ? \"bg-primary/10 text-primary\"\n : \"bg-muted text-muted-foreground\",\n )}\n >\n {option.icon}\n </div>\n <div>\n <p className=\"text-sm font-medium text-foreground\">\n {option.label}\n </p>\n <p className=\"text-xs text-muted-foreground mt-0.5\">\n {option.description}\n </p>\n </div>\n </button>\n ))}\n </div>\n </section>\n\n {!isDirectMessage && (\n <section\n data-tour=\"conversation-visibility-selector\"\n className=\"space-y-3\"\n >\n <h3 className=\"text-sm font-semibold text-foreground\">\n {tr(\"conversations.visibility\", \"Visibility\")}\n </h3>\n <div className=\"flex gap-2\">\n {VISIBILITY_OPTIONS.map((option) => (\n <button\n key={option.value}\n type=\"button\"\n onClick={() => setVisibility(option.value)}\n className={cn(\n \"flex items-center gap-2 px-4 py-2.5 rounded-lg border text-sm transition-all\",\n visibility === option.value\n ? \"border-primary bg-primary/5 text-primary font-medium\"\n : \"border-border text-muted-foreground hover:text-foreground hover:bg-muted/30\",\n )}\n >\n {option.icon}\n {option.label}\n </button>\n ))}\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {\n VISIBILITY_OPTIONS.find((option) => option.value === visibility)\n ?.description\n }\n </p>\n </section>\n )}\n\n {needsTitle && (\n <section\n data-tour=\"conversation-create-details\"\n className=\"space-y-3\"\n >\n <div>\n <label\n htmlFor=\"conv-title\"\n className=\"text-sm font-semibold text-foreground\"\n >\n {tr(\"conversations.name\", \"Name\")}{\" \"}\n <span className=\"text-destructive\">*</span>\n </label>\n <input\n id=\"conv-title\"\n type=\"text\"\n value={title}\n onChange={(event) => setTitle(event.target.value)}\n placeholder={\n selectedType === \"CHANNEL\"\n ? tr(\n \"conversations.channelNamePlaceholder\",\n \"e.g., engineering-general\",\n )\n : tr(\n \"conversations.conversationNamePlaceholder\",\n \"e.g., Product Launch Q1\",\n )\n }\n maxLength={200}\n className=\"mt-1.5 w-full px-3 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring\"\n />\n </div>\n <div>\n <label\n htmlFor=\"conv-desc\"\n className=\"text-sm font-semibold text-foreground\"\n >\n {tr(\"conversations.description\", \"Description\")}\n </label>\n <textarea\n id=\"conv-desc\"\n value={description}\n onChange={(event) => setDescription(event.target.value)}\n placeholder={tr(\n \"conversations.descriptionPlaceholder\",\n \"What is this conversation about?\",\n )}\n rows={2}\n maxLength={1000}\n className=\"mt-1.5 w-full px-3 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring resize-none\"\n />\n </div>\n </section>\n )}\n\n <section data-tour=\"conversation-participants\" className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n {isDirectMessage\n ? tr(\"conversations.participantId\", \"Participant ID\")\n : tr(\"conversations.participantIds\", \"Participant IDs\")}\n {!requiresParticipants && (\n <span className=\"ml-1.5 text-xs font-normal text-muted-foreground\">\n ({tr(\"conversations.optional\", \"optional\")})\n </span>\n )}\n </h3>\n\n {participantIds.length > 0 && (\n <div className=\"flex flex-wrap gap-2\">\n {participantIds.map((participantId) => (\n <span\n key={participantId}\n className=\"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-primary/10 text-primary text-xs font-medium\"\n >\n {participantId}\n <button\n type=\"button\"\n onClick={() => removeParticipant(participantId)}\n className=\"p-0.5 rounded-full hover:bg-primary/20\"\n >\n <X size={12} />\n </button>\n </span>\n ))}\n </div>\n )}\n\n <div className=\"flex items-center gap-2\">\n <input\n type=\"text\"\n value={participantInput}\n onChange={(event) => setParticipantInput(event.target.value)}\n onKeyDown={(event) => {\n if (event.key !== \"Enter\") return;\n event.preventDefault();\n addParticipant();\n }}\n placeholder={tr(\n \"conversations.participantInputPlaceholder\",\n \"Enter user ID and press Enter\",\n )}\n className=\"w-full px-3 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring\"\n />\n <button\n type=\"button\"\n onClick={addParticipant}\n className=\"px-3 py-2 text-sm rounded-lg border border-border hover:bg-muted/50\"\n >\n {tr(\"conversations.add\", \"Add\")}\n </button>\n </div>\n </section>\n\n {error && (\n <div className=\"text-xs text-destructive rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2\">\n {getErrorMessage(error)}\n </div>\n )}\n\n <div className=\"flex justify-end gap-3 pt-4 border-t border-border\">\n <button\n type=\"button\"\n onClick={() => navigate(\"..\")}\n className=\"px-4 py-2 text-sm font-medium rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors\"\n >\n {tr(\"conversations.cancel\", \"Cancel\")}\n </button>\n <button\n type=\"button\"\n onClick={() => void handleCreate()}\n disabled={!canCreate || loading}\n className={cn(\n \"inline-flex items-center gap-2 px-5 py-2 text-sm font-medium rounded-lg transition-all\",\n canCreate && !loading\n ? \"bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm\"\n : \"bg-muted text-muted-foreground cursor-not-allowed\",\n )}\n >\n {loading\n ? tr(\"conversations.creating\", \"Creating...\")\n : tr(\"conversations.createConversation\", \"Create Conversation\")}\n </button>\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AA2BA,SAAS,GAAgB,GAAsB;AAC7C,KAAI,mBAAmB,GAAO;EAC5B,IAAM,IAAa,EAChB;AACH,MAAI,KAAa,EAAU,SAAS,KAAK,EAAU,IAAI,QACrD,QAAO,EAAU,GAAG;;AAGxB,QAAO,EAAM;;AAUf,SAAgB,IAAsB;CACpC,IAAM,IAAW,GAAa,EACxB,EAAE,gBAAa,GAAkB,EACjC,EAAE,uBAAoB,YAAS,aAAU,IAAuB,EAChE,IAAM,GAAa,EACnB,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;IAG9B,IAA+C,QAC7C;EACJ;GACE,MAAM;GACN,OAAO,EAAG,+BAA+B,iBAAiB;GAC1D,aAAa,EACX,0CACA,kCACD;GACD,MAAM,kBAAC,GAAD,EAAe,MAAM,IAAM,CAAA;GAClC;EACD;GACE,MAAM;GACN,OAAO,EAAG,2BAA2B,aAAa;GAClD,aAAa,EACX,sCACA,iDACD;GACD,MAAM,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA;GAC1B;EACD;GACE,MAAM;GACN,OAAO,EAAG,yBAAyB,UAAU;GAC7C,aAAa,EACX,oCACA,6CACD;GACD,MAAM,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA;GACzB;EACD;GACE,MAAM;GACN,OAAO,EAAG,8BAA8B,eAAe;GACvD,aAAa,EACX,yCACA,yCACD;GACD,MAAM,kBAAC,GAAD,EAAW,MAAM,IAAM,CAAA;GAC9B;EACF,EAED,CAAC,EAAE,CACJ,EAEK,IAKD,QACG;EACJ;GACE,OAAO;GACP,OAAO,EAAG,wBAAwB,SAAS;GAC3C,aAAa,EACX,mCACA,gDACD;GACD,MAAM,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA;GAC1B;EACD;GACE,OAAO;GACP,OAAO,EAAG,yBAAyB,UAAU;GAC7C,aAAa,EACX,oCACA,kCACD;GACD,MAAM,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA;GACzB;EACD;GACE,OAAO;GACP,OAAO,EAAG,2BAA2B,YAAY;GACjD,aAAa,EACX,sCACA,6CACD;GACD,MAAM,kBAAC,GAAD,EAAQ,MAAM,IAAM,CAAA;GAC3B;EACF,EAED,CAAC,EAAE,CACJ,EAEK,CAAC,GAAc,KAAmB,EAA2B,QAAQ,EACrE,CAAC,GAAO,KAAY,EAAS,GAAG,EAChC,CAAC,GAAa,KAAkB,EAAS,GAAG,EAC5C,CAAC,GAAY,KACjB,EAAiC,UAAU,EACvC,CAAC,GAAkB,KAAuB,EAAS,GAAG,EACtD,CAAC,GAAgB,KAAqB,EAAmB,EAAE,CAAC,EAE5D,IAAkB,MAAiB,UACnC,IAAa,CAAC,GAEd,IAAiB,QAAkB;EACvC,IAAM,IAAQ,EAAiB,MAAM;AACjC,GAAC,KAAS,EAAe,SAAS,EAAM,KAE1C,EADE,IACgB,CAAC,EAAM,IAEN,MAAS,CAAC,GAAG,GAAM,EAAM,CAAC,EAE/C,EAAoB,GAAG;IACtB;EAAC;EAAkB;EAAgB;EAAgB,CAAC,EAEjD,IAAoB,GAAa,MAA0B;AAC/D,KAAmB,MAAS,EAAK,QAAQ,MAAO,MAAO,EAAc,CAAC;IACrE,EAAE,CAAC,EAEA,IACJ,MAAiB,YAAY,MAAiB,SAE1C,KAAe,EAAY,YAAY;AACvC,aAAwB,EAAe,WAAW,MAClD,OAAc,CAAC,EAAM,MAAM,GAE/B;KAAa,QAAQ;AACrB,OAAI;IACF,IAAM,IAAe,MAAM,EAAmB;KAC5C,MAAM;KACN,OAAO,IAAa,EAAM,MAAM,GAAG,KAAA;KACnC,aAAa,EAAY,MAAM,IAAI,KAAA;KACnC;KACA;KACD,CAAC;AAEF,QAAI,CAAC,GAAc;AACjB,OAAa,QAAQ;AACrB;;AAQF,IANA,EAAa,UAAU,EACvB,EAAI,KAAK,sCAAsC;KAC7C,gBAAgB,EAAa;KAC7B,kBAAkB;KAClB,QAAQ;KACT,CAAC,EACF,EAAS,EAAS,KAAY,kBAAkB,EAAa,GAAG,CAAC;YAC1D,GAAO;AAEd,UADA,EAAa,QAAQ,EACf;;;IAEP;EACD;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,EAEI,KACH,CAAC,KAAwB,EAAe,SAAS,OACjD,CAAC,KAAc,EAAM,MAAM,CAAC,SAAS;AAExC,QACE,kBAAC,GAAD;EACE,YAAY,EAA0B;EACtC,6BAAA;YAEA,kBAAC,GAAD;GACE,OAAO,EAAG,iCAAiC,mBAAmB;GAC9D,UAAU,EACR,yCACA,0CACD;GACD,MAAM,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA;GACxB,UAAS;GACT,eACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAS,KAAY,iBAAiB;IACrD,WAAU;cAHZ,CAKE,kBAAC,GAAD,EAAW,MAAM,IAAM,CAAA,EACtB,EAAG,sBAAsB,OAAO,CAC1B;;aAhBb;IAmBE,kBAAC,WAAD;KAAS,aAAU;KAA6B,WAAU;eAA1D,CACE,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAG,kCAAkC,oBAAoB;MACvD,CAAA,EACL,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAmB,KAAK,MACvB,kBAAC,UAAD;OAEE,MAAK;OACL,eAAe;AAEb,QADA,EAAgB,EAAO,KAAK,EACxB,EAAO,SAAS,YAClB,GAAmB,MAAS,EAAK,MAAM,GAAG,EAAE,CAAC;;OAGjD,WAAW,EACT,sFACA,MAAiB,EAAO,OACpB,uDACA,mEACL;iBAdH,CAgBE,kBAAC,OAAD;QACE,WAAW,EACT,kBACA,MAAiB,EAAO,OACpB,+BACA,iCACL;kBAEA,EAAO;QACJ,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAO;QACN,CAAA,EACJ,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAO;QACN,CAAA,CACA,EAAA,CAAA,CACC;SAjCF,EAAO,KAiCL,CACT;MACE,CAAA,CACE;;IAET,CAAC,KACA,kBAAC,WAAD;KACE,aAAU;KACV,WAAU;eAFZ;MAIE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAG,4BAA4B,aAAa;OAC1C,CAAA;MACL,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAmB,KAAK,MACvB,kBAAC,UAAD;QAEE,MAAK;QACL,eAAe,EAAc,EAAO,MAAM;QAC1C,WAAW,EACT,gFACA,MAAe,EAAO,QAClB,yDACA,8EACL;kBATH,CAWG,EAAO,MACP,EAAO,MACD;UAZF,EAAO,MAYL,CACT;OACE,CAAA;MACN,kBAAC,KAAD;OAAG,WAAU;iBAET,EAAmB,MAAM,MAAW,EAAO,UAAU,EAAW,EAC5D;OAEJ,CAAA;MACI;;IAGX,KACC,kBAAC,WAAD;KACE,aAAU;KACV,WAAU;eAFZ,CAIE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MACE,SAAQ;MACR,WAAU;gBAFZ;OAIG,EAAG,sBAAsB,OAAO;OAAE;OACnC,kBAAC,QAAD;QAAM,WAAU;kBAAmB;QAAQ,CAAA;OACrC;SACR,kBAAC,SAAD;MACE,IAAG;MACH,MAAK;MACL,OAAO;MACP,WAAW,MAAU,EAAS,EAAM,OAAO,MAAM;MACjD,aACE,MAAiB,YACb,EACE,wCACA,4BACD,GACD,EACE,6CACA,0BACD;MAEP,WAAW;MACX,WAAU;MACV,CAAA,CACE,EAAA,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MACE,SAAQ;MACR,WAAU;gBAET,EAAG,6BAA6B,cAAc;MACzC,CAAA,EACR,kBAAC,YAAD;MACE,IAAG;MACH,OAAO;MACP,WAAW,MAAU,EAAe,EAAM,OAAO,MAAM;MACvD,aAAa,EACX,wCACA,mCACD;MACD,MAAM;MACN,WAAW;MACX,WAAU;MACV,CAAA,CACE,EAAA,CAAA,CACE;;IAGZ,kBAAC,WAAD;KAAS,aAAU;KAA4B,WAAU;eAAzD;MACE,kBAAC,MAAD;OAAI,WAAU;iBAAd,CACG,IACG,EAAG,+BAA+B,iBAAiB,GACnD,EAAG,gCAAgC,kBAAkB,EACxD,CAAC,KACA,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SAAmE;SAC/D,EAAG,0BAA0B,WAAW;SAAC;SACtC;UAEN;;MAEJ,EAAe,SAAS,KACvB,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAe,KAAK,MACnB,kBAAC,QAAD;QAEE,WAAU;kBAFZ,CAIG,GACD,kBAAC,UAAD;SACE,MAAK;SACL,eAAe,EAAkB,EAAc;SAC/C,WAAU;mBAEV,kBAAC,GAAD,EAAG,MAAM,IAAM,CAAA;SACR,CAAA,CACJ;UAXA,EAWA,CACP;OACE,CAAA;MAGR,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,SAAD;QACE,MAAK;QACL,OAAO;QACP,WAAW,MAAU,EAAoB,EAAM,OAAO,MAAM;QAC5D,YAAY,MAAU;AAChB,WAAM,QAAQ,YAClB,EAAM,gBAAgB,EACtB,GAAgB;;QAElB,aAAa,EACX,6CACA,gCACD;QACD,WAAU;QACV,CAAA,EACF,kBAAC,UAAD;QACE,MAAK;QACL,SAAS;QACT,WAAU;kBAET,EAAG,qBAAqB,MAAM;QACxB,CAAA,CACL;;MACE;;IAET,KACC,kBAAC,OAAD;KAAK,WAAU;eACZ,GAAgB,EAAM;KACnB,CAAA;IAGR,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAS,KAAK;MAC7B,WAAU;gBAET,EAAG,wBAAwB,SAAS;MAC9B,CAAA,EACT,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,KAAK,IAAc;MAClC,UAAU,CAAC,KAAa;MACxB,WAAW,EACT,0FACA,KAAa,CAAC,IACV,qEACA,oDACL;gBAEA,IACG,EAAG,0BAA0B,cAAc,GAC3C,EAAG,oCAAoC,sBAAsB;MAC1D,CAAA,CACL;;IACK;;EACG,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationsProvider.d.ts","sourceRoot":"","sources":["../../../src/conversations/providers/ConversationsProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAmBnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAoDvD;;GAEG;AACH,eAAO,MAAM,gBAAgB,8BAQ5B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,EAAE,CACpC,iBAAiB,CAAC,sBAAsB,CAAC,CAqL1C,CAAC"}
1
+ {"version":3,"file":"ConversationsProvider.d.ts","sourceRoot":"","sources":["../../../src/conversations/providers/ConversationsProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAmBnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAoDvD;;GAEG;AACH,eAAO,MAAM,gBAAgB,8BAQ5B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,EAAE,CACpC,iBAAiB,CAAC,sBAAsB,CAAC,CAmM1C,CAAC"}
@@ -41,11 +41,12 @@ var w = e(null), T = () => {
41
41
  variant: N
42
42
  }, F = r(!1), I = r(k);
43
43
  I.current = k;
44
- let L = n(() => new a({ defaultOptions: { queries: {
44
+ let L = !!t, R = n(() => new a({ defaultOptions: { queries: {
45
45
  retry: 1,
46
46
  refetchOnWindowFocus: !1,
47
47
  staleTime: 300 * 1e3
48
- } } }), []), R = n(() => {
48
+ } } }), []), z = n(() => {
49
+ if (L) return null;
49
50
  let e = typeof window < "u" && (window.location.hostname === "localhost" || window.location.hostname.endsWith(".local.burdenoff.com")), t = e ? `${window.location.origin}/__local/conversations/graphql` : E ? `${E}/workspaces/graphql` : "/workspaces/graphql", n = {};
50
51
  D && (n.Authorization = `Bearer ${D}`), O && (n["X-Workspace-Authorization"] = `Bearer ${O}`), T && (n["x-workspace-id"] = T), e && v?.id && (n["x-actor-id"] = v.id, n["x-actor-type"] = "user");
51
52
  let r = new c({
@@ -85,23 +86,24 @@ var w = e(null), T = () => {
85
86
  E,
86
87
  D,
87
88
  v?.id,
89
+ L,
88
90
  T,
89
91
  O
90
- ]), z = !!t, B = /* @__PURE__ */ b(o, {
91
- client: L,
92
+ ]), B = /* @__PURE__ */ b(o, {
93
+ client: R,
92
94
  children: /* @__PURE__ */ b(w.Provider, {
93
95
  value: P,
94
- children: z ? /* @__PURE__ */ x(h, {
96
+ children: L ? /* @__PURE__ */ x(h, {
95
97
  scopeId: T ?? null,
96
98
  gateway: "workspace",
97
99
  children: [/* @__PURE__ */ b(S, {}), e]
98
100
  }) : /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b(S, {}), e] })
99
101
  })
100
102
  });
101
- return z ? B : /* @__PURE__ */ b(p, {
102
- client: R,
103
+ return L ? B : z ? /* @__PURE__ */ b(p, {
104
+ client: z,
103
105
  children: /* @__PURE__ */ b(i, { children: B })
104
- });
106
+ }) : null;
105
107
  };
106
108
  //#endregion
107
109
  export { E as ConversationsProvider, T as useConversations };