@burdenoff/microfe-conversations 2026.525.3 → 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.
Files changed (38) hide show
  1. package/dist/conversations/ConversationsUserRoutes.js +4 -3
  2. package/dist/conversations/ConversationsUserRoutes.js.map +1 -1
  3. package/dist/conversations/components/EmptyState.js +27 -0
  4. package/dist/conversations/components/EmptyState.js.map +1 -0
  5. package/dist/conversations/components/LazyPageBoundary.js +26 -42
  6. package/dist/conversations/components/LazyPageBoundary.js.map +1 -1
  7. package/dist/conversations/components/MessageBubble.js +3 -1
  8. package/dist/conversations/components/MessageBubble.js.map +1 -1
  9. package/dist/conversations/components/PageLayout.js +3 -39
  10. package/dist/conversations/components/PageLayout.js.map +1 -1
  11. package/dist/conversations/components/PageSection.js +25 -0
  12. package/dist/conversations/components/PageSection.js.map +1 -0
  13. package/dist/conversations/components/PermissionGuard.js +1 -1
  14. package/dist/conversations/components/PermissionGuard.js.map +1 -1
  15. package/dist/conversations/components/lazyRetry.js +23 -0
  16. package/dist/conversations/components/lazyRetry.js.map +1 -0
  17. package/dist/conversations/constants/permissions.js +0 -1
  18. package/dist/conversations/constants/permissions.js.map +1 -1
  19. package/dist/conversations/hooks/useMessageQueries.js.map +1 -1
  20. package/dist/conversations/hooks/useMessageSubscriptions.js.map +1 -1
  21. package/dist/conversations/pages/AdminConversationsLandingPage.js +6 -6
  22. package/dist/conversations/pages/AdminConversationsLandingPage.js.map +1 -1
  23. package/dist/conversations/pages/ConversationAdminPages.js +56 -49
  24. package/dist/conversations/pages/ConversationAdminPages.js.map +1 -1
  25. package/dist/conversations/pages/ConversationDetailPage.js +3 -2
  26. package/dist/conversations/pages/ConversationDetailPage.js.map +1 -1
  27. package/dist/conversations/pages/ConversationListPage.js +133 -136
  28. package/dist/conversations/pages/ConversationListPage.js.map +1 -1
  29. package/dist/conversations/pages/ConversationsLandingPage.js +50 -57
  30. package/dist/conversations/pages/ConversationsLandingPage.js.map +1 -1
  31. package/dist/conversations/pages/ConversationsSettingsPage.js +8 -5
  32. package/dist/conversations/pages/ConversationsSettingsPage.js.map +1 -1
  33. package/dist/conversations/pages/NewConversationPage.js +87 -88
  34. package/dist/conversations/pages/NewConversationPage.js.map +1 -1
  35. package/dist/conversations/providers/ConversationsProvider.js +1 -1
  36. package/dist/conversations/providers/ConversationsProvider.js.map +1 -1
  37. package/dist/conversations/utils/mappers.js.map +1 -1
  38. package/package.json +1 -1
@@ -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\nfunction formatDate(value: string): string {\n return new Intl.DateTimeFormat(undefined, {\n dateStyle: \"medium\",\n timeStyle: \"short\",\n }).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=\"h-5 w-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=\"h-6 w-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=\"h-5 w-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 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=\"h-5 w-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=\"h-5 w-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 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 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=\"h-4 w-4\" aria-hidden=\"true\" />\n Uninstall\n </button>\n </div>\n <div className=\"mt-3 grid gap-2\">\n <textarea\n rows={5}\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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"}
@@ -173,7 +173,7 @@ function _() {
173
173
  }) : !j || M ? /* @__PURE__ */ h("div", {
174
174
  className: "flex flex-col items-center justify-center h-[calc(100vh-64px)] text-center gap-3 px-4",
175
175
  children: [
176
- /* @__PURE__ */ m(pe, { className: "h-8 w-8 text-muted-foreground" }),
176
+ /* @__PURE__ */ m(pe, { className: "size-8 text-muted-foreground" }),
177
177
  /* @__PURE__ */ m("p", {
178
178
  className: "text-sm text-muted-foreground",
179
179
  children: M ? x("conversations.unavailable", "We couldn't load this conversation. Please try again.") : x("conversations.notFound", "Conversation not found")
@@ -306,6 +306,7 @@ function _() {
306
306
  }), /* @__PURE__ */ h("div", {
307
307
  className: "flex items-end gap-2",
308
308
  children: [/* @__PURE__ */ m("textarea", {
309
+ "aria-label": "Edit message",
309
310
  value: K,
310
311
  onChange: (e) => q(e.target.value),
311
312
  onKeyDown: (e) => {
@@ -418,7 +419,7 @@ function _() {
418
419
  children: Le.map((e) => /* @__PURE__ */ h("div", {
419
420
  className: "flex items-center gap-2.5 px-2 py-1.5 rounded-lg hover:bg-muted/50",
420
421
  children: [/* @__PURE__ */ m("div", {
421
- className: "w-7 h-7 rounded-full bg-muted flex items-center justify-center text-xs font-medium text-muted-foreground",
422
+ className: "size-7 rounded-full bg-muted flex items-center justify-center text-xs font-medium text-muted-foreground",
422
423
  children: e.name.split(" ").map((e) => e[0]).join("").slice(0, 2)
423
424
  }), /* @__PURE__ */ h("div", {
424
425
  className: "min-w-0",
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationDetailPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationDetailPage.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport { useNavigate, useParams } from \"react-router-dom\";\nimport {\n Archive,\n ArrowLeft,\n BellOff,\n Bell,\n ExternalLink,\n Hash,\n Loader2,\n MessageCircle,\n MoreVertical,\n Pin,\n PinOff,\n SendHorizontal,\n Users,\n X,\n} from \"lucide-react\";\nimport { MessageBubble } from \"../components/MessageBubble\";\nimport {\n useGetConversation,\n useConversationActions,\n} from \"../hooks/useConversationQueries\";\nimport {\n useGetMessages,\n useSendMessage,\n useEditMessage,\n useMessageActions,\n} from \"../hooks/useMessageQueries\";\nimport { useMessageStreamForConversation } from \"../hooks/useMessageSubscriptions\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport { cn } from \"../utils/cn\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { nativeImpact, nativeNotify } from \"../../utils/nativeBridge\";\nimport type { Message } from \"../types\";\n\nconst MESSAGES_PAGE_SIZE = 100;\n\nexport function ConversationDetailPage() {\n const { conversationId } = useParams<{ conversationId: string }>();\n const navigate = useNavigate();\n const { basePath, currentUser } = useConversations();\n const bus = useEventBus();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const {\n clearMessageDraft,\n isParticipantsPanelOpen,\n messageDrafts,\n setMessageDraft,\n toggleParticipantsPanel,\n } = useConversationsStore();\n\n const [messageInput, setMessageInput] = useState(\n messageDrafts[conversationId ?? \"\"] ?? \"\",\n );\n const [locallySentMessages, setLocallySentMessages] = useState<Message[]>([]);\n const messagesEndRef = useRef<HTMLDivElement>(null);\n\n const {\n conversation,\n loading: conversationLoading,\n error: conversationError,\n } = useGetConversation(conversationId ?? \"\");\n const {\n messages,\n loading: messagesLoading,\n error: messagesError,\n } = useGetMessages(conversationId ?? \"\", {\n first: MESSAGES_PAGE_SIZE,\n });\n // Subscribe to live message events so the list updates without polling.\n // Apollo cache merges by id; the hook handles append / evict explicitly.\n useMessageStreamForConversation(conversationId);\n const { sendMessage, loading: sendingMessage } = useSendMessage();\n const { editMessage, loading: editingMessage } = useEditMessage();\n const {\n deleteMessage,\n addReaction,\n removeReaction,\n pinMessage,\n unpinMessage,\n } = useMessageActions();\n const {\n archiveConversation,\n pinConversation,\n unpinConversation,\n muteConversation,\n unmuteConversation,\n } = useConversationActions();\n\n const [moreMenuOpen, setMoreMenuOpen] = useState(false);\n const [editingMessageId, setEditingMessageId] = useState<string | null>(null);\n const [editingContent, setEditingContent] = useState(\"\");\n const [replyingTo, setReplyingTo] = useState<{\n id: string;\n content: string;\n sender: string;\n } | null>(null);\n const moreMenuRef = useRef<HTMLDivElement>(null);\n\n const currentUserId = currentUser?.id ?? \"\";\n const sourceContext = conversation?.sourceContext;\n\n const displayedMessages = useMemo(() => {\n const byId = new Map<string, Message>();\n for (const message of locallySentMessages) {\n if (message.conversationId !== conversationId) continue;\n byId.set(message.id, message);\n }\n for (const message of messages) {\n byId.set(message.id, message);\n }\n return Array.from(byId.values()).sort(\n (a, b) =>\n new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),\n );\n }, [conversationId, locallySentMessages, messages]);\n\n useEffect(() => {\n messagesEndRef.current?.scrollIntoView({ behavior: \"smooth\" });\n }, [displayedMessages.length]);\n\n useEffect(() => {\n setLocallySentMessages([]);\n }, [conversationId]);\n\n useEffect(() => {\n if (locallySentMessages.length === 0 || messages.length === 0) return;\n const serverIds = new Set(messages.map((message) => message.id));\n setLocallySentMessages((current) => {\n const pending = current.filter((message) => !serverIds.has(message.id));\n return pending.length === current.length ? current : pending;\n });\n }, [locallySentMessages.length, messages]);\n\n useEffect(() => {\n if (!conversationId) return;\n if (messageInput.trim()) {\n setMessageDraft(conversationId, messageInput);\n return;\n }\n clearMessageDraft(conversationId);\n }, [messageInput, conversationId, setMessageDraft, clearMessageDraft]);\n\n const participants = useMemo(() => {\n const byAuthor = new Map<string, { id: string; name: string }>();\n for (const message of displayedMessages) {\n if (!message.authorId || byAuthor.has(message.authorId)) continue;\n byAuthor.set(message.authorId, {\n id: message.authorId,\n name: message.sender.name || message.authorId,\n });\n }\n return Array.from(byAuthor.values());\n }, [displayedMessages]);\n\n const handleSendMessage = useCallback(async () => {\n if (!conversationId || !messageInput.trim() || sendingMessage) return;\n const message = await sendMessage({\n conversationId,\n content: messageInput.trim(),\n type: \"TEXT\",\n parentMessageId: replyingTo?.id,\n refetchFirst: MESSAGES_PAGE_SIZE,\n });\n if (!message) return;\n setLocallySentMessages((current) => [...current, message]);\n bus.emit(\"conversations.message.sent\", {\n messageId: message.id,\n conversationId,\n });\n setMessageInput(\"\");\n setReplyingTo(null);\n clearMessageDraft(conversationId);\n }, [\n bus,\n conversationId,\n messageInput,\n sendingMessage,\n sendMessage,\n clearMessageDraft,\n replyingTo,\n ]);\n\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (event.key !== \"Enter\" || event.shiftKey) return;\n event.preventDefault();\n void handleSendMessage();\n },\n [handleSendMessage],\n );\n\n // Emit opened event so sinks (analytics/audit) can record the view\n useEffect(() => {\n if (!conversationId) return;\n bus.emit(\"conversations.conversation.opened\", { conversationId });\n }, [bus, conversationId]);\n\n // Close more-menu on outside click\n useEffect(() => {\n if (!moreMenuOpen) return;\n const handler = (e: MouseEvent) => {\n if (\n moreMenuRef.current &&\n !moreMenuRef.current.contains(e.target as Node)\n ) {\n setMoreMenuOpen(false);\n }\n };\n document.addEventListener(\"mousedown\", handler);\n return () => document.removeEventListener(\"mousedown\", handler);\n }, [moreMenuOpen]);\n\n const handleReply = useCallback(\n (message: { id: string; content: string; sender: { name: string } }) => {\n setReplyingTo({\n id: message.id,\n content: message.content,\n sender: message.sender.name,\n });\n },\n [],\n );\n\n const handleReact = useCallback(\n async (messageId: string, emoji: string) => {\n try {\n await addReaction(messageId, emoji);\n } catch {\n // If already reacted, toggle off\n await removeReaction(messageId, emoji).catch(() => {});\n }\n },\n [addReaction, removeReaction],\n );\n\n const handleEdit = useCallback((message: { id: string; content: string }) => {\n setEditingMessageId(message.id);\n setEditingContent(message.content);\n }, []);\n\n const handleEditSubmit = useCallback(async () => {\n if (!editingMessageId || !editingContent.trim() || editingMessage) return;\n await editMessage(editingMessageId, editingContent.trim());\n setEditingMessageId(null);\n setEditingContent(\"\");\n }, [editingMessageId, editingContent, editingMessage, editMessage]);\n\n const handleEditCancel = useCallback(() => {\n setEditingMessageId(null);\n setEditingContent(\"\");\n }, []);\n\n const handleDelete = useCallback(\n async (messageId: string) => {\n nativeImpact(\"medium\");\n try {\n await deleteMessage(messageId);\n nativeNotify(\"success\");\n } catch (error) {\n nativeNotify(\"error\");\n throw error;\n }\n },\n [deleteMessage],\n );\n\n const handleTogglePinMessage = useCallback(\n async (messageId: string, isPinned: boolean) => {\n if (isPinned) {\n await unpinMessage(messageId);\n return;\n }\n await pinMessage(messageId);\n },\n [pinMessage, unpinMessage],\n );\n\n const handlePin = useCallback(async () => {\n if (!conversationId) return;\n if (conversation?.isPinned) {\n await unpinConversation(conversationId);\n } else {\n await pinConversation(conversationId);\n }\n setMoreMenuOpen(false);\n }, [\n conversationId,\n conversation?.isPinned,\n pinConversation,\n unpinConversation,\n ]);\n\n const handleMute = useCallback(async () => {\n if (!conversationId) return;\n if (conversation?.isMuted) {\n await unmuteConversation(conversationId);\n } else {\n await muteConversation(conversationId);\n }\n setMoreMenuOpen(false);\n }, [\n conversationId,\n conversation?.isMuted,\n muteConversation,\n unmuteConversation,\n ]);\n\n const handleArchive = useCallback(async () => {\n if (!conversationId) return;\n await archiveConversation(conversationId);\n bus.emit(\"conversations.conversation.archived\", { conversationId });\n setMoreMenuOpen(false);\n navigate(basePath || \"/conversations\");\n }, [archiveConversation, basePath, bus, conversationId, navigate]);\n\n const handleOpenContext = useCallback(() => {\n if (!sourceContext?.contextHref) return;\n try {\n const url = new URL(sourceContext.contextHref);\n if (url.origin === window.location.origin) {\n navigate(`${url.pathname}${url.search}${url.hash}`);\n return;\n }\n } catch {\n // Fall through to full navigation for relative or invalid URLs.\n }\n\n window.location.assign(sourceContext.contextHref);\n }, [navigate, sourceContext?.contextHref]);\n\n if (!conversation && conversationLoading) {\n return (\n <div className=\"flex items-center justify-center h-[calc(100vh-64px)] text-sm text-muted-foreground\">\n {tr(\"conversations.loading\", \"Loading conversation...\")}\n </div>\n );\n }\n\n if (!conversation || conversationError) {\n return (\n <div className=\"flex flex-col items-center justify-center h-[calc(100vh-64px)] text-center gap-3 px-4\">\n <MessageCircle className=\"h-8 w-8 text-muted-foreground\" />\n <p className=\"text-sm text-muted-foreground\">\n {conversationError\n ? tr(\n \"conversations.unavailable\",\n \"We couldn't load this conversation. Please try again.\",\n )\n : tr(\"conversations.notFound\", \"Conversation not found\")}\n </p>\n <button\n type=\"button\"\n onClick={() => navigate(basePath || \"/conversations\")}\n className=\"px-3 py-2 text-sm font-medium rounded-lg border border-border hover:bg-muted/50\"\n >\n {tr(\"conversations.back\", \"Back to conversations\")}\n </button>\n </div>\n );\n }\n\n return (\n <div className=\"relative flex h-[calc(100vh-64px)] bg-background\">\n <div className=\"flex-1 flex flex-col min-w-0\">\n <header\n data-tour=\"conversation-detail-header\"\n className=\"flex items-center justify-between px-4 py-3 border-b border-border bg-background/95 backdrop-blur-sm\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <button\n type=\"button\"\n onClick={() => navigate(basePath || \"/conversations\")}\n className=\"flex min-h-10 min-w-10 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground\"\n >\n <ArrowLeft size={18} />\n </button>\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-1.5\">\n {conversation.type === \"CHANNEL\" && (\n <Hash\n size={14}\n className=\"text-muted-foreground flex-shrink-0\"\n />\n )}\n <h2 className=\"text-sm font-semibold text-foreground truncate\">\n {conversation.title}\n </h2>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {conversation.type === \"DIRECT\"\n ? tr(\n \"conversations.directConversation\",\n \"Direct conversation\",\n )\n : conversation.type === \"CHANNEL\"\n ? `${tr(\"conversations.channel\", \"Channel\")} · ${conversation.memberCount} ${tr(\"conversations.membersCountLabel\", \"members\")}`\n : conversation.type === \"ANNOUNCEMENT\"\n ? `${tr(\"conversations.announcement\", \"Announcement\")} · ${conversation.memberCount} ${tr(\"conversations.membersCountLabel\", \"members\")}`\n : `${conversation.memberCount} ${tr(\"conversations.membersCountLabel\", \"members\")}`}\n </p>\n </div>\n </div>\n\n <div className=\"flex shrink-0 items-center gap-1\">\n {sourceContext?.contextHref && (\n <HeaderButton\n dataTour=\"conversation-open-context\"\n icon={<ExternalLink size={16} />}\n label={tr(\"conversations.openContext\", \"Open context\")}\n onClick={handleOpenContext}\n />\n )}\n {conversation.isGroup && (\n <HeaderButton\n dataTour=\"conversation-participants-button\"\n icon={<Users size={16} />}\n label={tr(\"conversations.participants\", \"Participants\")}\n active={isParticipantsPanelOpen}\n onClick={toggleParticipantsPanel}\n />\n )}\n <div className=\"relative\" ref={moreMenuRef}>\n <HeaderButton\n icon={<MoreVertical size={16} />}\n label={tr(\"conversations.moreOptions\", \"More options\")}\n active={moreMenuOpen}\n onClick={() => setMoreMenuOpen((prev) => !prev)}\n />\n {moreMenuOpen && (\n <div className=\"absolute right-0 top-full mt-1 w-48 bg-popover border border-border rounded-lg shadow-lg z-50 py-1 animate-in fade-in-0 zoom-in-95 duration-100\">\n <DropdownItem\n icon={\n conversation.isPinned ? (\n <PinOff size={14} />\n ) : (\n <Pin size={14} />\n )\n }\n label={\n conversation.isPinned\n ? tr(\n \"conversations.unpinConversation\",\n \"Unpin conversation\",\n )\n : tr(\n \"conversations.pinConversation\",\n \"Pin conversation\",\n )\n }\n onClick={() => void handlePin()}\n />\n <DropdownItem\n icon={\n conversation.isMuted ? (\n <Bell size={14} />\n ) : (\n <BellOff size={14} />\n )\n }\n label={\n conversation.isMuted\n ? tr(\n \"conversations.unmuteConversation\",\n \"Unmute conversation\",\n )\n : tr(\n \"conversations.muteConversation\",\n \"Mute conversation\",\n )\n }\n onClick={() => void handleMute()}\n />\n <div className=\"h-px bg-border my-1\" />\n <DropdownItem\n icon={<Archive size={14} />}\n label={tr(\n \"conversations.archiveConversation\",\n \"Archive conversation\",\n )}\n onClick={() => void handleArchive()}\n destructive\n />\n </div>\n )}\n </div>\n </div>\n </header>\n\n <div className=\"flex-1 overflow-y-auto py-4 space-y-0.5\">\n {sourceContext?.pagePath && (\n <div className=\"mx-4 mb-4 rounded-lg border border-border bg-muted/20 px-3 py-2 text-xs text-muted-foreground\">\n <span className=\"font-medium text-foreground\">\n {sourceContext.contextTitle || sourceContext.pagePath}\n </span>\n {sourceContext.entityType && sourceContext.entityId\n ? ` · ${sourceContext.entityType} ${sourceContext.entityId}`\n : \"\"}\n </div>\n )}\n {messagesError && (\n <div className=\"mx-4 mb-4 rounded-lg border border-status-warning-border bg-status-warning-bg px-3 py-2 text-sm text-status-warning-text\">\n {tr(\n \"conversations.messagesUnavailable\",\n \"Messages couldn't be loaded right now. The conversation header and actions are still available.\",\n )}\n </div>\n )}\n {messagesLoading && (\n <div className=\"mx-4 mb-4 flex items-center gap-2 rounded-lg border border-border bg-muted/20 px-3 py-2 text-sm text-muted-foreground\">\n <Loader2 size={14} className=\"animate-spin\" />\n {tr(\"conversations.loadingMessages\", \"Loading messages...\")}\n </div>\n )}\n {!messagesLoading &&\n !messagesError &&\n displayedMessages.length === 0 && (\n <div className=\"mx-4 mb-4 rounded-lg border border-dashed border-border bg-muted/20 px-3 py-6 text-center text-sm text-muted-foreground\">\n {tr(\n \"conversations.noMessagesYet\",\n \"No messages yet. Start the conversation below.\",\n )}\n </div>\n )}\n {displayedMessages.map((message, index) => {\n const prevMessage = displayedMessages[index - 1];\n const showAvatar =\n !prevMessage ||\n prevMessage.authorId !== message.authorId ||\n prevMessage.type === \"SYSTEM\";\n\n return (\n <div key={message.id}>\n {editingMessageId === message.id ? (\n <div className=\"px-4 py-2\">\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground mb-1\">\n <span>\n {tr(\"conversations.editingMessage\", \"Editing message\")}\n </span>\n <button\n type=\"button\"\n onClick={handleEditCancel}\n className=\"hover:text-foreground\"\n >\n <X size={12} />\n </button>\n </div>\n <div className=\"flex items-end gap-2\">\n <textarea\n value={editingContent}\n onChange={(e) => setEditingContent(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n void handleEditSubmit();\n }\n if (e.key === \"Escape\") handleEditCancel();\n }}\n className=\"flex-1 px-3 py-2 text-sm rounded-lg border border-border bg-muted/30 resize-none focus:outline-none focus:ring-2 focus:ring-ring\"\n rows={2}\n />\n <button\n type=\"button\"\n onClick={() => void handleEditSubmit()}\n disabled={!editingContent.trim() || editingMessage}\n className=\"px-3 py-2 text-xs font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50\"\n >\n {tr(\"conversations.save\", \"Save\")}\n </button>\n </div>\n </div>\n ) : (\n <MessageBubble\n message={message}\n isOwnMessage={message.authorId === currentUserId}\n showAvatar={showAvatar}\n showSender={showAvatar}\n onReply={handleReply}\n onReact={(id, emoji) => void handleReact(id, emoji)}\n onEdit={handleEdit}\n onDelete={(id) => void handleDelete(id)}\n onTogglePin={(id, isPinned) =>\n void handleTogglePinMessage(id, isPinned)\n }\n />\n )}\n </div>\n );\n })}\n <div ref={messagesEndRef} />\n </div>\n\n <div\n data-tour=\"conversation-message-composer\"\n className=\"border-t border-border px-4 py-3 bg-background\"\n >\n {/* Reply-to indicator */}\n {replyingTo && (\n <div className=\"flex items-center gap-2 mb-2 px-3 py-1.5 rounded-lg bg-muted/50 text-xs text-muted-foreground\">\n <span>\n {tr(\"conversations.replyingTo\", \"Replying to\")}{\" \"}\n <strong className=\"text-foreground\">{replyingTo.sender}</strong>\n : {replyingTo.content.slice(0, 80)}\n {replyingTo.content.length > 80 ? \"…\" : \"\"}\n </span>\n <button\n type=\"button\"\n onClick={() => setReplyingTo(null)}\n aria-label={tr(\"conversations.cancelReply\", \"Cancel reply\")}\n className=\"ml-auto flex min-h-9 min-w-9 items-center justify-center rounded-md hover:bg-muted/70 hover:text-foreground\"\n >\n <X size={12} />\n </button>\n </div>\n )}\n <div className=\"flex items-end gap-2\">\n <div className=\"flex-1 relative\">\n <textarea\n aria-label={tr(\n \"conversations.messageComposer\",\n \"Message composer\",\n )}\n value={messageInput}\n onChange={(event) => setMessageInput(event.target.value)}\n onKeyDown={handleKeyDown}\n placeholder={`${tr(\"conversations.messageConversationPrefix\", \"Message\")} ${conversation.title}...`}\n rows={1}\n className={cn(\n \"w-full px-4 py-2.5 text-sm rounded-xl border border-border bg-muted/30\",\n \"resize-none overflow-hidden placeholder:text-muted-foreground\",\n \"focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent max-h-32\",\n )}\n style={{\n height: \"auto\",\n minHeight: \"40px\",\n }}\n onInput={(event) => {\n const target = event.target as HTMLTextAreaElement;\n target.style.height = \"auto\";\n target.style.height = `${Math.min(target.scrollHeight, 128)}px`;\n }}\n />\n </div>\n\n <button\n type=\"button\"\n aria-label={tr(\"conversations.sendMessage\", \"Send message\")}\n onClick={() => void handleSendMessage()}\n disabled={!messageInput.trim() || sendingMessage}\n className={cn(\n \"p-2.5 rounded-xl transition-all duration-150\",\n messageInput.trim() && !sendingMessage\n ? \"bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm\"\n : \"bg-muted text-muted-foreground cursor-not-allowed\",\n )}\n >\n {sendingMessage ? (\n <Loader2 size={18} className=\"animate-spin\" />\n ) : (\n <SendHorizontal size={18} />\n )}\n </button>\n </div>\n </div>\n </div>\n\n {isParticipantsPanelOpen && conversation.isGroup && (\n <aside className=\"absolute inset-y-0 right-0 z-20 w-full max-w-72 border-l border-border bg-background shadow-lg animate-in slide-in-from-right-4 duration-200 md:relative md:z-auto md:w-64 md:shadow-none\">\n <div className=\"p-4\">\n <div className=\"flex items-center justify-between mb-4\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n {tr(\"conversations.members\", \"Members\")} (\n {participants.length || conversation.memberCount})\n </h3>\n <button\n type=\"button\"\n onClick={toggleParticipantsPanel}\n className=\"p-1 rounded text-muted-foreground hover:text-foreground\"\n >\n <Pin size={14} />\n </button>\n </div>\n\n <div className=\"space-y-1\">\n {participants.map((participant) => (\n <div\n key={participant.id}\n className=\"flex items-center gap-2.5 px-2 py-1.5 rounded-lg hover:bg-muted/50\"\n >\n <div className=\"w-7 h-7 rounded-full bg-muted flex items-center justify-center text-xs font-medium text-muted-foreground\">\n {participant.name\n .split(\" \")\n .map((word) => word[0])\n .join(\"\")\n .slice(0, 2)}\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-xs font-medium text-foreground truncate\">\n {participant.name}\n </p>\n <p className=\"text-[10px] text-muted-foreground\">\n {tr(\"conversations.member\", \"Member\")}\n </p>\n </div>\n </div>\n ))}\n </div>\n </div>\n </aside>\n )}\n </div>\n );\n}\n\nfunction DropdownItem({\n icon,\n label,\n onClick,\n destructive,\n}: {\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n destructive?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className={cn(\n \"flex items-center gap-2.5 w-full px-3 py-2 text-sm transition-colors text-left\",\n destructive\n ? \"text-destructive hover:bg-destructive/10\"\n : \"text-foreground hover:bg-muted/50\",\n )}\n >\n {icon}\n <span>{label}</span>\n </button>\n );\n}\n\nfunction HeaderButton({\n icon,\n label,\n active,\n dataTour,\n onClick,\n}: {\n icon: React.ReactNode;\n label: string;\n active?: boolean;\n dataTour?: string;\n onClick?: () => void;\n}) {\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <button\n data-tour={dataTour}\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n title={label}\n className={cn(\n \"flex min-h-10 min-w-10 items-center justify-center rounded-lg transition-colors\",\n active\n ? \"bg-primary/10 text-primary\"\n : \"text-muted-foreground hover:text-foreground hover:bg-muted/50\",\n )}\n >\n {icon}\n </button>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ConversationDetailPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationDetailPage.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport { useNavigate, useParams } from \"react-router-dom\";\nimport {\n Archive,\n ArrowLeft,\n BellOff,\n Bell,\n ExternalLink,\n Hash,\n Loader2,\n MessageCircle,\n MoreVertical,\n Pin,\n PinOff,\n SendHorizontal,\n Users,\n X,\n} from \"lucide-react\";\nimport { MessageBubble } from \"../components/MessageBubble\";\nimport {\n useGetConversation,\n useConversationActions,\n} from \"../hooks/useConversationQueries\";\nimport {\n useGetMessages,\n useSendMessage,\n useEditMessage,\n useMessageActions,\n} from \"../hooks/useMessageQueries\";\nimport { useMessageStreamForConversation } from \"../hooks/useMessageSubscriptions\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport { cn } from \"../utils/cn\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { nativeImpact, nativeNotify } from \"../../utils/nativeBridge\";\nimport type { Message } from \"../types\";\n\nconst MESSAGES_PAGE_SIZE = 100;\n\nexport function ConversationDetailPage() {\n const { conversationId } = useParams<{ conversationId: string }>();\n const navigate = useNavigate();\n const { basePath, currentUser } = useConversations();\n const bus = useEventBus();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const {\n clearMessageDraft,\n isParticipantsPanelOpen,\n messageDrafts,\n setMessageDraft,\n toggleParticipantsPanel,\n } = useConversationsStore();\n\n const [messageInput, setMessageInput] = useState(\n messageDrafts[conversationId ?? \"\"] ?? \"\",\n );\n const [locallySentMessages, setLocallySentMessages] = useState<Message[]>([]);\n const messagesEndRef = useRef<HTMLDivElement>(null);\n\n const {\n conversation,\n loading: conversationLoading,\n error: conversationError,\n } = useGetConversation(conversationId ?? \"\");\n const {\n messages,\n loading: messagesLoading,\n error: messagesError,\n } = useGetMessages(conversationId ?? \"\", {\n first: MESSAGES_PAGE_SIZE,\n });\n // Subscribe to live message events so the list updates without polling.\n // Apollo cache merges by id; the hook handles append / evict explicitly.\n useMessageStreamForConversation(conversationId);\n const { sendMessage, loading: sendingMessage } = useSendMessage();\n const { editMessage, loading: editingMessage } = useEditMessage();\n const {\n deleteMessage,\n addReaction,\n removeReaction,\n pinMessage,\n unpinMessage,\n } = useMessageActions();\n const {\n archiveConversation,\n pinConversation,\n unpinConversation,\n muteConversation,\n unmuteConversation,\n } = useConversationActions();\n\n const [moreMenuOpen, setMoreMenuOpen] = useState(false);\n const [editingMessageId, setEditingMessageId] = useState<string | null>(null);\n const [editingContent, setEditingContent] = useState(\"\");\n const [replyingTo, setReplyingTo] = useState<{\n id: string;\n content: string;\n sender: string;\n } | null>(null);\n const moreMenuRef = useRef<HTMLDivElement>(null);\n\n const currentUserId = currentUser?.id ?? \"\";\n const sourceContext = conversation?.sourceContext;\n\n const displayedMessages = useMemo(() => {\n const byId = new Map<string, Message>();\n for (const message of locallySentMessages) {\n if (message.conversationId !== conversationId) continue;\n byId.set(message.id, message);\n }\n for (const message of messages) {\n byId.set(message.id, message);\n }\n return Array.from(byId.values()).sort(\n (a, b) =>\n new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),\n );\n }, [conversationId, locallySentMessages, messages]);\n\n useEffect(() => {\n messagesEndRef.current?.scrollIntoView({ behavior: \"smooth\" });\n }, [displayedMessages.length]);\n\n useEffect(() => {\n setLocallySentMessages([]);\n }, [conversationId]);\n\n useEffect(() => {\n if (locallySentMessages.length === 0 || messages.length === 0) return;\n const serverIds = new Set(messages.map((message) => message.id));\n setLocallySentMessages((current) => {\n const pending = current.filter((message) => !serverIds.has(message.id));\n return pending.length === current.length ? current : pending;\n });\n }, [locallySentMessages.length, messages]);\n\n useEffect(() => {\n if (!conversationId) return;\n if (messageInput.trim()) {\n setMessageDraft(conversationId, messageInput);\n return;\n }\n clearMessageDraft(conversationId);\n }, [messageInput, conversationId, setMessageDraft, clearMessageDraft]);\n\n const participants = useMemo(() => {\n const byAuthor = new Map<string, { id: string; name: string }>();\n for (const message of displayedMessages) {\n if (!message.authorId || byAuthor.has(message.authorId)) continue;\n byAuthor.set(message.authorId, {\n id: message.authorId,\n name: message.sender.name || message.authorId,\n });\n }\n return Array.from(byAuthor.values());\n }, [displayedMessages]);\n\n const handleSendMessage = useCallback(async () => {\n if (!conversationId || !messageInput.trim() || sendingMessage) return;\n const message = await sendMessage({\n conversationId,\n content: messageInput.trim(),\n type: \"TEXT\",\n parentMessageId: replyingTo?.id,\n refetchFirst: MESSAGES_PAGE_SIZE,\n });\n if (!message) return;\n setLocallySentMessages((current) => [...current, message]);\n bus.emit(\"conversations.message.sent\", {\n messageId: message.id,\n conversationId,\n });\n setMessageInput(\"\");\n setReplyingTo(null);\n clearMessageDraft(conversationId);\n }, [\n bus,\n conversationId,\n messageInput,\n sendingMessage,\n sendMessage,\n clearMessageDraft,\n replyingTo,\n ]);\n\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (event.key !== \"Enter\" || event.shiftKey) return;\n event.preventDefault();\n void handleSendMessage();\n },\n [handleSendMessage],\n );\n\n // Emit opened event so sinks (analytics/audit) can record the view\n useEffect(() => {\n if (!conversationId) return;\n bus.emit(\"conversations.conversation.opened\", { conversationId });\n }, [bus, conversationId]);\n\n // Close more-menu on outside click\n useEffect(() => {\n if (!moreMenuOpen) return;\n const handler = (e: MouseEvent) => {\n if (\n moreMenuRef.current &&\n !moreMenuRef.current.contains(e.target as Node)\n ) {\n setMoreMenuOpen(false);\n }\n };\n document.addEventListener(\"mousedown\", handler);\n return () => document.removeEventListener(\"mousedown\", handler);\n }, [moreMenuOpen]);\n\n const handleReply = useCallback(\n (message: { id: string; content: string; sender: { name: string } }) => {\n setReplyingTo({\n id: message.id,\n content: message.content,\n sender: message.sender.name,\n });\n },\n [],\n );\n\n const handleReact = useCallback(\n async (messageId: string, emoji: string) => {\n try {\n await addReaction(messageId, emoji);\n } catch {\n // If already reacted, toggle off\n await removeReaction(messageId, emoji).catch(() => {});\n }\n },\n [addReaction, removeReaction],\n );\n\n const handleEdit = useCallback((message: { id: string; content: string }) => {\n setEditingMessageId(message.id);\n setEditingContent(message.content);\n }, []);\n\n const handleEditSubmit = useCallback(async () => {\n if (!editingMessageId || !editingContent.trim() || editingMessage) return;\n await editMessage(editingMessageId, editingContent.trim());\n setEditingMessageId(null);\n setEditingContent(\"\");\n }, [editingMessageId, editingContent, editingMessage, editMessage]);\n\n const handleEditCancel = useCallback(() => {\n setEditingMessageId(null);\n setEditingContent(\"\");\n }, []);\n\n const handleDelete = useCallback(\n async (messageId: string) => {\n nativeImpact(\"medium\");\n try {\n await deleteMessage(messageId);\n nativeNotify(\"success\");\n } catch (error) {\n nativeNotify(\"error\");\n throw error;\n }\n },\n [deleteMessage],\n );\n\n const handleTogglePinMessage = useCallback(\n async (messageId: string, isPinned: boolean) => {\n if (isPinned) {\n await unpinMessage(messageId);\n return;\n }\n await pinMessage(messageId);\n },\n [pinMessage, unpinMessage],\n );\n\n const handlePin = useCallback(async () => {\n if (!conversationId) return;\n if (conversation?.isPinned) {\n await unpinConversation(conversationId);\n } else {\n await pinConversation(conversationId);\n }\n setMoreMenuOpen(false);\n }, [\n conversationId,\n conversation?.isPinned,\n pinConversation,\n unpinConversation,\n ]);\n\n const handleMute = useCallback(async () => {\n if (!conversationId) return;\n if (conversation?.isMuted) {\n await unmuteConversation(conversationId);\n } else {\n await muteConversation(conversationId);\n }\n setMoreMenuOpen(false);\n }, [\n conversationId,\n conversation?.isMuted,\n muteConversation,\n unmuteConversation,\n ]);\n\n const handleArchive = useCallback(async () => {\n if (!conversationId) return;\n await archiveConversation(conversationId);\n bus.emit(\"conversations.conversation.archived\", { conversationId });\n setMoreMenuOpen(false);\n navigate(basePath || \"/conversations\");\n }, [archiveConversation, basePath, bus, conversationId, navigate]);\n\n const handleOpenContext = useCallback(() => {\n if (!sourceContext?.contextHref) return;\n try {\n const url = new URL(sourceContext.contextHref);\n if (url.origin === window.location.origin) {\n navigate(`${url.pathname}${url.search}${url.hash}`);\n return;\n }\n } catch {\n // Fall through to full navigation for relative or invalid URLs.\n }\n\n window.location.assign(sourceContext.contextHref);\n }, [navigate, sourceContext?.contextHref]);\n\n if (!conversation && conversationLoading) {\n return (\n <div className=\"flex items-center justify-center h-[calc(100vh-64px)] text-sm text-muted-foreground\">\n {tr(\"conversations.loading\", \"Loading conversation...\")}\n </div>\n );\n }\n\n if (!conversation || conversationError) {\n return (\n <div className=\"flex flex-col items-center justify-center h-[calc(100vh-64px)] text-center gap-3 px-4\">\n <MessageCircle className=\"size-8 text-muted-foreground\" />\n <p className=\"text-sm text-muted-foreground\">\n {conversationError\n ? tr(\n \"conversations.unavailable\",\n \"We couldn't load this conversation. Please try again.\",\n )\n : tr(\"conversations.notFound\", \"Conversation not found\")}\n </p>\n <button\n type=\"button\"\n onClick={() => navigate(basePath || \"/conversations\")}\n className=\"px-3 py-2 text-sm font-medium rounded-lg border border-border hover:bg-muted/50\"\n >\n {tr(\"conversations.back\", \"Back to conversations\")}\n </button>\n </div>\n );\n }\n\n return (\n <div className=\"relative flex h-[calc(100vh-64px)] bg-background\">\n <div className=\"flex-1 flex flex-col min-w-0\">\n <header\n data-tour=\"conversation-detail-header\"\n className=\"flex items-center justify-between px-4 py-3 border-b border-border bg-background/95 backdrop-blur-sm\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <button\n type=\"button\"\n onClick={() => navigate(basePath || \"/conversations\")}\n className=\"flex min-h-10 min-w-10 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground\"\n >\n <ArrowLeft size={18} />\n </button>\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-1.5\">\n {conversation.type === \"CHANNEL\" && (\n <Hash\n size={14}\n className=\"text-muted-foreground flex-shrink-0\"\n />\n )}\n <h2 className=\"text-sm font-semibold text-foreground truncate\">\n {conversation.title}\n </h2>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {conversation.type === \"DIRECT\"\n ? tr(\n \"conversations.directConversation\",\n \"Direct conversation\",\n )\n : conversation.type === \"CHANNEL\"\n ? `${tr(\"conversations.channel\", \"Channel\")} · ${conversation.memberCount} ${tr(\"conversations.membersCountLabel\", \"members\")}`\n : conversation.type === \"ANNOUNCEMENT\"\n ? `${tr(\"conversations.announcement\", \"Announcement\")} · ${conversation.memberCount} ${tr(\"conversations.membersCountLabel\", \"members\")}`\n : `${conversation.memberCount} ${tr(\"conversations.membersCountLabel\", \"members\")}`}\n </p>\n </div>\n </div>\n\n <div className=\"flex shrink-0 items-center gap-1\">\n {sourceContext?.contextHref && (\n <HeaderButton\n dataTour=\"conversation-open-context\"\n icon={<ExternalLink size={16} />}\n label={tr(\"conversations.openContext\", \"Open context\")}\n onClick={handleOpenContext}\n />\n )}\n {conversation.isGroup && (\n <HeaderButton\n dataTour=\"conversation-participants-button\"\n icon={<Users size={16} />}\n label={tr(\"conversations.participants\", \"Participants\")}\n active={isParticipantsPanelOpen}\n onClick={toggleParticipantsPanel}\n />\n )}\n <div className=\"relative\" ref={moreMenuRef}>\n <HeaderButton\n icon={<MoreVertical size={16} />}\n label={tr(\"conversations.moreOptions\", \"More options\")}\n active={moreMenuOpen}\n onClick={() => setMoreMenuOpen((prev) => !prev)}\n />\n {moreMenuOpen && (\n <div className=\"absolute right-0 top-full mt-1 w-48 bg-popover border border-border rounded-lg shadow-lg z-50 py-1 animate-in fade-in-0 zoom-in-95 duration-100\">\n <DropdownItem\n icon={\n conversation.isPinned ? (\n <PinOff size={14} />\n ) : (\n <Pin size={14} />\n )\n }\n label={\n conversation.isPinned\n ? tr(\n \"conversations.unpinConversation\",\n \"Unpin conversation\",\n )\n : tr(\n \"conversations.pinConversation\",\n \"Pin conversation\",\n )\n }\n onClick={() => void handlePin()}\n />\n <DropdownItem\n icon={\n conversation.isMuted ? (\n <Bell size={14} />\n ) : (\n <BellOff size={14} />\n )\n }\n label={\n conversation.isMuted\n ? tr(\n \"conversations.unmuteConversation\",\n \"Unmute conversation\",\n )\n : tr(\n \"conversations.muteConversation\",\n \"Mute conversation\",\n )\n }\n onClick={() => void handleMute()}\n />\n <div className=\"h-px bg-border my-1\" />\n <DropdownItem\n icon={<Archive size={14} />}\n label={tr(\n \"conversations.archiveConversation\",\n \"Archive conversation\",\n )}\n onClick={() => void handleArchive()}\n destructive\n />\n </div>\n )}\n </div>\n </div>\n </header>\n\n <div className=\"flex-1 overflow-y-auto py-4 space-y-0.5\">\n {sourceContext?.pagePath && (\n <div className=\"mx-4 mb-4 rounded-lg border border-border bg-muted/20 px-3 py-2 text-xs text-muted-foreground\">\n <span className=\"font-medium text-foreground\">\n {sourceContext.contextTitle || sourceContext.pagePath}\n </span>\n {sourceContext.entityType && sourceContext.entityId\n ? ` · ${sourceContext.entityType} ${sourceContext.entityId}`\n : \"\"}\n </div>\n )}\n {messagesError && (\n <div className=\"mx-4 mb-4 rounded-lg border border-status-warning-border bg-status-warning-bg px-3 py-2 text-sm text-status-warning-text\">\n {tr(\n \"conversations.messagesUnavailable\",\n \"Messages couldn't be loaded right now. The conversation header and actions are still available.\",\n )}\n </div>\n )}\n {messagesLoading && (\n <div className=\"mx-4 mb-4 flex items-center gap-2 rounded-lg border border-border bg-muted/20 px-3 py-2 text-sm text-muted-foreground\">\n <Loader2 size={14} className=\"animate-spin\" />\n {tr(\"conversations.loadingMessages\", \"Loading messages...\")}\n </div>\n )}\n {!messagesLoading &&\n !messagesError &&\n displayedMessages.length === 0 && (\n <div className=\"mx-4 mb-4 rounded-lg border border-dashed border-border bg-muted/20 px-3 py-6 text-center text-sm text-muted-foreground\">\n {tr(\n \"conversations.noMessagesYet\",\n \"No messages yet. Start the conversation below.\",\n )}\n </div>\n )}\n {displayedMessages.map((message, index) => {\n const prevMessage = displayedMessages[index - 1];\n const showAvatar =\n !prevMessage ||\n prevMessage.authorId !== message.authorId ||\n prevMessage.type === \"SYSTEM\";\n\n return (\n <div key={message.id}>\n {editingMessageId === message.id ? (\n <div className=\"px-4 py-2\">\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground mb-1\">\n <span>\n {tr(\"conversations.editingMessage\", \"Editing message\")}\n </span>\n <button\n type=\"button\"\n onClick={handleEditCancel}\n className=\"hover:text-foreground\"\n >\n <X size={12} />\n </button>\n </div>\n <div className=\"flex items-end gap-2\">\n <textarea\n aria-label=\"Edit message\"\n value={editingContent}\n onChange={(e) => setEditingContent(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n void handleEditSubmit();\n }\n if (e.key === \"Escape\") handleEditCancel();\n }}\n className=\"flex-1 px-3 py-2 text-sm rounded-lg border border-border bg-muted/30 resize-none focus:outline-none focus:ring-2 focus:ring-ring\"\n rows={2}\n />\n <button\n type=\"button\"\n onClick={() => void handleEditSubmit()}\n disabled={!editingContent.trim() || editingMessage}\n className=\"px-3 py-2 text-xs font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50\"\n >\n {tr(\"conversations.save\", \"Save\")}\n </button>\n </div>\n </div>\n ) : (\n <MessageBubble\n message={message}\n isOwnMessage={message.authorId === currentUserId}\n showAvatar={showAvatar}\n showSender={showAvatar}\n onReply={handleReply}\n onReact={(id, emoji) => void handleReact(id, emoji)}\n onEdit={handleEdit}\n onDelete={(id) => void handleDelete(id)}\n onTogglePin={(id, isPinned) =>\n void handleTogglePinMessage(id, isPinned)\n }\n />\n )}\n </div>\n );\n })}\n <div ref={messagesEndRef} />\n </div>\n\n <div\n data-tour=\"conversation-message-composer\"\n className=\"border-t border-border px-4 py-3 bg-background\"\n >\n {/* Reply-to indicator */}\n {replyingTo && (\n <div className=\"flex items-center gap-2 mb-2 px-3 py-1.5 rounded-lg bg-muted/50 text-xs text-muted-foreground\">\n <span>\n {tr(\"conversations.replyingTo\", \"Replying to\")}{\" \"}\n <strong className=\"text-foreground\">{replyingTo.sender}</strong>\n : {replyingTo.content.slice(0, 80)}\n {replyingTo.content.length > 80 ? \"…\" : \"\"}\n </span>\n <button\n type=\"button\"\n onClick={() => setReplyingTo(null)}\n aria-label={tr(\"conversations.cancelReply\", \"Cancel reply\")}\n className=\"ml-auto flex min-h-9 min-w-9 items-center justify-center rounded-md hover:bg-muted/70 hover:text-foreground\"\n >\n <X size={12} />\n </button>\n </div>\n )}\n <div className=\"flex items-end gap-2\">\n <div className=\"flex-1 relative\">\n <textarea\n aria-label={tr(\n \"conversations.messageComposer\",\n \"Message composer\",\n )}\n value={messageInput}\n onChange={(event) => setMessageInput(event.target.value)}\n onKeyDown={handleKeyDown}\n placeholder={`${tr(\"conversations.messageConversationPrefix\", \"Message\")} ${conversation.title}...`}\n rows={1}\n className={cn(\n \"w-full px-4 py-2.5 text-sm rounded-xl border border-border bg-muted/30\",\n \"resize-none overflow-hidden placeholder:text-muted-foreground\",\n \"focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent max-h-32\",\n )}\n style={{\n height: \"auto\",\n minHeight: \"40px\",\n }}\n onInput={(event) => {\n const target = event.target as HTMLTextAreaElement;\n target.style.height = \"auto\";\n target.style.height = `${Math.min(target.scrollHeight, 128)}px`;\n }}\n />\n </div>\n\n <button\n type=\"button\"\n aria-label={tr(\"conversations.sendMessage\", \"Send message\")}\n onClick={() => void handleSendMessage()}\n disabled={!messageInput.trim() || sendingMessage}\n className={cn(\n \"p-2.5 rounded-xl transition-all duration-150\",\n messageInput.trim() && !sendingMessage\n ? \"bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm\"\n : \"bg-muted text-muted-foreground cursor-not-allowed\",\n )}\n >\n {sendingMessage ? (\n <Loader2 size={18} className=\"animate-spin\" />\n ) : (\n <SendHorizontal size={18} />\n )}\n </button>\n </div>\n </div>\n </div>\n\n {isParticipantsPanelOpen && conversation.isGroup && (\n <aside className=\"absolute inset-y-0 right-0 z-20 w-full max-w-72 border-l border-border bg-background shadow-lg animate-in slide-in-from-right-4 duration-200 md:relative md:z-auto md:w-64 md:shadow-none\">\n <div className=\"p-4\">\n <div className=\"flex items-center justify-between mb-4\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n {tr(\"conversations.members\", \"Members\")} (\n {participants.length || conversation.memberCount})\n </h3>\n <button\n type=\"button\"\n onClick={toggleParticipantsPanel}\n className=\"p-1 rounded text-muted-foreground hover:text-foreground\"\n >\n <Pin size={14} />\n </button>\n </div>\n\n <div className=\"space-y-1\">\n {participants.map((participant) => (\n <div\n key={participant.id}\n className=\"flex items-center gap-2.5 px-2 py-1.5 rounded-lg hover:bg-muted/50\"\n >\n <div className=\"size-7 rounded-full bg-muted flex items-center justify-center text-xs font-medium text-muted-foreground\">\n {participant.name\n .split(\" \")\n .map((word) => word[0])\n .join(\"\")\n .slice(0, 2)}\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-xs font-medium text-foreground truncate\">\n {participant.name}\n </p>\n <p className=\"text-[10px] text-muted-foreground\">\n {tr(\"conversations.member\", \"Member\")}\n </p>\n </div>\n </div>\n ))}\n </div>\n </div>\n </aside>\n )}\n </div>\n );\n}\n\nfunction DropdownItem({\n icon,\n label,\n onClick,\n destructive,\n}: {\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n destructive?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className={cn(\n \"flex items-center gap-2.5 w-full px-3 py-2 text-sm transition-colors text-left\",\n destructive\n ? \"text-destructive hover:bg-destructive/10\"\n : \"text-foreground hover:bg-muted/50\",\n )}\n >\n {icon}\n <span>{label}</span>\n </button>\n );\n}\n\nfunction HeaderButton({\n icon,\n label,\n active,\n dataTour,\n onClick,\n}: {\n icon: React.ReactNode;\n label: string;\n active?: boolean;\n dataTour?: string;\n onClick?: () => void;\n}) {\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <button\n data-tour={dataTour}\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n title={label}\n className={cn(\n \"flex min-h-10 min-w-10 items-center justify-center rounded-lg transition-colors\",\n active\n ? \"bg-primary/10 text-primary\"\n : \"text-muted-foreground hover:text-foreground hover:bg-muted/50\",\n )}\n >\n {icon}\n </button>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}