@burdenoff/microfe-conversations 2026.527.1 → 2026.527.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.
- package/dist/conversations/components/LazyPageBoundary.js.map +1 -1
- package/dist/conversations/components/lazyRetry.js.map +1 -1
- package/dist/conversations/hooks/useMessageQueries.js.map +1 -1
- package/dist/conversations/pages/ConversationAdminPages.js.map +1 -1
- package/dist/conversations/pages/ConversationListPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LazyPageBoundary.js","names":[],"sources":["../../../src/conversations/components/LazyPageBoundary.tsx"],"sourcesContent":["import {\n Component,\n Suspense,\n type ErrorInfo,\n type ReactNode,\n} from \"react\";\nimport { Loader2, AlertTriangle, RefreshCw } from \"lucide-react\";\nimport { useTr } from \"../utils/i18n\";\nimport { isChunkLoadError } from \"./lazyRetry\";\n\n/**\n * Fallback spinner rendered while a lazy-loaded page chunk is resolving.\n */\nfunction PageLoader() {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <Loader2 className=\"size-8 animate-spin text-primary\" />\n </div>\n );\n}\n\nfunction LazyErrorUI({\n errorMessage,\n onRetry,\n}: {\n errorMessage: string | null;\n onRetry: () => void;\n}) {\n const tr = useTr();\n\n return (\n <div className=\"flex flex-col items-center justify-center min-h-[400px] gap-4 text-center p-6\">\n <div className=\"p-3 rounded-full bg-destructive/10\">\n <AlertTriangle className=\"size-8 text-destructive\" />\n </div>\n <div className=\"space-y-2\">\n <h3 className=\"text-lg font-semibold text-foreground\">\n {tr(\"conversations.failedToLoadPage\", \"Failed to load page\")}\n </h3>\n <p className=\"text-sm text-muted-foreground max-w-md\">\n {tr(\n \"conversations.pageChunkLoadFailed\",\n \"The page chunk could not be loaded. This may be due to a network issue.\",\n )}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors\"\n >\n <RefreshCw className=\"size-4\" />\n {tr(\"conversations.tryAgain\", \"Try Again\")}\n </button>\n {errorMessage && (\n <details className=\"mt-2 max-w-md w-full text-left\">\n <summary className=\"cursor-pointer text-xs text-muted-foreground hover:text-foreground select-none\">\n {tr(\"conversations.errorDetails\", \"Error details\")}\n </summary>\n <pre className=\"mt-2 p-2 rounded bg-muted text-[11px] text-muted-foreground whitespace-pre-wrap break-all\">\n {errorMessage}\n </pre>\n </details>\n )}\n </div>\n );\n}\n\ntype LazyErrorBoundaryProps = {\n source?: string;\n children: (retryKey: number) => ReactNode;\n};\n\ntype LazyErrorBoundaryState = {\n hasError: boolean;\n error: Error | null;\n retryKey: number;\n};\n\n/**\n * Session-scoped flag used to prevent a reload loop when a chunk-load\n * failure is persistent (broken deploy, CDN outage, missing hash file).\n * First chunk-load retry triggers a page reload and sets the flag; if\n * the page mounts again with the same error, `handleRetry` sees the\n * flag and skips the second reload so the user stays on the error UI\n * and can take a different action instead of looping indefinitely.\n */\nconst RELOAD_FLAG_PREFIX = \"conversations-lazyreload-\";\n\nfunction reloadFlagKey(source?: string): string {\n return `${RELOAD_FLAG_PREFIX}${source ?? \"default\"}`;\n}\n\nfunction readSessionFlag(key: string): boolean {\n try {\n if (typeof sessionStorage === \"undefined\") return false;\n return sessionStorage.getItem(key) !== null;\n } catch {\n return false;\n }\n}\n\nfunction setSessionFlag(key: string): void {\n try {\n if (typeof sessionStorage === \"undefined\") return;\n sessionStorage.setItem(key, \"1\");\n } catch {\n // sessionStorage access can throw in some privacy / sandboxed modes\n }\n}\n\nfunction clearSessionFlag(key: string): void {\n try {\n if (typeof sessionStorage === \"undefined\") return;\n sessionStorage.removeItem(key);\n } catch {\n // ignore\n }\n}\n\n/**\n * Error boundary for lazy-loaded page chunks.\n *\n * On retry it branches by error type:\n * - **Chunk-load failures** (`ChunkLoadError`, \"Loading chunk X failed\",\n * \"Failed to fetch dynamically imported module\", etc.) — the rejected\n * promise is cached on the `LazyExoticComponent` itself; the only\n * reliable recovery is a full page reload, which re-creates all lazy\n * refs from scratch.\n * - **Other render errors** — clear the error state and bump an internal\n * `retryKey`; the enclosing `LazyPageBoundary` keys its `<Suspense>`\n * on that counter so the subtree unmounts and remounts. This resets\n * component state and helps recover from transient render issues.\n *\n * Accepts an optional `source` label used in the console tag so user-\n * route and admin-route chunks can be distinguished in telemetry.\n */\nclass LazyErrorBoundary extends Component<\n LazyErrorBoundaryProps,\n LazyErrorBoundaryState\n> {\n state: LazyErrorBoundaryState = {\n hasError: false,\n error: null,\n retryKey: 0,\n };\n\n static getDerivedStateFromError(error: Error) {\n return { hasError: true, error };\n }\n\n componentDidMount() {\n // Subtree mounted without catching an error — the previous session\n // state (if any) is stale. Clear the reload-guard flag so a future\n // chunk failure can still trigger one reload before giving up.\n if (!this.state.hasError) {\n clearSessionFlag(reloadFlagKey(this.props.source));\n }\n }\n\n componentDidUpdate(\n _prevProps: LazyErrorBoundaryProps,\n prevState: LazyErrorBoundaryState,\n ) {\n // Error → recovered (state reset / retryKey bump → clean render)\n // also invalidates the reload-guard flag.\n if (prevState.hasError && !this.state.hasError) {\n clearSessionFlag(reloadFlagKey(this.props.source));\n }\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n const tag = this.props.source\n ? `[Conversations:${this.props.source}]`\n : \"[Conversations]\";\n // Log the raw error details (including the message) to the console\n // only — user-facing copy is always a localised fallback so that\n // bundler-internal strings like \"ChunkLoadError: Loading chunk 42 failed\"\n // never surface to end users.\n console.error(`${tag} Lazy page load failed:`, error, errorInfo);\n }\n\n handleRetry = () => {\n if (isChunkLoadError(this.state.error)) {\n // React.lazy caches rejected promises on the LazyExoticComponent\n // itself — keying Suspense or resetting boundary state does NOT\n // clear it. A full reload is the only reliable recovery.\n //\n // Guard against a reload loop: if the flag is already set we\n // reloaded on a prior iteration and the chunk still failed.\n // Leave the flag set and do nothing — the error UI is already\n // visible and `componentDidMount` / `componentDidUpdate` are\n // the only places that should clear the flag (they fire on a\n // successful render, signalling that the chunk is finally\n // healthy). Clearing here would let a subsequent click trigger\n // another reload and loop indefinitely.\n const flagKey = reloadFlagKey(this.props.source);\n if (readSessionFlag(flagKey)) {\n return;\n }\n setSessionFlag(flagKey);\n if (typeof window !== \"undefined\") {\n window.location.reload();\n }\n return;\n }\n this.setState((s) => ({\n hasError: false,\n error: null,\n retryKey: s.retryKey + 1,\n }));\n };\n\n render() {\n if (this.state.hasError) {\n // The primary copy is always the localised fallback so bundler-\n // internal strings (\"ChunkLoadError: Loading chunk 42 failed\",\n // \"Failed to fetch dynamically imported module\", etc.) never lead\n // the UI. The raw message is surfaced behind a collapsible\n // \"Error details\" affordance so support / dev users can still\n // diagnose persistent failures without reloading the tab.\n return (\n <LazyErrorUI\n errorMessage={this.state.error?.message ?? null}\n onRetry={this.handleRetry}\n />\n );\n }\n return this.props.children(this.state.retryKey);\n }\n}\n\n/**\n * Wraps a subtree with `<LazyErrorBoundary>` + `<Suspense>` so that any\n * `React.lazy()` / `lazyRetry()` page chunk below it gets a consistent\n * loading spinner and retry UI.\n *\n * The Suspense is keyed by the boundary's internal `retryKey` counter\n * so non-chunk render errors can be recovered by unmounting + remounting\n * the subtree. Chunk-load errors bypass this path and trigger a full\n * page reload (see `LazyErrorBoundary.handleRetry` — React.lazy's\n * promise cache cannot be cleared without creating a fresh lazy ref).\n */\nexport function LazyPageBoundary({\n children,\n source,\n}: {\n children: ReactNode;\n source?: string;\n}) {\n return (\n <LazyErrorBoundary source={source}>\n {(retryKey) => (\n <Suspense key={retryKey} fallback={<PageLoader />}>\n {children}\n </Suspense>\n )}\n </LazyErrorBoundary>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"LazyPageBoundary.js","names":[],"sources":["../../../src/conversations/components/LazyPageBoundary.tsx"],"sourcesContent":["import { Component, Suspense, type ErrorInfo, type ReactNode } from \"react\";\nimport { Loader2, AlertTriangle, RefreshCw } from \"lucide-react\";\nimport { useTr } from \"../utils/i18n\";\nimport { isChunkLoadError } from \"./lazyRetry\";\n\n/**\n * Fallback spinner rendered while a lazy-loaded page chunk is resolving.\n */\nfunction PageLoader() {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <Loader2 className=\"size-8 animate-spin text-primary\" />\n </div>\n );\n}\n\nfunction LazyErrorUI({\n errorMessage,\n onRetry,\n}: {\n errorMessage: string | null;\n onRetry: () => void;\n}) {\n const tr = useTr();\n\n return (\n <div className=\"flex flex-col items-center justify-center min-h-[400px] gap-4 text-center p-6\">\n <div className=\"p-3 rounded-full bg-destructive/10\">\n <AlertTriangle className=\"size-8 text-destructive\" />\n </div>\n <div className=\"space-y-2\">\n <h3 className=\"text-lg font-semibold text-foreground\">\n {tr(\"conversations.failedToLoadPage\", \"Failed to load page\")}\n </h3>\n <p className=\"text-sm text-muted-foreground max-w-md\">\n {tr(\n \"conversations.pageChunkLoadFailed\",\n \"The page chunk could not be loaded. This may be due to a network issue.\",\n )}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors\"\n >\n <RefreshCw className=\"size-4\" />\n {tr(\"conversations.tryAgain\", \"Try Again\")}\n </button>\n {errorMessage && (\n <details className=\"mt-2 max-w-md w-full text-left\">\n <summary className=\"cursor-pointer text-xs text-muted-foreground hover:text-foreground select-none\">\n {tr(\"conversations.errorDetails\", \"Error details\")}\n </summary>\n <pre className=\"mt-2 p-2 rounded bg-muted text-[11px] text-muted-foreground whitespace-pre-wrap break-all\">\n {errorMessage}\n </pre>\n </details>\n )}\n </div>\n );\n}\n\ntype LazyErrorBoundaryProps = {\n source?: string;\n children: (retryKey: number) => ReactNode;\n};\n\ntype LazyErrorBoundaryState = {\n hasError: boolean;\n error: Error | null;\n retryKey: number;\n};\n\n/**\n * Session-scoped flag used to prevent a reload loop when a chunk-load\n * failure is persistent (broken deploy, CDN outage, missing hash file).\n * First chunk-load retry triggers a page reload and sets the flag; if\n * the page mounts again with the same error, `handleRetry` sees the\n * flag and skips the second reload so the user stays on the error UI\n * and can take a different action instead of looping indefinitely.\n */\nconst RELOAD_FLAG_PREFIX = \"conversations-lazyreload-\";\n\nfunction reloadFlagKey(source?: string): string {\n return `${RELOAD_FLAG_PREFIX}${source ?? \"default\"}`;\n}\n\nfunction readSessionFlag(key: string): boolean {\n try {\n if (typeof sessionStorage === \"undefined\") return false;\n return sessionStorage.getItem(key) !== null;\n } catch {\n return false;\n }\n}\n\nfunction setSessionFlag(key: string): void {\n try {\n if (typeof sessionStorage === \"undefined\") return;\n sessionStorage.setItem(key, \"1\");\n } catch {\n // sessionStorage access can throw in some privacy / sandboxed modes\n }\n}\n\nfunction clearSessionFlag(key: string): void {\n try {\n if (typeof sessionStorage === \"undefined\") return;\n sessionStorage.removeItem(key);\n } catch {\n // ignore\n }\n}\n\n/**\n * Error boundary for lazy-loaded page chunks.\n *\n * On retry it branches by error type:\n * - **Chunk-load failures** (`ChunkLoadError`, \"Loading chunk X failed\",\n * \"Failed to fetch dynamically imported module\", etc.) — the rejected\n * promise is cached on the `LazyExoticComponent` itself; the only\n * reliable recovery is a full page reload, which re-creates all lazy\n * refs from scratch.\n * - **Other render errors** — clear the error state and bump an internal\n * `retryKey`; the enclosing `LazyPageBoundary` keys its `<Suspense>`\n * on that counter so the subtree unmounts and remounts. This resets\n * component state and helps recover from transient render issues.\n *\n * Accepts an optional `source` label used in the console tag so user-\n * route and admin-route chunks can be distinguished in telemetry.\n */\nclass LazyErrorBoundary extends Component<\n LazyErrorBoundaryProps,\n LazyErrorBoundaryState\n> {\n state: LazyErrorBoundaryState = {\n hasError: false,\n error: null,\n retryKey: 0,\n };\n\n static getDerivedStateFromError(error: Error) {\n return { hasError: true, error };\n }\n\n componentDidMount() {\n // Subtree mounted without catching an error — the previous session\n // state (if any) is stale. Clear the reload-guard flag so a future\n // chunk failure can still trigger one reload before giving up.\n if (!this.state.hasError) {\n clearSessionFlag(reloadFlagKey(this.props.source));\n }\n }\n\n componentDidUpdate(\n _prevProps: LazyErrorBoundaryProps,\n prevState: LazyErrorBoundaryState,\n ) {\n // Error → recovered (state reset / retryKey bump → clean render)\n // also invalidates the reload-guard flag.\n if (prevState.hasError && !this.state.hasError) {\n clearSessionFlag(reloadFlagKey(this.props.source));\n }\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n const tag = this.props.source\n ? `[Conversations:${this.props.source}]`\n : \"[Conversations]\";\n // Log the raw error details (including the message) to the console\n // only — user-facing copy is always a localised fallback so that\n // bundler-internal strings like \"ChunkLoadError: Loading chunk 42 failed\"\n // never surface to end users.\n console.error(`${tag} Lazy page load failed:`, error, errorInfo);\n }\n\n handleRetry = () => {\n if (isChunkLoadError(this.state.error)) {\n // React.lazy caches rejected promises on the LazyExoticComponent\n // itself — keying Suspense or resetting boundary state does NOT\n // clear it. A full reload is the only reliable recovery.\n //\n // Guard against a reload loop: if the flag is already set we\n // reloaded on a prior iteration and the chunk still failed.\n // Leave the flag set and do nothing — the error UI is already\n // visible and `componentDidMount` / `componentDidUpdate` are\n // the only places that should clear the flag (they fire on a\n // successful render, signalling that the chunk is finally\n // healthy). Clearing here would let a subsequent click trigger\n // another reload and loop indefinitely.\n const flagKey = reloadFlagKey(this.props.source);\n if (readSessionFlag(flagKey)) {\n return;\n }\n setSessionFlag(flagKey);\n if (typeof window !== \"undefined\") {\n window.location.reload();\n }\n return;\n }\n this.setState((s) => ({\n hasError: false,\n error: null,\n retryKey: s.retryKey + 1,\n }));\n };\n\n render() {\n if (this.state.hasError) {\n // The primary copy is always the localised fallback so bundler-\n // internal strings (\"ChunkLoadError: Loading chunk 42 failed\",\n // \"Failed to fetch dynamically imported module\", etc.) never lead\n // the UI. The raw message is surfaced behind a collapsible\n // \"Error details\" affordance so support / dev users can still\n // diagnose persistent failures without reloading the tab.\n return (\n <LazyErrorUI\n errorMessage={this.state.error?.message ?? null}\n onRetry={this.handleRetry}\n />\n );\n }\n return this.props.children(this.state.retryKey);\n }\n}\n\n/**\n * Wraps a subtree with `<LazyErrorBoundary>` + `<Suspense>` so that any\n * `React.lazy()` / `lazyRetry()` page chunk below it gets a consistent\n * loading spinner and retry UI.\n *\n * The Suspense is keyed by the boundary's internal `retryKey` counter\n * so non-chunk render errors can be recovered by unmounting + remounting\n * the subtree. Chunk-load errors bypass this path and trigger a full\n * page reload (see `LazyErrorBoundary.handleRetry` — React.lazy's\n * promise cache cannot be cleared without creating a fresh lazy ref).\n */\nexport function LazyPageBoundary({\n children,\n source,\n}: {\n children: ReactNode;\n source?: string;\n}) {\n return (\n <LazyErrorBoundary source={source}>\n {(retryKey) => (\n <Suspense key={retryKey} fallback={<PageLoader />}>\n {children}\n </Suspense>\n )}\n </LazyErrorBoundary>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lazyRetry.js","names":[],"sources":["../../../src/conversations/components/lazyRetry.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"lazyRetry.js","names":[],"sources":["../../../src/conversations/components/lazyRetry.ts"],"sourcesContent":["import { lazy, type ComponentType, type LazyExoticComponent } from \"react\";\n\nfunction isChunkLoadError(error: Error | null | undefined): boolean {\n if (!error) return false;\n if (error.name === \"ChunkLoadError\") return true;\n const message = error.message ?? \"\";\n return (\n message.includes(\"Loading chunk\") ||\n message.includes(\"Failed to fetch dynamically imported module\") ||\n message.includes(\"Importing a module script failed\") ||\n message.includes(\"Failed to fetch\")\n );\n}\n\nconst LAZY_RETRY_DELAY_MS = 300;\n\nexport function lazyRetry<TProps extends object = object>(\n factory: () => Promise<{ default: ComponentType<TProps> }>,\n): LazyExoticComponent<ComponentType<TProps>> {\n return lazy(() =>\n factory().catch((err: unknown) => {\n if (err instanceof Error && isChunkLoadError(err)) {\n return new Promise<{ default: ComponentType<TProps> }>(\n (resolve, reject) => {\n setTimeout(() => {\n factory().then(resolve, reject);\n }, LAZY_RETRY_DELAY_MS);\n },\n );\n }\n throw err;\n }),\n );\n}\n\nexport { isChunkLoadError };\n"],"mappings":";;AAEA,SAAS,EAAiB,GAA0C;AAClE,KAAI,CAAC,EAAO,QAAO;AACnB,KAAI,EAAM,SAAS,iBAAkB,QAAO;CAC5C,IAAM,IAAU,EAAM,WAAW;AACjC,QACE,EAAQ,SAAS,gBAAgB,IACjC,EAAQ,SAAS,8CAA8C,IAC/D,EAAQ,SAAS,mCAAmC,IACpD,EAAQ,SAAS,kBAAkB;;AAIvC,IAAM,IAAsB;AAE5B,SAAgB,EACd,GAC4C;AAC5C,QAAO,QACL,GAAS,CAAC,OAAO,MAAiB;AAChC,MAAI,aAAe,SAAS,EAAiB,EAAI,CAC/C,QAAO,IAAI,SACR,GAAS,MAAW;AACnB,oBAAiB;AACf,OAAS,CAAC,KAAK,GAAS,EAAO;MAC9B,EAAoB;IAE1B;AAEH,QAAM;GACN,CACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useMessageQueries.js","names":[],"sources":["../../../src/conversations/hooks/useMessageQueries.ts"],"sourcesContent":["import { useCallback, useMemo } from \"react\";\nimport {\n useAddReactionMutation,\n useDeleteMessageMutation,\n useEditMessageMutation,\n GetMessagesDocument,\n useGetMessagesQuery,\n usePinMessageByHookMutation,\n useRemoveReactionMutation,\n useSendMessageMutation,\n useUnpinMessageByHookMutation,\n} from \"../../generated/wspace-operations\";\nimport type { Message, MessageType } from \"../types\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { mapMessage } from \"../utils/mappers\";\n\ninterface UseGetMessagesOptions {\n first?: number;\n after?: string;\n before?: string;\n}\n\ninterface UseGetMessagesResult {\n messages: Message[];\n loading: boolean;\n error: Error | null;\n hasNextPage: boolean;\n hasPreviousPage: boolean;\n fetchMore: () => void;\n fetchPrevious: () => void;\n refetch: () => void;\n}\n\n/**\n * Hook: Fetch paginated messages for a conversation.\n */\nexport function useGetMessages(\n conversationId: string,\n options?: UseGetMessagesOptions,\n): UseGetMessagesResult {\n const { workspaceId } = useConversations();\n const queryResult = useGetMessagesQuery({\n variables: {\n conversationId,\n first: options?.first ?? 50,\n after: options?.after,\n before: options?.before,\n },\n skip: !workspaceId || !conversationId,\n });\n\n const messages = useMemo(\n () =>\n queryResult.data?.messages.edges.map(\n (edge: { node: Record<string, unknown> }) => mapMessage(edge.node),\n ) ?? [],\n [queryResult.data],\n );\n\n const pageInfo = queryResult.data?.messages.pageInfo;\n\n return {\n messages,\n loading: queryResult.loading,\n error: queryResult.error ?? null,\n hasNextPage: pageInfo?.hasNextPage ?? false,\n hasPreviousPage: pageInfo?.hasPreviousPage ?? false,\n fetchMore: () => {\n const endCursor = pageInfo?.endCursor;\n if (!endCursor || !pageInfo?.hasNextPage) return;\n void queryResult.fetchMore({\n variables: {\n conversationId,\n first: options?.first ?? 50,\n after: endCursor,\n },\n });\n },\n fetchPrevious: () => {\n const startCursor = pageInfo?.startCursor;\n if (!startCursor || !pageInfo?.hasPreviousPage) return;\n void queryResult.fetchMore({\n variables: {\n conversationId,\n before: startCursor,\n first: options?.first ?? 50,\n },\n });\n },\n refetch: () => {\n void queryResult.refetch();\n },\n };\n}\n\ninterface SendMessageInput {\n conversationId: string;\n content: string;\n type?: MessageType;\n parentMessageId?: string;\n attachmentIds?: string[];\n mentionUserIds?: string[];\n refetchFirst?: number;\n}\n\ninterface UseSendMessageResult {\n sendMessage: (input: SendMessageInput) => Promise<Message | null>;\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * Hook: Send a message to a conversation.\n */\nexport function useSendMessage(): UseSendMessageResult {\n const [sendMessageMutation, mutationState] = useSendMessageMutation();\n\n const sendMessage = useCallback(\n async (input: SendMessageInput): Promise<Message | null> => {\n try {\n const { data } = await sendMessageMutation({\n variables: {\n input: {\n conversationId: input.conversationId,\n type: input.type ?? \"TEXT\",\n content: { text: input.content },\n parentMessageId: input.parentMessageId,\n attachmentIds: input.attachmentIds,\n mentionUserIds: input.mentionUserIds,\n },\n },\n refetchQueries: [\n {\n query: GetMessagesDocument,\n variables: {\n conversationId: input.conversationId,\n first: input.refetchFirst ?? 50,\n },\n },\n ],\n });\n\n const message = data?.sendMessage.message;\n return message ? mapMessage(message) : null;\n } catch (err) {\n throw err instanceof Error ? err : new Error(\"Failed to send message\");\n }\n },\n [sendMessageMutation],\n );\n\n return {\n sendMessage,\n loading: mutationState.loading,\n error: mutationState.error ?? null,\n };\n}\n\ninterface UseEditMessageResult {\n editMessage: (\n messageId: string,\n content: string,\n reason?: string,\n ) => Promise<Message | null>;\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * Hook: Edit an existing message.\n */\nexport function useEditMessage(): UseEditMessageResult {\n const [editMessageMutation, mutationState] = useEditMessageMutation();\n\n const editMessage = useCallback(\n async (\n messageId: string,\n content: string,\n reason?: string,\n ): Promise<Message | null> => {\n try {\n const { data } = await editMessageMutation({\n variables: {\n input: {\n id: messageId,\n content: { text: content },\n reason,\n },\n },\n });\n const message = data?.editMessage;\n return message ? mapMessage(message as Record<string, unknown>) : null;\n } catch (err) {\n throw err instanceof Error ? err : new Error(\"Failed to edit message\");\n }\n },\n [editMessageMutation],\n );\n\n return {\n editMessage,\n loading: mutationState.loading,\n error: mutationState.error ?? null,\n };\n}\n\ninterface UseMessageActionsResult {\n deleteMessage: (messageId: string) => Promise<boolean>;\n addReaction: (messageId: string, emoji: string) => Promise<boolean>;\n removeReaction: (messageId: string, emoji: string) => Promise<boolean>;\n pinMessage: (messageId: string) => Promise<boolean>;\n unpinMessage: (messageId: string) => Promise<boolean>;\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * Hook: Message action mutations (delete, react, pin).\n */\nexport function useMessageActions(): UseMessageActionsResult {\n const [deleteMessageMutation, deleteState] = useDeleteMessageMutation();\n const [addReactionMutation, addReactionState] = useAddReactionMutation();\n const [removeReactionMutation, removeReactionState] =\n useRemoveReactionMutation();\n const [pinMessageMutation, pinState] = usePinMessageByHookMutation();\n const [unpinMessageMutation, unpinState] = useUnpinMessageByHookMutation();\n\n const deleteMessage = useCallback(\n async (messageId: string): Promise<boolean> => {\n const { data } = await deleteMessageMutation({\n variables: { id: messageId },\n });\n return data?.deleteMessage.success ?? false;\n },\n [deleteMessageMutation],\n );\n\n const addReaction = useCallback(\n async (messageId: string, emoji: string): Promise<boolean> => {\n const { data } = await addReactionMutation({\n variables: { messageId, emoji },\n });\n return Boolean(data?.addReaction.id);\n },\n [addReactionMutation],\n );\n\n const removeReaction = useCallback(\n async (messageId: string, emoji: string): Promise<boolean> => {\n const { data } = await removeReactionMutation({\n variables: { messageId, emoji },\n });\n return data?.removeReaction.success ?? false;\n },\n [removeReactionMutation],\n );\n\n const pinMessage = useCallback(\n async (messageId: string): Promise<boolean> => {\n const { data } = await pinMessageMutation({ variables: { messageId } });\n return Boolean(data?.pinMessage?.id);\n },\n [pinMessageMutation],\n );\n\n const unpinMessage = useCallback(\n async (messageId: string): Promise<boolean> => {\n const { data } = await unpinMessageMutation({ variables: { messageId } });\n return Boolean(data?.unpinMessage?.id);\n },\n [unpinMessageMutation],\n );\n\n return {\n deleteMessage,\n addReaction,\n removeReaction,\n pinMessage,\n unpinMessage,\n loading:\n deleteState.loading ||\n addReactionState.loading ||\n removeReactionState.loading ||\n pinState.loading ||\n unpinState.loading,\n error:\n (deleteState.error as Error | undefined) ??\n (addReactionState.error as Error | undefined) ??\n (removeReactionState.error as Error | undefined) ??\n (pinState.error as Error | undefined) ??\n (unpinState.error as Error | undefined) ??\n null,\n };\n}\n\n"],"mappings":";;;;;AAoCA,SAAgB,EACd,GACA,GACsB;CACtB,IAAM,EAAE,mBAAgB,GAAkB,EACpC,IAAc,EAAoB;EACtC,WAAW;GACT;GACA,OAAO,GAAS,SAAS;GACzB,OAAO,GAAS;GAChB,QAAQ,GAAS;GAClB;EACD,MAAM,CAAC,KAAe,CAAC;EACxB,CAAC,EAEI,IAAW,QAEb,EAAY,MAAM,SAAS,MAAM,KAC9B,MAA4C,EAAW,EAAK,KAAK,CACnE,IAAI,EAAE,EACT,CAAC,EAAY,KAAK,CACnB,EAEK,IAAW,EAAY,MAAM,SAAS;AAE5C,QAAO;EACL;EACA,SAAS,EAAY;EACrB,OAAO,EAAY,SAAS;EAC5B,aAAa,GAAU,eAAe;EACtC,iBAAiB,GAAU,mBAAmB;EAC9C,iBAAiB;GACf,IAAM,IAAY,GAAU;AACxB,IAAC,KAAa,CAAC,GAAU,eACxB,EAAY,UAAU,EACzB,WAAW;IACT;IACA,OAAO,GAAS,SAAS;IACzB,OAAO;IACR,EACF,CAAC;;EAEJ,qBAAqB;GACnB,IAAM,IAAc,GAAU;AAC1B,IAAC,KAAe,CAAC,GAAU,mBAC1B,EAAY,UAAU,EACzB,WAAW;IACT;IACA,QAAQ;IACR,OAAO,GAAS,SAAS;IAC1B,EACF,CAAC;;EAEJ,eAAe;AACR,KAAY,SAAS;;EAE7B;;AAsBH,SAAgB,IAAuC;CACrD,IAAM,CAAC,GAAqB,KAAiB,GAAwB;AAoCrE,QAAO;EACL,aAnCkB,EAClB,OAAO,MAAqD;AAC1D,OAAI;IACF,IAAM,EAAE,YAAS,MAAM,EAAoB;KACzC,WAAW,EACT,OAAO;MACL,gBAAgB,EAAM;MACtB,MAAM,EAAM,QAAQ;MACpB,SAAS,EAAE,MAAM,EAAM,SAAS;MAChC,iBAAiB,EAAM;MACvB,eAAe,EAAM;MACrB,gBAAgB,EAAM;MACvB,EACF;KACD,gBAAgB,CACd;MACE,OAAO;MACP,WAAW;OACT,gBAAgB,EAAM;OACtB,OAAO,EAAM,gBAAgB;OAC9B;MACF,CACF;KACF,CAAC,EAEI,IAAU,GAAM,YAAY;AAClC,WAAO,IAAU,EAAW,EAAQ,GAAG;YAChC,GAAK;AACZ,UAAM,aAAe,QAAQ,IAAM,gBAAI,MAAM,yBAAyB;;KAG1E,CAAC,EAAoB,CACtB;EAIC,SAAS,EAAc;EACvB,OAAO,EAAc,SAAS;EAC/B;;AAgBH,SAAgB,IAAuC;CACrD,IAAM,CAAC,GAAqB,KAAiB,GAAwB;AA2BrE,QAAO;EACL,aA1BkB,EAClB,OACE,GACA,GACA,MAC4B;AAC5B,OAAI;IACF,IAAM,EAAE,YAAS,MAAM,EAAoB,EACzC,WAAW,EACT,OAAO;KACL,IAAI;KACJ,SAAS,EAAE,MAAM,GAAS;KAC1B;KACD,EACF,EACF,CAAC,EACI,IAAU,GAAM;AACtB,WAAO,IAAU,EAAW,EAAmC,GAAG;YAC3D,GAAK;AACZ,UAAM,aAAe,QAAQ,IAAM,gBAAI,MAAM,yBAAyB;;KAG1E,CAAC,EAAoB,CACtB;EAIC,SAAS,EAAc;EACvB,OAAO,EAAc,SAAS;EAC/B;;AAgBH,SAAgB,IAA6C;CAC3D,IAAM,CAAC,GAAuB,KAAe,GAA0B,EACjE,CAAC,GAAqB,KAAoB,GAAwB,EAClE,CAAC,GAAwB,KAC7B,GAA2B,EACvB,CAAC,GAAoB,KAAY,GAA6B,EAC9D,CAAC,GAAsB,KAAc,GAA+B;AAgD1E,QAAO;EACL,eA/CoB,EACpB,OAAO,MAAwC;GAC7C,IAAM,EAAE,YAAS,MAAM,EAAsB,EAC3C,WAAW,EAAE,IAAI,GAAW,EAC7B,CAAC;AACF,UAAO,GAAM,cAAc,WAAW;KAExC,CAAC,EAAsB,CACxB;EAwCC,aAtCkB,EAClB,OAAO,GAAmB,MAAoC;GAC5D,IAAM,EAAE,YAAS,MAAM,EAAoB,EACzC,WAAW;IAAE;IAAW;IAAO,EAChC,CAAC;AACF,UAAO,EAAQ,GAAM,YAAY;KAEnC,CAAC,EAAoB,CACtB;EA+BC,gBA7BqB,EACrB,OAAO,GAAmB,MAAoC;GAC5D,IAAM,EAAE,YAAS,MAAM,EAAuB,EAC5C,WAAW;IAAE;IAAW;IAAO,EAChC,CAAC;AACF,UAAO,GAAM,eAAe,WAAW;KAEzC,CAAC,EAAuB,CACzB;EAsBC,YApBiB,EACjB,OAAO,MAAwC;GAC7C,IAAM,EAAE,YAAS,MAAM,EAAmB,EAAE,WAAW,EAAE,cAAW,EAAE,CAAC;AACvE,UAAO,EAAQ,GAAM,YAAY;KAEnC,CAAC,EAAmB,CACrB;EAeC,cAbmB,EACnB,OAAO,MAAwC;GAC7C,IAAM,EAAE,YAAS,MAAM,EAAqB,EAAE,WAAW,EAAE,cAAW,EAAE,CAAC;AACzE,UAAO,EAAQ,GAAM,cAAc;KAErC,CAAC,EAAqB,CACvB;EAQC,SACE,EAAY,WACZ,EAAiB,WACjB,EAAoB,WACpB,EAAS,WACT,EAAW;EACb,OACG,EAAY,SACZ,EAAiB,SACjB,EAAoB,SACpB,EAAS,SACT,EAAW,SACZ;EACH"}
|
|
1
|
+
{"version":3,"file":"useMessageQueries.js","names":[],"sources":["../../../src/conversations/hooks/useMessageQueries.ts"],"sourcesContent":["import { useCallback, useMemo } from \"react\";\nimport {\n useAddReactionMutation,\n useDeleteMessageMutation,\n useEditMessageMutation,\n GetMessagesDocument,\n useGetMessagesQuery,\n usePinMessageByHookMutation,\n useRemoveReactionMutation,\n useSendMessageMutation,\n useUnpinMessageByHookMutation,\n} from \"../../generated/wspace-operations\";\nimport type { Message, MessageType } from \"../types\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { mapMessage } from \"../utils/mappers\";\n\ninterface UseGetMessagesOptions {\n first?: number;\n after?: string;\n before?: string;\n}\n\ninterface UseGetMessagesResult {\n messages: Message[];\n loading: boolean;\n error: Error | null;\n hasNextPage: boolean;\n hasPreviousPage: boolean;\n fetchMore: () => void;\n fetchPrevious: () => void;\n refetch: () => void;\n}\n\n/**\n * Hook: Fetch paginated messages for a conversation.\n */\nexport function useGetMessages(\n conversationId: string,\n options?: UseGetMessagesOptions,\n): UseGetMessagesResult {\n const { workspaceId } = useConversations();\n const queryResult = useGetMessagesQuery({\n variables: {\n conversationId,\n first: options?.first ?? 50,\n after: options?.after,\n before: options?.before,\n },\n skip: !workspaceId || !conversationId,\n });\n\n const messages = useMemo(\n () =>\n queryResult.data?.messages.edges.map(\n (edge: { node: Record<string, unknown> }) => mapMessage(edge.node),\n ) ?? [],\n [queryResult.data],\n );\n\n const pageInfo = queryResult.data?.messages.pageInfo;\n\n return {\n messages,\n loading: queryResult.loading,\n error: queryResult.error ?? null,\n hasNextPage: pageInfo?.hasNextPage ?? false,\n hasPreviousPage: pageInfo?.hasPreviousPage ?? false,\n fetchMore: () => {\n const endCursor = pageInfo?.endCursor;\n if (!endCursor || !pageInfo?.hasNextPage) return;\n void queryResult.fetchMore({\n variables: {\n conversationId,\n first: options?.first ?? 50,\n after: endCursor,\n },\n });\n },\n fetchPrevious: () => {\n const startCursor = pageInfo?.startCursor;\n if (!startCursor || !pageInfo?.hasPreviousPage) return;\n void queryResult.fetchMore({\n variables: {\n conversationId,\n before: startCursor,\n first: options?.first ?? 50,\n },\n });\n },\n refetch: () => {\n void queryResult.refetch();\n },\n };\n}\n\ninterface SendMessageInput {\n conversationId: string;\n content: string;\n type?: MessageType;\n parentMessageId?: string;\n attachmentIds?: string[];\n mentionUserIds?: string[];\n refetchFirst?: number;\n}\n\ninterface UseSendMessageResult {\n sendMessage: (input: SendMessageInput) => Promise<Message | null>;\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * Hook: Send a message to a conversation.\n */\nexport function useSendMessage(): UseSendMessageResult {\n const [sendMessageMutation, mutationState] = useSendMessageMutation();\n\n const sendMessage = useCallback(\n async (input: SendMessageInput): Promise<Message | null> => {\n try {\n const { data } = await sendMessageMutation({\n variables: {\n input: {\n conversationId: input.conversationId,\n type: input.type ?? \"TEXT\",\n content: { text: input.content },\n parentMessageId: input.parentMessageId,\n attachmentIds: input.attachmentIds,\n mentionUserIds: input.mentionUserIds,\n },\n },\n refetchQueries: [\n {\n query: GetMessagesDocument,\n variables: {\n conversationId: input.conversationId,\n first: input.refetchFirst ?? 50,\n },\n },\n ],\n });\n\n const message = data?.sendMessage.message;\n return message ? mapMessage(message) : null;\n } catch (err) {\n throw err instanceof Error ? err : new Error(\"Failed to send message\");\n }\n },\n [sendMessageMutation],\n );\n\n return {\n sendMessage,\n loading: mutationState.loading,\n error: mutationState.error ?? null,\n };\n}\n\ninterface UseEditMessageResult {\n editMessage: (\n messageId: string,\n content: string,\n reason?: string,\n ) => Promise<Message | null>;\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * Hook: Edit an existing message.\n */\nexport function useEditMessage(): UseEditMessageResult {\n const [editMessageMutation, mutationState] = useEditMessageMutation();\n\n const editMessage = useCallback(\n async (\n messageId: string,\n content: string,\n reason?: string,\n ): Promise<Message | null> => {\n try {\n const { data } = await editMessageMutation({\n variables: {\n input: {\n id: messageId,\n content: { text: content },\n reason,\n },\n },\n });\n const message = data?.editMessage;\n return message ? mapMessage(message as Record<string, unknown>) : null;\n } catch (err) {\n throw err instanceof Error ? err : new Error(\"Failed to edit message\");\n }\n },\n [editMessageMutation],\n );\n\n return {\n editMessage,\n loading: mutationState.loading,\n error: mutationState.error ?? null,\n };\n}\n\ninterface UseMessageActionsResult {\n deleteMessage: (messageId: string) => Promise<boolean>;\n addReaction: (messageId: string, emoji: string) => Promise<boolean>;\n removeReaction: (messageId: string, emoji: string) => Promise<boolean>;\n pinMessage: (messageId: string) => Promise<boolean>;\n unpinMessage: (messageId: string) => Promise<boolean>;\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * Hook: Message action mutations (delete, react, pin).\n */\nexport function useMessageActions(): UseMessageActionsResult {\n const [deleteMessageMutation, deleteState] = useDeleteMessageMutation();\n const [addReactionMutation, addReactionState] = useAddReactionMutation();\n const [removeReactionMutation, removeReactionState] =\n useRemoveReactionMutation();\n const [pinMessageMutation, pinState] = usePinMessageByHookMutation();\n const [unpinMessageMutation, unpinState] = useUnpinMessageByHookMutation();\n\n const deleteMessage = useCallback(\n async (messageId: string): Promise<boolean> => {\n const { data } = await deleteMessageMutation({\n variables: { id: messageId },\n });\n return data?.deleteMessage.success ?? false;\n },\n [deleteMessageMutation],\n );\n\n const addReaction = useCallback(\n async (messageId: string, emoji: string): Promise<boolean> => {\n const { data } = await addReactionMutation({\n variables: { messageId, emoji },\n });\n return Boolean(data?.addReaction.id);\n },\n [addReactionMutation],\n );\n\n const removeReaction = useCallback(\n async (messageId: string, emoji: string): Promise<boolean> => {\n const { data } = await removeReactionMutation({\n variables: { messageId, emoji },\n });\n return data?.removeReaction.success ?? false;\n },\n [removeReactionMutation],\n );\n\n const pinMessage = useCallback(\n async (messageId: string): Promise<boolean> => {\n const { data } = await pinMessageMutation({ variables: { messageId } });\n return Boolean(data?.pinMessage?.id);\n },\n [pinMessageMutation],\n );\n\n const unpinMessage = useCallback(\n async (messageId: string): Promise<boolean> => {\n const { data } = await unpinMessageMutation({ variables: { messageId } });\n return Boolean(data?.unpinMessage?.id);\n },\n [unpinMessageMutation],\n );\n\n return {\n deleteMessage,\n addReaction,\n removeReaction,\n pinMessage,\n unpinMessage,\n loading:\n deleteState.loading ||\n addReactionState.loading ||\n removeReactionState.loading ||\n pinState.loading ||\n unpinState.loading,\n error:\n (deleteState.error as Error | undefined) ??\n (addReactionState.error as Error | undefined) ??\n (removeReactionState.error as Error | undefined) ??\n (pinState.error as Error | undefined) ??\n (unpinState.error as Error | undefined) ??\n null,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConversationAdminPages.js","names":[],"sources":["../../../src/conversations/pages/ConversationAdminPages.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from \"react\";\nimport { gql } from \"@apollo/client\";\nimport { useMutation, useQuery } from \"@apollo/client/react\";\nimport {\n Activity,\n Archive,\n CheckCircle2,\n Package,\n ShieldCheck,\n Trash2,\n} from \"lucide-react\";\n\nimport { EmptyState, PageLayout, PageSection } from \"../components/PageLayout\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useTr } from \"../utils/i18n\";\n\nconst GET_MESSAGE_REPORTS = gql`\n query ConversationAdminMessageReports(\n $first: Int\n $filter: MessageReportFilter\n ) {\n messageReports(first: $first, filter: $filter) {\n edges {\n node {\n id\n messageId\n reportedBy\n reason\n status\n notes\n createdAt\n reviewedAt\n message {\n id\n conversationId\n authorId\n content\n isHidden\n createdAt\n }\n }\n }\n }\n }\n`;\n\nconst RESOLVE_MESSAGE_REPORT = gql`\n mutation ConversationAdminResolveMessageReport(\n $input: ResolveMessageReportInput!\n ) {\n resolveMessageReport(input: $input) {\n id\n status\n reviewedAt\n notes\n message {\n id\n isHidden\n }\n }\n }\n`;\n\nconst GET_BULK_CONVERSATIONS = gql`\n query ConversationAdminBulkConversations(\n $first: Int\n $filter: ConversationFilter\n ) {\n conversations(first: $first, filter: $filter) {\n edges {\n node {\n id\n title\n type\n state\n updatedAt\n }\n }\n }\n }\n`;\n\nconst BULK_UPDATE_CONVERSATIONS = gql`\n mutation ConversationAdminBulkUpdate(\n $input: BulkConversationOperationInput!\n ) {\n bulkUpdateConversations(input: $input) {\n success\n processedCount\n successCount\n failureCount\n conversationIds\n errors {\n conversationId\n message\n }\n }\n }\n`;\n\nconst GET_CONVERSATION_AUDIT_LOG = gql`\n query ConversationAdminAuditLog($filter: ActivityLogFilter) {\n getActivities(filter: $filter) {\n id\n action\n description\n resourceId\n resourceType\n userId\n timestamp\n }\n }\n`;\n\nconst GET_APP_INSTALLATIONS = gql`\n query ConversationAdminAppInstallations($workspaceId: ID) {\n appInstallations(workspaceId: $workspaceId) {\n id\n appId\n appVersion\n state\n settings\n permissions\n installedBy\n installedAt\n updatedAt\n }\n }\n`;\n\nconst INSTALL_APP = gql`\n mutation ConversationAdminInstallApp($input: InstallAppInput!) {\n installApp(input: $input) {\n id\n appId\n state\n settings\n installedAt\n }\n }\n`;\n\nconst UPDATE_APP_SETTINGS = gql`\n mutation ConversationAdminUpdateAppSettings($id: ID!, $settings: JSON!) {\n updateAppSettings(id: $id, settings: $settings) {\n id\n settings\n updatedAt\n }\n }\n`;\n\nconst UNINSTALL_APP = gql`\n mutation ConversationAdminUninstallApp($id: ID!) {\n uninstallApp(id: $id) {\n appInstallationId\n success\n }\n }\n`;\n\ninterface MessageReportNode {\n id: string;\n messageId: string;\n reportedBy: string;\n reason: string;\n status: string;\n notes?: string | null;\n createdAt: string;\n reviewedAt?: string | null;\n message: {\n id: string;\n conversationId: string;\n authorId: string;\n content: unknown;\n isHidden: boolean;\n createdAt: string;\n };\n}\n\ninterface ConversationNode {\n id: string;\n title?: string | null;\n type: string;\n state: string;\n updatedAt: string;\n}\n\ninterface ActivityLogNode {\n id: string;\n action: string;\n description?: string | null;\n resourceId?: string | null;\n resourceType?: string | null;\n userId: string;\n timestamp: string;\n}\n\ninterface AppInstallationNode {\n id: string;\n appId: string;\n appVersion: string;\n state: string;\n settings: unknown;\n permissions: string[];\n installedBy: string;\n installedAt: string;\n updatedAt: string;\n}\n\ninterface BulkUpdateConversationsResult {\n bulkUpdateConversations: {\n success: boolean;\n processedCount: number;\n successCount: number;\n failureCount: number;\n conversationIds: string[];\n errors: Array<{ conversationId: string; message: string }>;\n };\n}\n\nfunction formatContent(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (content && typeof content === \"object\" && \"text\" in content) {\n const text = (content as { text?: unknown }).text;\n if (typeof text === \"string\") return text;\n }\n try {\n return JSON.stringify(content);\n } catch {\n return \"Message content unavailable\";\n }\n}\n\nconst DATE_FORMATTER = new Intl.DateTimeFormat(undefined, {\n dateStyle: \"medium\",\n timeStyle: \"short\",\n});\n\nfunction formatDate(value: string): string {\n return DATE_FORMATTER.format(new Date(value));\n}\n\nfunction StatusPill({ children }: { children: string }) {\n return (\n <span className=\"rounded-full border border-border bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground\">\n {children}\n </span>\n );\n}\n\nexport function ModerationQueuePage() {\n const tr = useTr();\n const { data, loading, error, refetch } = useQuery<{\n messageReports: { edges: Array<{ node: MessageReportNode }> };\n }>(GET_MESSAGE_REPORTS, {\n variables: { first: 50, filter: { statuses: [\"PENDING\", \"REVIEWING\"] } },\n });\n const [resolveReport, { loading: resolving }] = useMutation(\n RESOLVE_MESSAGE_REPORT,\n {\n onCompleted: () => void refetch(),\n },\n );\n\n const reports = data?.messageReports.edges.map((edge) => edge.node) ?? [];\n\n return (\n <PageLayout\n title={tr(\"conversations.admin.moderationPageTitle\", \"Moderation queue\")}\n subtitle={tr(\n \"conversations.admin.moderationPageDescription\",\n \"Review reported messages and apply moderator decisions.\",\n )}\n icon={<ShieldCheck className=\"size-5\" aria-hidden=\"true\" />}\n maxWidth=\"2xl\"\n >\n {loading && (\n <p className=\"text-sm text-muted-foreground\">Loading reports…</p>\n )}\n {error && <p className=\"text-sm text-destructive\">{error.message}</p>}\n {!loading && !error && reports.length === 0 && (\n <EmptyState\n title=\"No reports waiting for review\"\n description=\"Reported messages will appear here as they are submitted.\"\n icon={<CheckCircle2 className=\"size-6\" aria-hidden=\"true\" />}\n />\n )}\n <div className=\"grid gap-3\">\n {reports.map((report) => (\n <article\n key={report.id}\n className=\"rounded-lg border border-border bg-card p-4 shadow-sm\"\n >\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-2\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusPill>{report.status}</StatusPill>\n {report.message.isHidden && <StatusPill>Hidden</StatusPill>}\n <span className=\"text-xs text-muted-foreground\">\n Reported {formatDate(report.createdAt)}\n </span>\n </div>\n <p className=\"text-sm font-medium text-foreground\">\n {report.reason}\n </p>\n <p className=\"rounded-md bg-muted p-3 text-sm text-muted-foreground\">\n {formatContent(report.message.content)}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n Message {report.messageId} in conversation{\" \"}\n {report.message.conversationId} by {report.message.authorId};\n reported by {report.reportedBy}\n </p>\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n disabled={resolving}\n className=\"rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground hover:bg-accent disabled:opacity-50\"\n onClick={() =>\n void resolveReport({\n variables: {\n input: { reportId: report.id, action: \"DISMISS\" },\n },\n })\n }\n >\n Dismiss\n </button>\n <button\n type=\"button\"\n disabled={resolving}\n className=\"rounded-md bg-primary px-3 py-2 text-sm text-primary-foreground hover:bg-primary/90 disabled:opacity-50\"\n onClick={() =>\n void resolveReport({\n variables: {\n input: { reportId: report.id, action: \"HIDE_MESSAGE\" },\n },\n })\n }\n >\n Hide and resolve\n </button>\n </div>\n </div>\n </article>\n ))}\n </div>\n </PageLayout>\n );\n}\n\nexport function BulkOperationsPage() {\n const tr = useTr();\n const [selectedIds, setSelectedIds] = useState<Set<string>>(() => new Set());\n const [action, setAction] = useState<\"ARCHIVE\" | \"UNARCHIVE\" | \"DELETE\">(\n \"ARCHIVE\",\n );\n const { data, loading, error, refetch } = useQuery<{\n conversations: { edges: Array<{ node: ConversationNode }> };\n }>(GET_BULK_CONVERSATIONS, { variables: { first: 100 } });\n const [bulkUpdate, { data: bulkResult, loading: updating }] = useMutation<\n BulkUpdateConversationsResult,\n { input: { conversationIds: string[]; action: typeof action } }\n >(BULK_UPDATE_CONVERSATIONS, {\n onCompleted: () => {\n setSelectedIds(new Set());\n void refetch();\n },\n });\n const conversations =\n data?.conversations.edges.map((edge) => edge.node) ?? [];\n\n const toggle = (id: string) => {\n setSelectedIds((current) => {\n const next = new Set(current);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n };\n\n return (\n <PageLayout\n title={tr(\n \"conversations.admin.bulkOperationsPageTitle\",\n \"Bulk operations\",\n )}\n subtitle={tr(\n \"conversations.admin.bulkOperationsPageDescription\",\n \"Select workspace conversations and apply archive, unarchive, or delete operations.\",\n )}\n icon={<Archive className=\"size-5\" aria-hidden=\"true\" />}\n maxWidth=\"2xl\"\n headerActions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <select\n value={action}\n onChange={(event) => setAction(event.target.value as typeof action)}\n className=\"rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground\"\n >\n <option value=\"ARCHIVE\">Archive</option>\n <option value=\"UNARCHIVE\">Unarchive</option>\n <option value=\"DELETE\">Delete</option>\n </select>\n <button\n type=\"button\"\n disabled={selectedIds.size === 0 || updating}\n className=\"rounded-md bg-primary px-3 py-2 text-sm text-primary-foreground hover:bg-primary/90 disabled:opacity-50\"\n onClick={() =>\n void bulkUpdate({\n variables: {\n input: { conversationIds: Array.from(selectedIds), action },\n },\n })\n }\n >\n Apply to {selectedIds.size}\n </button>\n </div>\n }\n >\n {loading && (\n <p className=\"text-sm text-muted-foreground\">\n Loading conversations…\n </p>\n )}\n {error && <p className=\"text-sm text-destructive\">{error.message}</p>}\n {bulkResult?.bulkUpdateConversations && (\n <div className=\"rounded-lg border border-border bg-muted p-3 text-sm text-muted-foreground\">\n {bulkResult.bulkUpdateConversations.successCount} succeeded,{\" \"}\n {bulkResult.bulkUpdateConversations.failureCount} failed.\n </div>\n )}\n <div className=\"overflow-hidden rounded-lg border border-border bg-card\">\n {conversations.map((conversation) => (\n <label\n key={conversation.id}\n className=\"flex cursor-pointer items-center gap-3 border-b border-border px-4 py-3 last:border-b-0 hover:bg-accent\"\n >\n <input\n type=\"checkbox\"\n aria-label={`Select ${conversation.title || \"untitled conversation\"}`}\n checked={selectedIds.has(conversation.id)}\n onChange={() => toggle(conversation.id)}\n />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-foreground\">\n {conversation.title || \"Untitled conversation\"}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {conversation.type} · {conversation.state} · Updated{\" \"}\n {formatDate(conversation.updatedAt)}\n </p>\n </div>\n </label>\n ))}\n </div>\n </PageLayout>\n );\n}\n\nexport function ConversationAuditLogPage() {\n const tr = useTr();\n const { workspaceId } = useConversations();\n const { data, loading, error } = useQuery<{\n getActivities: ActivityLogNode[];\n }>(GET_CONVERSATION_AUDIT_LOG, {\n variables: {\n filter: { workspaceId, resourceType: \"conversation\", limit: 50 },\n },\n skip: !workspaceId,\n });\n const logs = data?.getActivities ?? [];\n\n return (\n <PageLayout\n title={tr(\"conversations.admin.auditLogPageTitle\", \"Audit log\")}\n subtitle={tr(\n \"conversations.admin.auditLogPageDescription\",\n \"Activity stream for admin and moderator actions on workspace conversations.\",\n )}\n icon={<Activity className=\"size-5\" aria-hidden=\"true\" />}\n maxWidth=\"2xl\"\n >\n {loading && (\n <p className=\"text-sm text-muted-foreground\">Loading activity…</p>\n )}\n {error && <p className=\"text-sm text-destructive\">{error.message}</p>}\n {!loading && !error && logs.length === 0 && (\n <EmptyState\n title=\"No activity found\"\n description=\"Conversation admin actions will appear here.\"\n />\n )}\n <div className=\"grid gap-3\">\n {logs.map((log) => (\n <article\n key={log.id}\n className=\"rounded-lg border border-border bg-card p-4\"\n >\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <p className=\"text-sm font-medium text-foreground\">\n {log.action}\n </p>\n <span className=\"text-xs text-muted-foreground\">\n {formatDate(log.timestamp)}\n </span>\n </div>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {log.description || \"No description provided\"}\n </p>\n <p className=\"mt-2 text-xs text-muted-foreground\">\n User {log.userId} · Resource {log.resourceId ?? \"workspace\"}\n </p>\n </article>\n ))}\n </div>\n </PageLayout>\n );\n}\n\nexport function AppInstallationsPage() {\n const tr = useTr();\n const { workspaceId } = useConversations();\n const [appId, setAppId] = useState(\"\");\n const [settingsInput, setSettingsInput] = useState(\"{}\");\n const [settingsDrafts, setSettingsDrafts] = useState<Record<string, string>>(\n {},\n );\n const [formError, setFormError] = useState<string | null>(null);\n const { data, loading, error, refetch } = useQuery<{\n appInstallations: AppInstallationNode[];\n }>(GET_APP_INSTALLATIONS, {\n variables: { workspaceId },\n skip: !workspaceId,\n });\n const [installApp, { loading: installing }] = useMutation(INSTALL_APP, {\n onCompleted: () => void refetch(),\n });\n const [updateSettings, { loading: saving }] = useMutation(\n UPDATE_APP_SETTINGS,\n { onCompleted: () => void refetch() },\n );\n const [uninstallApp, { loading: uninstalling }] = useMutation(UNINSTALL_APP, {\n onCompleted: () => void refetch(),\n });\n const installations = useMemo(\n () => data?.appInstallations ?? [],\n [data?.appInstallations],\n );\n\n useEffect(() => {\n setSettingsDrafts((current) => {\n if (installations.every((installation) => current[installation.id])) {\n return current;\n }\n const next = { ...current };\n for (const installation of installations) {\n if (!next[installation.id]) {\n next[installation.id] = JSON.stringify(\n installation.settings ?? {},\n null,\n 2,\n );\n }\n }\n return next;\n });\n }, [installations]);\n\n const parsedSettings = (raw: string): Record<string, unknown> | null => {\n try {\n const parsed = JSON.parse(raw) as unknown;\n return parsed && typeof parsed === \"object\" && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : null;\n } catch {\n return null;\n }\n };\n\n const handleInstall = () => {\n const settings = parsedSettings(settingsInput);\n if (!appId.trim() || !settings) {\n setFormError(\"Enter an app ID and a valid JSON settings object.\");\n return;\n }\n setFormError(null);\n void installApp({\n variables: { input: { appId: appId.trim(), workspaceId, settings } },\n });\n };\n\n return (\n <PageLayout\n title={tr(\n \"conversations.admin.appInstallationsPageTitle\",\n \"App installations\",\n )}\n subtitle={tr(\n \"conversations.admin.appInstallationsPageDescription\",\n \"Install, configure, and remove conversation apps and integrations.\",\n )}\n icon={<Package className=\"size-5\" aria-hidden=\"true\" />}\n maxWidth=\"2xl\"\n >\n <PageSection\n title=\"Install app\"\n description=\"Install a workspace-scoped conversation app by app ID.\"\n >\n <div className=\"grid gap-3 rounded-lg border border-border bg-card p-4 sm:grid-cols-[minmax(0,1fr)_minmax(0,2fr)_auto]\">\n <input\n value={appId}\n onChange={(event) => setAppId(event.target.value)}\n placeholder=\"app-id\"\n aria-label=\"App ID\"\n className=\"rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground\"\n />\n <textarea\n value={settingsInput}\n onChange={(event) => setSettingsInput(event.target.value)}\n rows={3}\n aria-label=\"App settings JSON\"\n className=\"rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground\"\n />\n <button\n type=\"button\"\n disabled={installing}\n className=\"rounded-md bg-primary px-3 py-2 text-sm text-primary-foreground hover:bg-primary/90 disabled:opacity-50\"\n onClick={handleInstall}\n >\n Install\n </button>\n </div>\n {formError && <p className=\"text-sm text-destructive\">{formError}</p>}\n </PageSection>\n {loading && (\n <p className=\"text-sm text-muted-foreground\">\n Loading app installations…\n </p>\n )}\n {error && <p className=\"text-sm text-destructive\">{error.message}</p>}\n <div className=\"grid gap-3\">\n {installations.map((installation) => (\n <article\n key={installation.id}\n className=\"rounded-lg border border-border bg-card p-4\"\n >\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <div className=\"flex items-center gap-2\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n {installation.appId}\n </h3>\n <StatusPill>{installation.state}</StatusPill>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n Version {installation.appVersion} · Installed by{\" \"}\n {installation.installedBy} on{\" \"}\n {formatDate(installation.installedAt)}\n </p>\n </div>\n <button\n type=\"button\"\n disabled={uninstalling}\n className=\"inline-flex items-center gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground hover:bg-accent disabled:opacity-50\"\n onClick={() =>\n void uninstallApp({ variables: { id: installation.id } })\n }\n >\n <Trash2 className=\"size-4\" aria-hidden=\"true\" />\n Uninstall\n </button>\n </div>\n <div className=\"mt-3 grid gap-2\">\n <textarea\n rows={5}\n aria-label={`Settings for ${installation.appId}`}\n value={settingsDrafts[installation.id] ?? \"{}\"}\n onChange={(event) =>\n setSettingsDrafts((current) => ({\n ...current,\n [installation.id]: event.target.value,\n }))\n }\n className=\"rounded-md border border-border bg-background px-3 py-2 font-mono text-xs text-foreground\"\n />\n <button\n type=\"button\"\n disabled={saving}\n className=\"w-fit rounded-md bg-secondary px-3 py-2 text-sm text-secondary-foreground hover:bg-secondary/90 disabled:opacity-50\"\n onClick={() => {\n const settings = parsedSettings(\n settingsDrafts[installation.id] ?? \"{}\",\n );\n if (!settings) {\n setFormError(\n \"Installation settings must be a valid JSON object.\",\n );\n return;\n }\n setFormError(null);\n void updateSettings({\n variables: { id: installation.id, settings },\n });\n }}\n >\n Save settings\n </button>\n </div>\n </article>\n ))}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ConversationAdminPages.js","names":[],"sources":["../../../src/conversations/pages/ConversationAdminPages.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from \"react\";\nimport { gql } from \"@apollo/client\";\nimport { useMutation, useQuery } from \"@apollo/client/react\";\nimport {\n Activity,\n Archive,\n CheckCircle2,\n Package,\n ShieldCheck,\n Trash2,\n} from \"lucide-react\";\n\nimport { EmptyState, PageLayout, PageSection } from \"../components/PageLayout\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useTr } from \"../utils/i18n\";\n\nconst GET_MESSAGE_REPORTS = gql`\n query ConversationAdminMessageReports(\n $first: Int\n $filter: MessageReportFilter\n ) {\n messageReports(first: $first, filter: $filter) {\n edges {\n node {\n id\n messageId\n reportedBy\n reason\n status\n notes\n createdAt\n reviewedAt\n message {\n id\n conversationId\n authorId\n content\n isHidden\n createdAt\n }\n }\n }\n }\n }\n`;\n\nconst RESOLVE_MESSAGE_REPORT = gql`\n mutation ConversationAdminResolveMessageReport(\n $input: ResolveMessageReportInput!\n ) {\n resolveMessageReport(input: $input) {\n id\n status\n reviewedAt\n notes\n message {\n id\n isHidden\n }\n }\n }\n`;\n\nconst GET_BULK_CONVERSATIONS = gql`\n query ConversationAdminBulkConversations(\n $first: Int\n $filter: ConversationFilter\n ) {\n conversations(first: $first, filter: $filter) {\n edges {\n node {\n id\n title\n type\n state\n updatedAt\n }\n }\n }\n }\n`;\n\nconst BULK_UPDATE_CONVERSATIONS = gql`\n mutation ConversationAdminBulkUpdate(\n $input: BulkConversationOperationInput!\n ) {\n bulkUpdateConversations(input: $input) {\n success\n processedCount\n successCount\n failureCount\n conversationIds\n errors {\n conversationId\n message\n }\n }\n }\n`;\n\nconst GET_CONVERSATION_AUDIT_LOG = gql`\n query ConversationAdminAuditLog($filter: ActivityLogFilter) {\n getActivities(filter: $filter) {\n id\n action\n description\n resourceId\n resourceType\n userId\n timestamp\n }\n }\n`;\n\nconst GET_APP_INSTALLATIONS = gql`\n query ConversationAdminAppInstallations($workspaceId: ID) {\n appInstallations(workspaceId: $workspaceId) {\n id\n appId\n appVersion\n state\n settings\n permissions\n installedBy\n installedAt\n updatedAt\n }\n }\n`;\n\nconst INSTALL_APP = gql`\n mutation ConversationAdminInstallApp($input: InstallAppInput!) {\n installApp(input: $input) {\n id\n appId\n state\n settings\n installedAt\n }\n }\n`;\n\nconst UPDATE_APP_SETTINGS = gql`\n mutation ConversationAdminUpdateAppSettings($id: ID!, $settings: JSON!) {\n updateAppSettings(id: $id, settings: $settings) {\n id\n settings\n updatedAt\n }\n }\n`;\n\nconst UNINSTALL_APP = gql`\n mutation ConversationAdminUninstallApp($id: ID!) {\n uninstallApp(id: $id) {\n appInstallationId\n success\n }\n }\n`;\n\ninterface MessageReportNode {\n id: string;\n messageId: string;\n reportedBy: string;\n reason: string;\n status: string;\n notes?: string | null;\n createdAt: string;\n reviewedAt?: string | null;\n message: {\n id: string;\n conversationId: string;\n authorId: string;\n content: unknown;\n isHidden: boolean;\n createdAt: string;\n };\n}\n\ninterface ConversationNode {\n id: string;\n title?: string | null;\n type: string;\n state: string;\n updatedAt: string;\n}\n\ninterface ActivityLogNode {\n id: string;\n action: string;\n description?: string | null;\n resourceId?: string | null;\n resourceType?: string | null;\n userId: string;\n timestamp: string;\n}\n\ninterface AppInstallationNode {\n id: string;\n appId: string;\n appVersion: string;\n state: string;\n settings: unknown;\n permissions: string[];\n installedBy: string;\n installedAt: string;\n updatedAt: string;\n}\n\ninterface BulkUpdateConversationsResult {\n bulkUpdateConversations: {\n success: boolean;\n processedCount: number;\n successCount: number;\n failureCount: number;\n conversationIds: string[];\n errors: Array<{ conversationId: string; message: string }>;\n };\n}\n\nfunction formatContent(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (content && typeof content === \"object\" && \"text\" in content) {\n const text = (content as { text?: unknown }).text;\n if (typeof text === \"string\") return text;\n }\n try {\n return JSON.stringify(content);\n } catch {\n return \"Message content unavailable\";\n }\n}\n\nconst DATE_FORMATTER = new Intl.DateTimeFormat(undefined, {\n dateStyle: \"medium\",\n timeStyle: \"short\",\n});\n\nfunction formatDate(value: string): string {\n return DATE_FORMATTER.format(new Date(value));\n}\n\nfunction StatusPill({ children }: { children: string }) {\n return (\n <span className=\"rounded-full border border-border bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground\">\n {children}\n </span>\n );\n}\n\nexport function ModerationQueuePage() {\n const tr = useTr();\n const { data, loading, error, refetch } = useQuery<{\n messageReports: { edges: Array<{ node: MessageReportNode }> };\n }>(GET_MESSAGE_REPORTS, {\n variables: { first: 50, filter: { statuses: [\"PENDING\", \"REVIEWING\"] } },\n });\n const [resolveReport, { loading: resolving }] = useMutation(\n RESOLVE_MESSAGE_REPORT,\n {\n onCompleted: () => void refetch(),\n },\n );\n\n const reports = data?.messageReports.edges.map((edge) => edge.node) ?? [];\n\n return (\n <PageLayout\n title={tr(\"conversations.admin.moderationPageTitle\", \"Moderation queue\")}\n subtitle={tr(\n \"conversations.admin.moderationPageDescription\",\n \"Review reported messages and apply moderator decisions.\",\n )}\n icon={<ShieldCheck className=\"size-5\" aria-hidden=\"true\" />}\n maxWidth=\"2xl\"\n >\n {loading && (\n <p className=\"text-sm text-muted-foreground\">Loading reports…</p>\n )}\n {error && <p className=\"text-sm text-destructive\">{error.message}</p>}\n {!loading && !error && reports.length === 0 && (\n <EmptyState\n title=\"No reports waiting for review\"\n description=\"Reported messages will appear here as they are submitted.\"\n icon={<CheckCircle2 className=\"size-6\" aria-hidden=\"true\" />}\n />\n )}\n <div className=\"grid gap-3\">\n {reports.map((report) => (\n <article\n key={report.id}\n className=\"rounded-lg border border-border bg-card p-4 shadow-sm\"\n >\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-2\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusPill>{report.status}</StatusPill>\n {report.message.isHidden && <StatusPill>Hidden</StatusPill>}\n <span className=\"text-xs text-muted-foreground\">\n Reported {formatDate(report.createdAt)}\n </span>\n </div>\n <p className=\"text-sm font-medium text-foreground\">\n {report.reason}\n </p>\n <p className=\"rounded-md bg-muted p-3 text-sm text-muted-foreground\">\n {formatContent(report.message.content)}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n Message {report.messageId} in conversation{\" \"}\n {report.message.conversationId} by {report.message.authorId};\n reported by {report.reportedBy}\n </p>\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n disabled={resolving}\n className=\"rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground hover:bg-accent disabled:opacity-50\"\n onClick={() =>\n void resolveReport({\n variables: {\n input: { reportId: report.id, action: \"DISMISS\" },\n },\n })\n }\n >\n Dismiss\n </button>\n <button\n type=\"button\"\n disabled={resolving}\n className=\"rounded-md bg-primary px-3 py-2 text-sm text-primary-foreground hover:bg-primary/90 disabled:opacity-50\"\n onClick={() =>\n void resolveReport({\n variables: {\n input: { reportId: report.id, action: \"HIDE_MESSAGE\" },\n },\n })\n }\n >\n Hide and resolve\n </button>\n </div>\n </div>\n </article>\n ))}\n </div>\n </PageLayout>\n );\n}\n\nexport function BulkOperationsPage() {\n const tr = useTr();\n const [selectedIds, setSelectedIds] = useState<Set<string>>(() => new Set());\n const [action, setAction] = useState<\"ARCHIVE\" | \"UNARCHIVE\" | \"DELETE\">(\n \"ARCHIVE\",\n );\n const { data, loading, error, refetch } = useQuery<{\n conversations: { edges: Array<{ node: ConversationNode }> };\n }>(GET_BULK_CONVERSATIONS, { variables: { first: 100 } });\n const [bulkUpdate, { data: bulkResult, loading: updating }] = useMutation<\n BulkUpdateConversationsResult,\n { input: { conversationIds: string[]; action: typeof action } }\n >(BULK_UPDATE_CONVERSATIONS, {\n onCompleted: () => {\n setSelectedIds(new Set());\n void refetch();\n },\n });\n const conversations =\n data?.conversations.edges.map((edge) => edge.node) ?? [];\n\n const toggle = (id: string) => {\n setSelectedIds((current) => {\n const next = new Set(current);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n };\n\n return (\n <PageLayout\n title={tr(\n \"conversations.admin.bulkOperationsPageTitle\",\n \"Bulk operations\",\n )}\n subtitle={tr(\n \"conversations.admin.bulkOperationsPageDescription\",\n \"Select workspace conversations and apply archive, unarchive, or delete operations.\",\n )}\n icon={<Archive className=\"size-5\" aria-hidden=\"true\" />}\n maxWidth=\"2xl\"\n headerActions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <select\n value={action}\n onChange={(event) => setAction(event.target.value as typeof action)}\n className=\"rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground\"\n >\n <option value=\"ARCHIVE\">Archive</option>\n <option value=\"UNARCHIVE\">Unarchive</option>\n <option value=\"DELETE\">Delete</option>\n </select>\n <button\n type=\"button\"\n disabled={selectedIds.size === 0 || updating}\n className=\"rounded-md bg-primary px-3 py-2 text-sm text-primary-foreground hover:bg-primary/90 disabled:opacity-50\"\n onClick={() =>\n void bulkUpdate({\n variables: {\n input: { conversationIds: Array.from(selectedIds), action },\n },\n })\n }\n >\n Apply to {selectedIds.size}\n </button>\n </div>\n }\n >\n {loading && (\n <p className=\"text-sm text-muted-foreground\">Loading conversations…</p>\n )}\n {error && <p className=\"text-sm text-destructive\">{error.message}</p>}\n {bulkResult?.bulkUpdateConversations && (\n <div className=\"rounded-lg border border-border bg-muted p-3 text-sm text-muted-foreground\">\n {bulkResult.bulkUpdateConversations.successCount} succeeded,{\" \"}\n {bulkResult.bulkUpdateConversations.failureCount} failed.\n </div>\n )}\n <div className=\"overflow-hidden rounded-lg border border-border bg-card\">\n {conversations.map((conversation) => (\n <label\n key={conversation.id}\n className=\"flex cursor-pointer items-center gap-3 border-b border-border px-4 py-3 last:border-b-0 hover:bg-accent\"\n >\n <input\n type=\"checkbox\"\n aria-label={`Select ${conversation.title || \"untitled conversation\"}`}\n checked={selectedIds.has(conversation.id)}\n onChange={() => toggle(conversation.id)}\n />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-foreground\">\n {conversation.title || \"Untitled conversation\"}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {conversation.type} · {conversation.state} · Updated{\" \"}\n {formatDate(conversation.updatedAt)}\n </p>\n </div>\n </label>\n ))}\n </div>\n </PageLayout>\n );\n}\n\nexport function ConversationAuditLogPage() {\n const tr = useTr();\n const { workspaceId } = useConversations();\n const { data, loading, error } = useQuery<{\n getActivities: ActivityLogNode[];\n }>(GET_CONVERSATION_AUDIT_LOG, {\n variables: {\n filter: { workspaceId, resourceType: \"conversation\", limit: 50 },\n },\n skip: !workspaceId,\n });\n const logs = data?.getActivities ?? [];\n\n return (\n <PageLayout\n title={tr(\"conversations.admin.auditLogPageTitle\", \"Audit log\")}\n subtitle={tr(\n \"conversations.admin.auditLogPageDescription\",\n \"Activity stream for admin and moderator actions on workspace conversations.\",\n )}\n icon={<Activity className=\"size-5\" aria-hidden=\"true\" />}\n maxWidth=\"2xl\"\n >\n {loading && (\n <p className=\"text-sm text-muted-foreground\">Loading activity…</p>\n )}\n {error && <p className=\"text-sm text-destructive\">{error.message}</p>}\n {!loading && !error && logs.length === 0 && (\n <EmptyState\n title=\"No activity found\"\n description=\"Conversation admin actions will appear here.\"\n />\n )}\n <div className=\"grid gap-3\">\n {logs.map((log) => (\n <article\n key={log.id}\n className=\"rounded-lg border border-border bg-card p-4\"\n >\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <p className=\"text-sm font-medium text-foreground\">\n {log.action}\n </p>\n <span className=\"text-xs text-muted-foreground\">\n {formatDate(log.timestamp)}\n </span>\n </div>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {log.description || \"No description provided\"}\n </p>\n <p className=\"mt-2 text-xs text-muted-foreground\">\n User {log.userId} · Resource {log.resourceId ?? \"workspace\"}\n </p>\n </article>\n ))}\n </div>\n </PageLayout>\n );\n}\n\nexport function AppInstallationsPage() {\n const tr = useTr();\n const { workspaceId } = useConversations();\n const [appId, setAppId] = useState(\"\");\n const [settingsInput, setSettingsInput] = useState(\"{}\");\n const [settingsDrafts, setSettingsDrafts] = useState<Record<string, string>>(\n {},\n );\n const [formError, setFormError] = useState<string | null>(null);\n const { data, loading, error, refetch } = useQuery<{\n appInstallations: AppInstallationNode[];\n }>(GET_APP_INSTALLATIONS, {\n variables: { workspaceId },\n skip: !workspaceId,\n });\n const [installApp, { loading: installing }] = useMutation(INSTALL_APP, {\n onCompleted: () => void refetch(),\n });\n const [updateSettings, { loading: saving }] = useMutation(\n UPDATE_APP_SETTINGS,\n { onCompleted: () => void refetch() },\n );\n const [uninstallApp, { loading: uninstalling }] = useMutation(UNINSTALL_APP, {\n onCompleted: () => void refetch(),\n });\n const installations = useMemo(\n () => data?.appInstallations ?? [],\n [data?.appInstallations],\n );\n\n useEffect(() => {\n setSettingsDrafts((current) => {\n if (installations.every((installation) => current[installation.id])) {\n return current;\n }\n const next = { ...current };\n for (const installation of installations) {\n if (!next[installation.id]) {\n next[installation.id] = JSON.stringify(\n installation.settings ?? {},\n null,\n 2,\n );\n }\n }\n return next;\n });\n }, [installations]);\n\n const parsedSettings = (raw: string): Record<string, unknown> | null => {\n try {\n const parsed = JSON.parse(raw) as unknown;\n return parsed && typeof parsed === \"object\" && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : null;\n } catch {\n return null;\n }\n };\n\n const handleInstall = () => {\n const settings = parsedSettings(settingsInput);\n if (!appId.trim() || !settings) {\n setFormError(\"Enter an app ID and a valid JSON settings object.\");\n return;\n }\n setFormError(null);\n void installApp({\n variables: { input: { appId: appId.trim(), workspaceId, settings } },\n });\n };\n\n return (\n <PageLayout\n title={tr(\n \"conversations.admin.appInstallationsPageTitle\",\n \"App installations\",\n )}\n subtitle={tr(\n \"conversations.admin.appInstallationsPageDescription\",\n \"Install, configure, and remove conversation apps and integrations.\",\n )}\n icon={<Package className=\"size-5\" aria-hidden=\"true\" />}\n maxWidth=\"2xl\"\n >\n <PageSection\n title=\"Install app\"\n description=\"Install a workspace-scoped conversation app by app ID.\"\n >\n <div className=\"grid gap-3 rounded-lg border border-border bg-card p-4 sm:grid-cols-[minmax(0,1fr)_minmax(0,2fr)_auto]\">\n <input\n value={appId}\n onChange={(event) => setAppId(event.target.value)}\n placeholder=\"app-id\"\n aria-label=\"App ID\"\n className=\"rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground\"\n />\n <textarea\n value={settingsInput}\n onChange={(event) => setSettingsInput(event.target.value)}\n rows={3}\n aria-label=\"App settings JSON\"\n className=\"rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground\"\n />\n <button\n type=\"button\"\n disabled={installing}\n className=\"rounded-md bg-primary px-3 py-2 text-sm text-primary-foreground hover:bg-primary/90 disabled:opacity-50\"\n onClick={handleInstall}\n >\n Install\n </button>\n </div>\n {formError && <p className=\"text-sm text-destructive\">{formError}</p>}\n </PageSection>\n {loading && (\n <p className=\"text-sm text-muted-foreground\">\n Loading app installations…\n </p>\n )}\n {error && <p className=\"text-sm text-destructive\">{error.message}</p>}\n <div className=\"grid gap-3\">\n {installations.map((installation) => (\n <article\n key={installation.id}\n className=\"rounded-lg border border-border bg-card p-4\"\n >\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <div className=\"flex items-center gap-2\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n {installation.appId}\n </h3>\n <StatusPill>{installation.state}</StatusPill>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n Version {installation.appVersion} · Installed by{\" \"}\n {installation.installedBy} on{\" \"}\n {formatDate(installation.installedAt)}\n </p>\n </div>\n <button\n type=\"button\"\n disabled={uninstalling}\n className=\"inline-flex items-center gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground hover:bg-accent disabled:opacity-50\"\n onClick={() =>\n void uninstallApp({ variables: { id: installation.id } })\n }\n >\n <Trash2 className=\"size-4\" aria-hidden=\"true\" />\n Uninstall\n </button>\n </div>\n <div className=\"mt-3 grid gap-2\">\n <textarea\n rows={5}\n aria-label={`Settings for ${installation.appId}`}\n value={settingsDrafts[installation.id] ?? \"{}\"}\n onChange={(event) =>\n setSettingsDrafts((current) => ({\n ...current,\n [installation.id]: event.target.value,\n }))\n }\n className=\"rounded-md border border-border bg-background px-3 py-2 font-mono text-xs text-foreground\"\n />\n <button\n type=\"button\"\n disabled={saving}\n className=\"w-fit rounded-md bg-secondary px-3 py-2 text-sm text-secondary-foreground hover:bg-secondary/90 disabled:opacity-50\"\n onClick={() => {\n const settings = parsedSettings(\n settingsDrafts[installation.id] ?? \"{}\",\n );\n if (!settings) {\n setFormError(\n \"Installation settings must be a valid JSON object.\",\n );\n return;\n }\n setFormError(null);\n void updateSettings({\n variables: { id: installation.id, settings },\n });\n }}\n >\n Save settings\n </button>\n </div>\n </article>\n ))}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
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 = isSearchActive ? searchedConversations : baseConversations;\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
|
+
{"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 = isSearchActive\n ? searchedConversations\n : baseConversations;\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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"}
|
package/package.json
CHANGED