@burdenoff/microfe-conversations 2026.519.1 → 2026.523.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/conversations/ConversationsUserRoutes.d.ts.map +1 -1
  2. package/dist/conversations/ConversationsUserRoutes.js +17 -19
  3. package/dist/conversations/ConversationsUserRoutes.js.map +1 -1
  4. package/dist/conversations/components/ConversationCard.js +1 -1
  5. package/dist/conversations/components/ConversationCard.js.map +1 -1
  6. package/dist/conversations/components/LazyPageBoundary.d.ts +3 -3
  7. package/dist/conversations/components/LazyPageBoundary.d.ts.map +1 -1
  8. package/dist/conversations/components/LazyPageBoundary.js.map +1 -1
  9. package/dist/conversations/components/MessageBubble.d.ts.map +1 -1
  10. package/dist/conversations/components/MessageBubble.js +40 -42
  11. package/dist/conversations/components/MessageBubble.js.map +1 -1
  12. package/dist/conversations/hooks/useConversationQueries.js +1 -1
  13. package/dist/conversations/hooks/useConversationQueries.js.map +1 -1
  14. package/dist/conversations/hooks/useMessageQueries.d.ts +1 -0
  15. package/dist/conversations/hooks/useMessageQueries.d.ts.map +1 -1
  16. package/dist/conversations/hooks/useMessageQueries.js +48 -42
  17. package/dist/conversations/hooks/useMessageQueries.js.map +1 -1
  18. package/dist/conversations/index.js +1 -1
  19. package/dist/conversations/pages/ConversationAdminPages.d.ts.map +1 -1
  20. package/dist/conversations/pages/ConversationAdminPages.js +139 -138
  21. package/dist/conversations/pages/ConversationAdminPages.js.map +1 -1
  22. package/dist/conversations/pages/ConversationDetailPage.d.ts.map +1 -1
  23. package/dist/conversations/pages/ConversationDetailPage.js +253 -232
  24. package/dist/conversations/pages/ConversationDetailPage.js.map +1 -1
  25. package/dist/conversations/pages/ConversationListPage.d.ts +1 -2
  26. package/dist/conversations/pages/ConversationListPage.d.ts.map +1 -1
  27. package/dist/conversations/pages/ConversationListPage.js +132 -125
  28. package/dist/conversations/pages/ConversationListPage.js.map +1 -1
  29. package/dist/conversations/pages/ConversationsLandingPage.js +1 -1
  30. package/dist/conversations/pages/ConversationsLandingPage.js.map +1 -1
  31. package/dist/conversations/pages/ConversationsSettingsPage.js +4 -4
  32. package/dist/conversations/providers/ConversationsProvider.d.ts.map +1 -1
  33. package/dist/conversations/providers/ConversationsProvider.js +10 -8
  34. package/dist/conversations/providers/ConversationsProvider.js.map +1 -1
  35. package/dist/index.js +5 -5
  36. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationsUserRoutes.d.ts","sourceRoot":"","sources":["../../src/conversations/ConversationsUserRoutes.tsx"],"names":[],"mappings":"AAsBA;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,4CAatC"}
1
+ {"version":3,"file":"ConversationsUserRoutes.d.ts","sourceRoot":"","sources":["../../src/conversations/ConversationsUserRoutes.tsx"],"names":[],"mappings":"AAgCA;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,4CAatC"}
@@ -1,37 +1,35 @@
1
1
  import { LazyPageBoundary as e, lazyRetry as t } from "./components/LazyPageBoundary.js";
2
- import { ConversationListPage as n } from "./pages/ConversationListPage.js";
3
- import { ConversationsLandingPage as r } from "./pages/ConversationsLandingPage.js";
4
- import { Navigate as i, Route as a, Routes as o } from "react-router-dom";
5
- import { jsx as s, jsxs as c } from "react/jsx-runtime";
2
+ import { Navigate as n, Route as r, Routes as i } from "react-router-dom";
3
+ import { jsx as a, jsxs as o } from "react/jsx-runtime";
6
4
  //#region src/conversations/ConversationsUserRoutes.tsx
7
- var l = t(() => import("./pages/ConversationDetailPage.js").then((e) => ({ default: e.ConversationDetailPage }))), u = t(() => import("./pages/NewConversationPage.js").then((e) => ({ default: e.NewConversationPage }))), d = t(() => import("./pages/ConversationsSettingsPage.js").then((e) => ({ default: e.ConversationsSettingsPage })));
5
+ var s = t(() => import("./pages/ConversationListPage.js").then((e) => ({ default: e.ConversationListPage }))), c = t(() => import("./pages/ConversationsLandingPage.js").then((e) => ({ default: e.ConversationsLandingPage }))), l = t(() => import("./pages/ConversationDetailPage.js").then((e) => ({ default: e.ConversationDetailPage }))), u = t(() => import("./pages/NewConversationPage.js").then((e) => ({ default: e.NewConversationPage }))), d = t(() => import("./pages/ConversationsSettingsPage.js").then((e) => ({ default: e.ConversationsSettingsPage })));
8
6
  function f() {
9
- return /* @__PURE__ */ s(e, {
7
+ return /* @__PURE__ */ a(e, {
10
8
  source: "user",
11
- children: /* @__PURE__ */ c(o, { children: [
12
- /* @__PURE__ */ s(a, {
9
+ children: /* @__PURE__ */ o(i, { children: [
10
+ /* @__PURE__ */ a(r, {
13
11
  index: !0,
14
- element: /* @__PURE__ */ s(r, {})
12
+ element: /* @__PURE__ */ a(c, {})
15
13
  }),
16
- /* @__PURE__ */ s(a, {
14
+ /* @__PURE__ */ a(r, {
17
15
  path: "list",
18
- element: /* @__PURE__ */ s(n, {})
16
+ element: /* @__PURE__ */ a(s, {})
19
17
  }),
20
- /* @__PURE__ */ s(a, {
18
+ /* @__PURE__ */ a(r, {
21
19
  path: "new",
22
- element: /* @__PURE__ */ s(u, {})
20
+ element: /* @__PURE__ */ a(u, {})
23
21
  }),
24
- /* @__PURE__ */ s(a, {
22
+ /* @__PURE__ */ a(r, {
25
23
  path: "settings",
26
- element: /* @__PURE__ */ s(d, {})
24
+ element: /* @__PURE__ */ a(d, {})
27
25
  }),
28
- /* @__PURE__ */ s(a, {
26
+ /* @__PURE__ */ a(r, {
29
27
  path: ":conversationId",
30
- element: /* @__PURE__ */ s(l, {})
28
+ element: /* @__PURE__ */ a(l, {})
31
29
  }),
32
- /* @__PURE__ */ s(a, {
30
+ /* @__PURE__ */ a(r, {
33
31
  path: "*",
34
- element: /* @__PURE__ */ s(i, {
32
+ element: /* @__PURE__ */ a(n, {
35
33
  to: ".",
36
34
  replace: !0
37
35
  })
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationsUserRoutes.js","names":[],"sources":["../../src/conversations/ConversationsUserRoutes.tsx"],"sourcesContent":["import { Navigate, Route, Routes } from \"react-router-dom\";\n\nimport { LazyPageBoundary, lazyRetry } from \"./components/LazyPageBoundary\";\nimport { ConversationListPage } from \"./pages/ConversationListPage\";\nimport { ConversationsLandingPage } from \"./pages/ConversationsLandingPage\";\n\nconst ConversationDetailPage = lazyRetry(() =>\n import(\"./pages/ConversationDetailPage\").then((m) => ({\n default: m.ConversationDetailPage,\n })),\n);\nconst NewConversationPage = lazyRetry(() =>\n import(\"./pages/NewConversationPage\").then((m) => ({\n default: m.NewConversationPage,\n })),\n);\nconst ConversationsSettingsPage = lazyRetry(() =>\n import(\"./pages/ConversationsSettingsPage\").then((m) => ({\n default: m.ConversationsSettingsPage,\n })),\n);\n\n/**\n * User-facing routes for the Conversations microfrontend.\n *\n * Routes (relative to basePath, e.g., /conversations):\n * - / -> Conversation list page (inbox tabs)\n * - /list -> Conversation list page\n * - /new -> Create new conversation\n * - /:conversationId -> Conversation detail (chat view)\n * - /settings -> Personal notification settings\n */\nexport function ConversationsUserRoutes() {\n return (\n <LazyPageBoundary source=\"user\">\n <Routes>\n <Route index element={<ConversationsLandingPage />} />\n <Route path=\"list\" element={<ConversationListPage />} />\n <Route path=\"new\" element={<NewConversationPage />} />\n <Route path=\"settings\" element={<ConversationsSettingsPage />} />\n <Route path=\":conversationId\" element={<ConversationDetailPage />} />\n <Route path=\"*\" element={<Navigate to=\".\" replace />} />\n </Routes>\n </LazyPageBoundary>\n );\n}\n"],"mappings":";;;;;;AAMA,IAAM,IAAyB,QAC7B,OAAO,qCAAkC,MAAM,OAAO,EACpD,SAAS,EAAE,wBACZ,EAAE,CACJ,EACK,IAAsB,QAC1B,OAAO,kCAA+B,MAAM,OAAO,EACjD,SAAS,EAAE,qBACZ,EAAE,CACJ,EACK,IAA4B,QAChC,OAAO,wCAAqC,MAAM,OAAO,EACvD,SAAS,EAAE,2BACZ,EAAE,CACJ;AAYD,SAAgB,IAA0B;AACxC,QACE,kBAAC,GAAD;EAAkB,QAAO;YACvB,kBAAC,GAAD,EAAA,UAAA;GACE,kBAAC,GAAD;IAAO,OAAA;IAAM,SAAS,kBAAC,GAAD,EAA4B,CAAA;IAAI,CAAA;GACtD,kBAAC,GAAD;IAAO,MAAK;IAAO,SAAS,kBAAC,GAAD,EAAwB,CAAA;IAAI,CAAA;GACxD,kBAAC,GAAD;IAAO,MAAK;IAAM,SAAS,kBAAC,GAAD,EAAuB,CAAA;IAAI,CAAA;GACtD,kBAAC,GAAD;IAAO,MAAK;IAAW,SAAS,kBAAC,GAAD,EAA6B,CAAA;IAAI,CAAA;GACjE,kBAAC,GAAD;IAAO,MAAK;IAAkB,SAAS,kBAAC,GAAD,EAA0B,CAAA;IAAI,CAAA;GACrE,kBAAC,GAAD;IAAO,MAAK;IAAI,SAAS,kBAAC,GAAD;KAAU,IAAG;KAAI,SAAA;KAAU,CAAA;IAAI,CAAA;GACjD,EAAA,CAAA;EACQ,CAAA"}
1
+ {"version":3,"file":"ConversationsUserRoutes.js","names":[],"sources":["../../src/conversations/ConversationsUserRoutes.tsx"],"sourcesContent":["import { Navigate, Route, Routes } from \"react-router-dom\";\n\nimport { LazyPageBoundary, lazyRetry } from \"./components/LazyPageBoundary\";\nimport type { ConversationListPageProps } from \"./pages/ConversationListPage\";\n\nconst ConversationListPage = lazyRetry<ConversationListPageProps>(() =>\n import(\"./pages/ConversationListPage\").then((m) => ({\n default: m.ConversationListPage,\n })),\n);\nconst ConversationsLandingPage = lazyRetry<Record<string, never>>(() =>\n import(\"./pages/ConversationsLandingPage\").then((m) => ({\n default: m.ConversationsLandingPage,\n })),\n);\n\nconst ConversationDetailPage = lazyRetry<Record<string, never>>(() =>\n import(\"./pages/ConversationDetailPage\").then((m) => ({\n default: m.ConversationDetailPage,\n })),\n);\nconst NewConversationPage = lazyRetry<Record<string, never>>(() =>\n import(\"./pages/NewConversationPage\").then((m) => ({\n default: m.NewConversationPage,\n })),\n);\nconst ConversationsSettingsPage = lazyRetry<Record<string, never>>(() =>\n import(\"./pages/ConversationsSettingsPage\").then((m) => ({\n default: m.ConversationsSettingsPage,\n })),\n);\n\n/**\n * User-facing routes for the Conversations microfrontend.\n *\n * Routes (relative to basePath, e.g., /conversations):\n * - / -> Conversation list page (inbox tabs)\n * - /list -> Conversation list page\n * - /new -> Create new conversation\n * - /:conversationId -> Conversation detail (chat view)\n * - /settings -> Personal notification settings\n */\nexport function ConversationsUserRoutes() {\n return (\n <LazyPageBoundary source=\"user\">\n <Routes>\n <Route index element={<ConversationsLandingPage />} />\n <Route path=\"list\" element={<ConversationListPage />} />\n <Route path=\"new\" element={<NewConversationPage />} />\n <Route path=\"settings\" element={<ConversationsSettingsPage />} />\n <Route path=\":conversationId\" element={<ConversationDetailPage />} />\n <Route path=\"*\" element={<Navigate to=\".\" replace />} />\n </Routes>\n </LazyPageBoundary>\n );\n}\n"],"mappings":";;;;AAKA,IAAM,IAAuB,QAC3B,OAAO,mCAAgC,MAAM,OAAO,EAClD,SAAS,EAAE,sBACZ,EAAE,CACJ,EACK,IAA2B,QAC/B,OAAO,uCAAoC,MAAM,OAAO,EACtD,SAAS,EAAE,0BACZ,EAAE,CACJ,EAEK,IAAyB,QAC7B,OAAO,qCAAkC,MAAM,OAAO,EACpD,SAAS,EAAE,wBACZ,EAAE,CACJ,EACK,IAAsB,QAC1B,OAAO,kCAA+B,MAAM,OAAO,EACjD,SAAS,EAAE,qBACZ,EAAE,CACJ,EACK,IAA4B,QAChC,OAAO,wCAAqC,MAAM,OAAO,EACvD,SAAS,EAAE,2BACZ,EAAE,CACJ;AAYD,SAAgB,IAA0B;AACxC,QACE,kBAAC,GAAD;EAAkB,QAAO;YACvB,kBAAC,GAAD,EAAA,UAAA;GACE,kBAAC,GAAD;IAAO,OAAA;IAAM,SAAS,kBAAC,GAAD,EAA4B,CAAA;IAAI,CAAA;GACtD,kBAAC,GAAD;IAAO,MAAK;IAAO,SAAS,kBAAC,GAAD,EAAwB,CAAA;IAAI,CAAA;GACxD,kBAAC,GAAD;IAAO,MAAK;IAAM,SAAS,kBAAC,GAAD,EAAuB,CAAA;IAAI,CAAA;GACtD,kBAAC,GAAD;IAAO,MAAK;IAAW,SAAS,kBAAC,GAAD,EAA6B,CAAA;IAAI,CAAA;GACjE,kBAAC,GAAD;IAAO,MAAK;IAAkB,SAAS,kBAAC,GAAD,EAA0B,CAAA;IAAI,CAAA;GACrE,kBAAC,GAAD;IAAO,MAAK;IAAI,SAAS,kBAAC,GAAD;KAAU,IAAG;KAAI,SAAA;KAAU,CAAA;IAAI,CAAA;GACjD,EAAA,CAAA;EACQ,CAAA"}
@@ -39,7 +39,7 @@ var _ = t(function({ conversation: t, isActive: n = !1, onClick: r }) {
39
39
  onClick: () => r?.(t),
40
40
  className: e("w-full flex items-start gap-3 px-4 py-3 text-left rounded-lg", "transition-all duration-150 ease-in-out", "hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", n && "bg-primary/5 border-l-2 border-l-primary", !n && "border-l-2 border-l-transparent", C && !n && "bg-muted/20"),
41
41
  children: [/* @__PURE__ */ d("div", {
42
- className: e("flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold", a === "DIRECT" ? "bg-status-info-bg-subtle text-status-info-text" : a === "CHANNEL" ? "bg-status-success-bg-subtle text-status-success-text" : a === "GROUP" ? "bg-accent-purple-subtle text-accent-purple" : "bg-muted text-muted-foreground"),
42
+ className: e("flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold", a === "DIRECT" ? "bg-status-info-bg-subtle text-status-info-text" : a === "CHANNEL" ? "bg-status-success-bg-subtle text-status-success-text" : a === "GROUP" ? "bg-accent text-accent-foreground" : "bg-muted text-muted-foreground"),
43
43
  children: t.avatar ? /* @__PURE__ */ d("img", {
44
44
  src: t.avatar,
45
45
  alt: i,
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationCard.js","names":[],"sources":["../../../src/conversations/components/ConversationCard.tsx"],"sourcesContent":["import { memo } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n MessageCircle,\n Hash,\n Users,\n Megaphone,\n Bot,\n HeadphonesIcon,\n Pin,\n VolumeX,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport type { Conversation } from \"../types\";\n\ninterface ConversationCardProps {\n conversation: Conversation;\n isActive?: boolean;\n onClick?: (conversation: Conversation) => void;\n}\n\n/**\n * Conversation type icon mapping\n */\nfunction ConversationTypeIcon({\n type,\n size = 16,\n}: {\n type: Conversation[\"type\"];\n size?: number;\n}) {\n switch (type) {\n case \"DIRECT\":\n return <MessageCircle size={size} />;\n case \"GROUP\":\n return <Users size={size} />;\n case \"CHANNEL\":\n return <Hash size={size} />;\n case \"BROADCAST\":\n case \"ANNOUNCEMENT\":\n return <Megaphone size={size} />;\n case \"BOT\":\n return <Bot size={size} />;\n case \"SUPPORT\":\n return <HeadphonesIcon size={size} />;\n default:\n return <MessageCircle size={size} />;\n }\n}\n\n/**\n * Format a timestamp relative to now (e.g., \"2m ago\", \"3h\", \"Yesterday\")\n */\nfunction formatRelativeTime(\n dateString: string,\n labels: { now: string; yesterday: string },\n): string {\n const now = new Date();\n const date = new Date(dateString);\n const diffMs = now.getTime() - date.getTime();\n const diffSec = Math.floor(diffMs / 1000);\n const diffMin = Math.floor(diffSec / 60);\n const diffHr = Math.floor(diffMin / 60);\n const diffDay = Math.floor(diffHr / 24);\n\n if (diffMin < 1) return labels.now;\n if (diffMin < 60) return `${diffMin}m`;\n if (diffHr < 24) return `${diffHr}h`;\n if (diffDay === 1) return labels.yesterday;\n if (diffDay < 7) return `${diffDay}d`;\n return date.toLocaleDateString(undefined, { month: \"short\", day: \"numeric\" });\n}\n\n/**\n * Generate initials from a conversation title\n */\nfunction getInitials(title: string): string {\n return title\n .split(\" \")\n .slice(0, 2)\n .map((word) => word[0])\n .join(\"\")\n .toUpperCase();\n}\n\n/**\n * ConversationCard - Displays a conversation in the list view.\n * Shows avatar, title, last message preview, unread count, and metadata.\n */\nexport const ConversationCard = memo(function ConversationCard({\n conversation,\n isActive = false,\n onClick,\n}: ConversationCardProps) {\n const {\n title,\n type,\n lastMessage,\n unreadCount,\n isPinned,\n isMuted,\n memberCount,\n updatedAt,\n sourceContext,\n } = conversation;\n\n const hasUnread = unreadCount > 0;\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 timeLabels = {\n now: tr(\"conversations.now\", \"now\"),\n yesterday: tr(\"conversations.yesterday\", \"Yesterday\"),\n };\n\n return (\n <button\n type=\"button\"\n onClick={() => onClick?.(conversation)}\n className={cn(\n \"w-full flex items-start gap-3 px-4 py-3 text-left rounded-lg\",\n \"transition-all duration-150 ease-in-out\",\n \"hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n isActive && \"bg-primary/5 border-l-2 border-l-primary\",\n !isActive && \"border-l-2 border-l-transparent\",\n hasUnread && !isActive && \"bg-muted/20\",\n )}\n >\n {/* Avatar / Type Icon */}\n <div\n className={cn(\n \"flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold\",\n type === \"DIRECT\"\n ? \"bg-status-info-bg-subtle text-status-info-text\"\n : type === \"CHANNEL\"\n ? \"bg-status-success-bg-subtle text-status-success-text\"\n : type === \"GROUP\"\n ? \"bg-accent-purple-subtle text-accent-purple\"\n : \"bg-muted text-muted-foreground\",\n )}\n >\n {conversation.avatar ? (\n <img\n src={conversation.avatar}\n alt={title}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : type === \"DIRECT\" ? (\n getInitials(title)\n ) : (\n <ConversationTypeIcon type={type} size={18} />\n )}\n </div>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n {/* Title row */}\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex items-center gap-1.5 min-w-0\">\n {type !== \"DIRECT\" && (\n <ConversationTypeIcon type={type} size={12} />\n )}\n <span\n className={cn(\n \"text-sm truncate\",\n hasUnread\n ? \"font-semibold text-foreground\"\n : \"font-medium text-foreground\",\n )}\n >\n {title}\n </span>\n {isPinned && (\n <Pin size={12} className=\"flex-shrink-0 text-muted-foreground\" />\n )}\n {isMuted && (\n <VolumeX\n size={12}\n className=\"flex-shrink-0 text-muted-foreground\"\n />\n )}\n </div>\n\n {/* Timestamp */}\n <span\n className={cn(\n \"text-xs flex-shrink-0\",\n hasUnread ? \"text-primary font-medium\" : \"text-muted-foreground\",\n )}\n >\n {lastMessage\n ? formatRelativeTime(lastMessage.createdAt, timeLabels)\n : formatRelativeTime(updatedAt, timeLabels)}\n </span>\n </div>\n\n {/* Last message preview */}\n <div className=\"flex items-center justify-between gap-2 mt-0.5\">\n <p\n className={cn(\n \"text-xs truncate\",\n hasUnread ? \"text-foreground/80\" : \"text-muted-foreground\",\n )}\n >\n {lastMessage ? (\n <>\n {lastMessage.sender.name && (\n <span className=\"font-medium\">\n {lastMessage.sender.name}:{\" \"}\n </span>\n )}\n {lastMessage.content ||\n tr(\"conversations.sentAttachment\", \"Sent an attachment\")}\n </>\n ) : (\n <span className=\"italic\">\n {tr(\"conversations.noMessages\", \"No messages yet\")}\n </span>\n )}\n </p>\n\n {/* Unread badge + member count */}\n <div className=\"flex items-center gap-1.5 flex-shrink-0\">\n {type !== \"DIRECT\" && (\n <span className=\"text-[10px] text-muted-foreground flex items-center gap-0.5\">\n <Users size={10} />\n {memberCount}\n </span>\n )}\n {hasUnread && (\n <span className=\"inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 text-[10px] font-bold text-primary-foreground bg-primary rounded-full\">\n {unreadCount > 99 ? \"99+\" : unreadCount}\n </span>\n )}\n </div>\n </div>\n\n {sourceContext?.pagePath ? (\n <p className=\"mt-1 truncate text-[11px] text-muted-foreground\">\n {sourceContext.contextTitle || sourceContext.pagePath}\n </p>\n ) : null}\n </div>\n </button>\n );\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"ConversationCard.js","names":[],"sources":["../../../src/conversations/components/ConversationCard.tsx"],"sourcesContent":["import { memo } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n MessageCircle,\n Hash,\n Users,\n Megaphone,\n Bot,\n HeadphonesIcon,\n Pin,\n VolumeX,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport type { Conversation } from \"../types\";\n\ninterface ConversationCardProps {\n conversation: Conversation;\n isActive?: boolean;\n onClick?: (conversation: Conversation) => void;\n}\n\n/**\n * Conversation type icon mapping\n */\nfunction ConversationTypeIcon({\n type,\n size = 16,\n}: {\n type: Conversation[\"type\"];\n size?: number;\n}) {\n switch (type) {\n case \"DIRECT\":\n return <MessageCircle size={size} />;\n case \"GROUP\":\n return <Users size={size} />;\n case \"CHANNEL\":\n return <Hash size={size} />;\n case \"BROADCAST\":\n case \"ANNOUNCEMENT\":\n return <Megaphone size={size} />;\n case \"BOT\":\n return <Bot size={size} />;\n case \"SUPPORT\":\n return <HeadphonesIcon size={size} />;\n default:\n return <MessageCircle size={size} />;\n }\n}\n\n/**\n * Format a timestamp relative to now (e.g., \"2m ago\", \"3h\", \"Yesterday\")\n */\nfunction formatRelativeTime(\n dateString: string,\n labels: { now: string; yesterday: string },\n): string {\n const now = new Date();\n const date = new Date(dateString);\n const diffMs = now.getTime() - date.getTime();\n const diffSec = Math.floor(diffMs / 1000);\n const diffMin = Math.floor(diffSec / 60);\n const diffHr = Math.floor(diffMin / 60);\n const diffDay = Math.floor(diffHr / 24);\n\n if (diffMin < 1) return labels.now;\n if (diffMin < 60) return `${diffMin}m`;\n if (diffHr < 24) return `${diffHr}h`;\n if (diffDay === 1) return labels.yesterday;\n if (diffDay < 7) return `${diffDay}d`;\n return date.toLocaleDateString(undefined, { month: \"short\", day: \"numeric\" });\n}\n\n/**\n * Generate initials from a conversation title\n */\nfunction getInitials(title: string): string {\n return title\n .split(\" \")\n .slice(0, 2)\n .map((word) => word[0])\n .join(\"\")\n .toUpperCase();\n}\n\n/**\n * ConversationCard - Displays a conversation in the list view.\n * Shows avatar, title, last message preview, unread count, and metadata.\n */\nexport const ConversationCard = memo(function ConversationCard({\n conversation,\n isActive = false,\n onClick,\n}: ConversationCardProps) {\n const {\n title,\n type,\n lastMessage,\n unreadCount,\n isPinned,\n isMuted,\n memberCount,\n updatedAt,\n sourceContext,\n } = conversation;\n\n const hasUnread = unreadCount > 0;\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 timeLabels = {\n now: tr(\"conversations.now\", \"now\"),\n yesterday: tr(\"conversations.yesterday\", \"Yesterday\"),\n };\n\n return (\n <button\n type=\"button\"\n onClick={() => onClick?.(conversation)}\n className={cn(\n \"w-full flex items-start gap-3 px-4 py-3 text-left rounded-lg\",\n \"transition-all duration-150 ease-in-out\",\n \"hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n isActive && \"bg-primary/5 border-l-2 border-l-primary\",\n !isActive && \"border-l-2 border-l-transparent\",\n hasUnread && !isActive && \"bg-muted/20\",\n )}\n >\n {/* Avatar / Type Icon */}\n <div\n className={cn(\n \"flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold\",\n type === \"DIRECT\"\n ? \"bg-status-info-bg-subtle text-status-info-text\"\n : type === \"CHANNEL\"\n ? \"bg-status-success-bg-subtle text-status-success-text\"\n : type === \"GROUP\"\n ? \"bg-accent text-accent-foreground\"\n : \"bg-muted text-muted-foreground\",\n )}\n >\n {conversation.avatar ? (\n <img\n src={conversation.avatar}\n alt={title}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : type === \"DIRECT\" ? (\n getInitials(title)\n ) : (\n <ConversationTypeIcon type={type} size={18} />\n )}\n </div>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n {/* Title row */}\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex items-center gap-1.5 min-w-0\">\n {type !== \"DIRECT\" && (\n <ConversationTypeIcon type={type} size={12} />\n )}\n <span\n className={cn(\n \"text-sm truncate\",\n hasUnread\n ? \"font-semibold text-foreground\"\n : \"font-medium text-foreground\",\n )}\n >\n {title}\n </span>\n {isPinned && (\n <Pin size={12} className=\"flex-shrink-0 text-muted-foreground\" />\n )}\n {isMuted && (\n <VolumeX\n size={12}\n className=\"flex-shrink-0 text-muted-foreground\"\n />\n )}\n </div>\n\n {/* Timestamp */}\n <span\n className={cn(\n \"text-xs flex-shrink-0\",\n hasUnread ? \"text-primary font-medium\" : \"text-muted-foreground\",\n )}\n >\n {lastMessage\n ? formatRelativeTime(lastMessage.createdAt, timeLabels)\n : formatRelativeTime(updatedAt, timeLabels)}\n </span>\n </div>\n\n {/* Last message preview */}\n <div className=\"flex items-center justify-between gap-2 mt-0.5\">\n <p\n className={cn(\n \"text-xs truncate\",\n hasUnread ? \"text-foreground/80\" : \"text-muted-foreground\",\n )}\n >\n {lastMessage ? (\n <>\n {lastMessage.sender.name && (\n <span className=\"font-medium\">\n {lastMessage.sender.name}:{\" \"}\n </span>\n )}\n {lastMessage.content ||\n tr(\"conversations.sentAttachment\", \"Sent an attachment\")}\n </>\n ) : (\n <span className=\"italic\">\n {tr(\"conversations.noMessages\", \"No messages yet\")}\n </span>\n )}\n </p>\n\n {/* Unread badge + member count */}\n <div className=\"flex items-center gap-1.5 flex-shrink-0\">\n {type !== \"DIRECT\" && (\n <span className=\"text-[10px] text-muted-foreground flex items-center gap-0.5\">\n <Users size={10} />\n {memberCount}\n </span>\n )}\n {hasUnread && (\n <span className=\"inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 text-[10px] font-bold text-primary-foreground bg-primary rounded-full\">\n {unreadCount > 99 ? \"99+\" : unreadCount}\n </span>\n )}\n </div>\n </div>\n\n {sourceContext?.pagePath ? (\n <p className=\"mt-1 truncate text-[11px] text-muted-foreground\">\n {sourceContext.contextTitle || sourceContext.pagePath}\n </p>\n ) : null}\n </div>\n </button>\n );\n});\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  import { ComponentType, LazyExoticComponent, ReactNode } from 'react';
2
2
 
3
- export declare function lazyRetry<T extends ComponentType<unknown>>(factory: () => Promise<{
4
- default: T;
5
- }>): LazyExoticComponent<T>;
3
+ export declare function lazyRetry<TProps extends object = object>(factory: () => Promise<{
4
+ default: ComponentType<TProps>;
5
+ }>): LazyExoticComponent<ComponentType<TProps>>;
6
6
  /**
7
7
  * Fallback spinner rendered while a lazy-loaded page chunk is resolving.
8
8
  */
@@ -1 +1 @@
1
- {"version":3,"file":"LazyPageBoundary.d.ts","sourceRoot":"","sources":["../../../src/conversations/components/LazyPageBoundary.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,aAAa,EAElB,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AA8Ef,wBAAgB,SAAS,CAAC,CAAC,SAAS,aAAa,CAAC,OAAO,CAAC,EACxD,OAAO,EAAE,MAAM,OAAO,CAAC;IAAE,OAAO,EAAE,CAAC,CAAA;CAAE,CAAC,GACrC,mBAAmB,CAAC,CAAC,CAAC,CAaxB;AAED;;GAEG;AACH,wBAAgB,UAAU,4CAMzB;AAqND;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,SAAS,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,2CAUA"}
1
+ {"version":3,"file":"LazyPageBoundary.d.ts","sourceRoot":"","sources":["../../../src/conversations/components/LazyPageBoundary.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,aAAa,EAElB,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AA8Ef,wBAAgB,SAAS,CAAC,MAAM,SAAS,MAAM,GAAG,MAAM,EACtD,OAAO,EAAE,MAAM,OAAO,CAAC;IAAE,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,GACzD,mBAAmB,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAe5C;AAED;;GAEG;AACH,wBAAgB,UAAU,4CAMzB;AAqND;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,SAAS,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,2CAUA"}
@@ -1 +1 @@
1
- {"version":3,"file":"LazyPageBoundary.js","names":[],"sources":["../../../src/conversations/components/LazyPageBoundary.tsx"],"sourcesContent":["import {\n Component,\n Suspense,\n lazy,\n type ComponentType,\n type ErrorInfo,\n type LazyExoticComponent,\n type ReactNode,\n} from \"react\";\nimport { Loader2, AlertTriangle, RefreshCw } from \"lucide-react\";\nimport { useTr } from \"../utils/i18n\";\n\n/**\n * Heuristic match for chunk-load failures raised by the runtime bundler.\n *\n * Covers:\n * - Webpack: `error.name === \"ChunkLoadError\"`, `\"Loading chunk X failed\"`\n * - Vite / modern ESM: `\"Failed to fetch dynamically imported module\"`\n * - Safari module import failures: `\"Importing a module script failed\"`\n * - Generic `TypeError: Failed to fetch` thrown from a dynamic import whose\n * module path was stripped by the browser (observed in some Vite+proxy\n * configurations where the URL never reaches the origin)\n *\n * This is inherently heuristic — different bundlers and browsers phrase\n * chunk-load failures differently and new phrasings can appear. We err on\n * the side of matching rather than missing; a false positive triggers a\n * page reload which is still a reasonable recovery for other transient\n * network-side failures. A false negative leaves the boundary in its\n * state-reset path, which is also safe (just less effective than reload\n * for a true chunk failure).\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 // Vite + some CDN/proxy combos surface as \"TypeError: Failed to fetch\"\n // on a dynamic `import()` call. Match the bare phrase as a last-resort\n // hint; there's no cleaner signal short of probing the stack trace.\n message.includes(\"Failed to fetch\")\n );\n}\n\n/**\n * Retry-aware `React.lazy` wrapper.\n *\n * `React.lazy` calls the factory once and caches the resulting promise.\n * If that promise rejects, the cached rejection is replayed on every\n * subsequent render of the same `LazyExoticComponent` — a plain\n * error-boundary reset simply re-throws the cached error without\n * issuing a new network request.\n *\n * `lazyRetry` gives the initial chunk load one silent auto-retry — but\n * **only for transient chunk-load failures** detected by `isChunkLoadError`.\n * Non-transient errors (import syntax errors, missing exports, bundler\n * configuration bugs) are rethrown after the first failure so the\n * `LazyErrorBoundary` activates immediately instead of doubling the\n * latency with a guaranteed-to-fail second network round-trip.\n *\n * For a user-driven retry after *both* attempts fail, see\n * `LazyPageBoundary` below: the only reliable way to discard the cached\n * rejection on a `LazyExoticComponent` is a full page reload, so the\n * boundary's \"Try again\" button triggers `window.location.reload()` for\n * detected chunk-load failures. For non-chunk render errors the boundary\n * instead resets its state and bumps a `retryKey` so the suspense\n * subtree re-mounts cleanly.\n *\n * @example\n * const AdminModerationPage = lazyRetry(() =>\n * import(\"./pages/AdminModerationPage\").then((m) => ({\n * default: m.AdminModerationPage,\n * })),\n * );\n */\n/**\n * Small delay before the silent auto-retry in `lazyRetry`. Fires between\n * the first failed `import()` and the second attempt so stale CDN / edge\n * caches have a chance to revalidate before the second request lands.\n * A true network outage still surfaces after this delay; a cache-miss\n * typically recovers.\n */\nconst LAZY_RETRY_DELAY_MS = 300;\n\nexport function lazyRetry<T extends ComponentType<unknown>>(\n factory: () => Promise<{ default: T }>,\n): LazyExoticComponent<T> {\n return lazy(() =>\n factory().catch((err: unknown) => {\n if (err instanceof Error && isChunkLoadError(err)) {\n return new Promise<{ default: T }>((resolve, reject) => {\n setTimeout(() => {\n factory().then(resolve, reject);\n }, LAZY_RETRY_DELAY_MS);\n });\n }\n throw err;\n }),\n );\n}\n\n/**\n * Fallback spinner rendered while a lazy-loaded page chunk is resolving.\n */\nexport function PageLoader() {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <Loader2 className=\"h-8 w-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=\"h-8 w-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=\"h-4 w-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 {\n Component,\n Suspense,\n lazy,\n type ComponentType,\n type ErrorInfo,\n type LazyExoticComponent,\n type ReactNode,\n} from \"react\";\nimport { Loader2, AlertTriangle, RefreshCw } from \"lucide-react\";\nimport { useTr } from \"../utils/i18n\";\n\n/**\n * Heuristic match for chunk-load failures raised by the runtime bundler.\n *\n * Covers:\n * - Webpack: `error.name === \"ChunkLoadError\"`, `\"Loading chunk X failed\"`\n * - Vite / modern ESM: `\"Failed to fetch dynamically imported module\"`\n * - Safari module import failures: `\"Importing a module script failed\"`\n * - Generic `TypeError: Failed to fetch` thrown from a dynamic import whose\n * module path was stripped by the browser (observed in some Vite+proxy\n * configurations where the URL never reaches the origin)\n *\n * This is inherently heuristic — different bundlers and browsers phrase\n * chunk-load failures differently and new phrasings can appear. We err on\n * the side of matching rather than missing; a false positive triggers a\n * page reload which is still a reasonable recovery for other transient\n * network-side failures. A false negative leaves the boundary in its\n * state-reset path, which is also safe (just less effective than reload\n * for a true chunk failure).\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 // Vite + some CDN/proxy combos surface as \"TypeError: Failed to fetch\"\n // on a dynamic `import()` call. Match the bare phrase as a last-resort\n // hint; there's no cleaner signal short of probing the stack trace.\n message.includes(\"Failed to fetch\")\n );\n}\n\n/**\n * Retry-aware `React.lazy` wrapper.\n *\n * `React.lazy` calls the factory once and caches the resulting promise.\n * If that promise rejects, the cached rejection is replayed on every\n * subsequent render of the same `LazyExoticComponent` — a plain\n * error-boundary reset simply re-throws the cached error without\n * issuing a new network request.\n *\n * `lazyRetry` gives the initial chunk load one silent auto-retry — but\n * **only for transient chunk-load failures** detected by `isChunkLoadError`.\n * Non-transient errors (import syntax errors, missing exports, bundler\n * configuration bugs) are rethrown after the first failure so the\n * `LazyErrorBoundary` activates immediately instead of doubling the\n * latency with a guaranteed-to-fail second network round-trip.\n *\n * For a user-driven retry after *both* attempts fail, see\n * `LazyPageBoundary` below: the only reliable way to discard the cached\n * rejection on a `LazyExoticComponent` is a full page reload, so the\n * boundary's \"Try again\" button triggers `window.location.reload()` for\n * detected chunk-load failures. For non-chunk render errors the boundary\n * instead resets its state and bumps a `retryKey` so the suspense\n * subtree re-mounts cleanly.\n *\n * @example\n * const AdminModerationPage = lazyRetry(() =>\n * import(\"./pages/AdminModerationPage\").then((m) => ({\n * default: m.AdminModerationPage,\n * })),\n * );\n */\n/**\n * Small delay before the silent auto-retry in `lazyRetry`. Fires between\n * the first failed `import()` and the second attempt so stale CDN / edge\n * caches have a chance to revalidate before the second request lands.\n * A true network outage still surfaces after this delay; a cache-miss\n * typically recovers.\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\n/**\n * Fallback spinner rendered while a lazy-loaded page chunk is resolving.\n */\nexport function PageLoader() {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <Loader2 className=\"h-8 w-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=\"h-8 w-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=\"h-4 w-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":"MessageBubble.d.ts","sourceRoot":"","sources":["../../../src/conversations/components/MessageBubble.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,UAAU,kBAAkB;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACpC,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;CAC9D;AAiDD;;;;GAIG;AACH,eAAO,MAAM,aAAa,0DA6SxB,CAAC"}
1
+ {"version":3,"file":"MessageBubble.d.ts","sourceRoot":"","sources":["../../../src/conversations/components/MessageBubble.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,UAAU,kBAAkB;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACpC,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;CAC9D;AAiDD;;;;GAIG;AACH,eAAO,MAAM,aAAa,0DAwSxB,CAAC"}
@@ -23,7 +23,7 @@ function v({ status: e, failedLabel: t }) {
23
23
  });
24
24
  case "READ": return /* @__PURE__ */ m(i, {
25
25
  size: 12,
26
- className: "text-accent-blue"
26
+ className: "text-primary"
27
27
  });
28
28
  case "FAILED": return /* @__PURE__ */ m("span", {
29
29
  className: "text-[10px] text-destructive font-medium",
@@ -36,28 +36,26 @@ function y(e) {
36
36
  return e.startsWith("image/");
37
37
  }
38
38
  var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showSender: b = !0, onReply: C, onReact: w, onEdit: T, onDelete: E, onTogglePin: D }) {
39
- let [O, k] = n(!1), [A, j] = n(!1), [M, N] = n(!1), { t: P } = g(), F = (e, t) => {
40
- let n = P(e);
39
+ let [O, k] = n(!1), [A, j] = n(!1), { t: M } = g(), N = (e, t) => {
40
+ let n = M(e);
41
41
  return n === e ? t : n;
42
- }, { sender: I, content: L, type: R, status: z, reactions: B, attachments: V, isEdited: H, createdAt: U, isPinned: W } = t;
43
- return R === "SYSTEM" ? /* @__PURE__ */ m("div", {
42
+ }, { sender: P, content: F, type: I, status: L, reactions: R, attachments: z, isEdited: B, createdAt: V, isPinned: H } = t;
43
+ return I === "SYSTEM" ? /* @__PURE__ */ m("div", {
44
44
  className: "flex justify-center my-2 px-4",
45
45
  children: /* @__PURE__ */ m("span", {
46
46
  className: "text-xs text-muted-foreground bg-muted/50 px-3 py-1 rounded-full",
47
- children: L
47
+ children: F
48
48
  })
49
49
  }) : /* @__PURE__ */ h("div", {
50
50
  className: e("group flex gap-2 px-4 py-1", r ? "flex-row-reverse" : "flex-row"),
51
- onMouseEnter: () => k(!0),
52
- onMouseLeave: () => k(!1),
53
51
  children: [
54
52
  i ? /* @__PURE__ */ m("div", {
55
53
  className: e("flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5", r ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground"),
56
- children: I.avatarUrl ? /* @__PURE__ */ m("img", {
57
- src: I.avatarUrl,
58
- alt: I.name,
54
+ children: P.avatarUrl ? /* @__PURE__ */ m("img", {
55
+ src: P.avatarUrl,
56
+ alt: P.name,
59
57
  className: "w-full h-full rounded-full object-cover"
60
- }) : I.name.split(" ").map((e) => e[0]).join("").toUpperCase().slice(0, 2)
58
+ }) : P.name.split(" ").map((e) => e[0]).join("").toUpperCase().slice(0, 2)
61
59
  }) : /* @__PURE__ */ m("div", { className: "w-8 flex-shrink-0" }),
62
60
  /* @__PURE__ */ h("div", {
63
61
  className: e("flex flex-col max-w-[85%] sm:max-w-[70%]", r ? "items-end" : "items-start"),
@@ -66,8 +64,8 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
66
64
  className: "flex items-center gap-1.5 mb-0.5",
67
65
  children: [/* @__PURE__ */ m("span", {
68
66
  className: "text-xs font-semibold text-foreground",
69
- children: I.name
70
- }), W && /* @__PURE__ */ m(l, {
67
+ children: P.name
68
+ }), H && /* @__PURE__ */ m(l, {
71
69
  size: 10,
72
70
  className: "text-status-warning-text"
73
71
  })]
@@ -76,10 +74,10 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
76
74
  className: e("relative rounded-2xl px-3.5 py-2 text-sm leading-relaxed", r ? "bg-primary text-primary-foreground rounded-tr-md" : "bg-muted/70 text-foreground rounded-tl-md"),
77
75
  children: [/* @__PURE__ */ m("p", {
78
76
  className: "whitespace-pre-wrap break-words",
79
- children: L
80
- }), V.length > 0 && /* @__PURE__ */ m("div", {
77
+ children: F
78
+ }), z.length > 0 && /* @__PURE__ */ m("div", {
81
79
  className: "mt-2 grid gap-2",
82
- children: V.map((t) => {
80
+ children: z.map((t) => {
83
81
  let n = e("flex min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-xs transition-colors", r ? "bg-primary-foreground/10 text-primary-foreground hover:bg-primary-foreground/15" : "bg-background/50 text-foreground hover:bg-background/70"), i = /* @__PURE__ */ h(p, { children: [/* @__PURE__ */ m("div", {
84
82
  className: "flex size-10 flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-background/40",
85
83
  children: t.thumbnail && y(t.mimeType) ? /* @__PURE__ */ m("img", {
@@ -116,9 +114,9 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
116
114
  })
117
115
  })]
118
116
  }),
119
- B.length > 0 && /* @__PURE__ */ m("div", {
117
+ R.length > 0 && /* @__PURE__ */ m("div", {
120
118
  className: "flex flex-wrap gap-1 mt-1",
121
- children: B.map((n) => /* @__PURE__ */ h("button", {
119
+ children: R.map((n) => /* @__PURE__ */ h("button", {
122
120
  type: "button",
123
121
  onClick: () => w?.(t.id, n.emoji),
124
122
  className: e("inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs", "border transition-colors", n.currentUserReacted ? "bg-primary/10 border-primary/30 text-primary" : "bg-muted/50 border-border text-muted-foreground hover:bg-muted"),
@@ -133,55 +131,55 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
133
131
  children: [
134
132
  /* @__PURE__ */ m("span", {
135
133
  className: "text-[10px] text-muted-foreground",
136
- children: _(U)
134
+ children: _(V)
137
135
  }),
138
- H && /* @__PURE__ */ m("span", {
136
+ B && /* @__PURE__ */ m("span", {
139
137
  className: "text-[10px] text-muted-foreground italic",
140
- children: F("conversations.edited", "edited")
138
+ children: N("conversations.edited", "edited")
141
139
  }),
142
140
  r && /* @__PURE__ */ m(v, {
143
- status: z,
144
- failedLabel: F("conversations.failed", "Failed")
141
+ status: L,
142
+ failedLabel: N("conversations.failed", "Failed")
145
143
  })
146
144
  ]
147
145
  })
148
146
  ]
149
147
  }),
150
- (O || A || M) && /* @__PURE__ */ h("div", {
151
- className: e("relative flex items-center gap-0.5 self-start mt-1", "animate-in fade-in-0 zoom-in-95 duration-150"),
148
+ /* @__PURE__ */ h("div", {
149
+ className: e("relative mt-1 flex items-center gap-0.5 self-start opacity-100 transition-opacity duration-150 sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100", (O || A) && "sm:opacity-100"),
152
150
  children: [
153
151
  t.permissions.canReact && /* @__PURE__ */ m(x, {
154
152
  icon: /* @__PURE__ */ m(d, { size: 14 }),
155
- label: F("conversations.react", "React"),
156
- pressed: A,
153
+ label: N("conversations.react", "React"),
154
+ pressed: O,
157
155
  onClick: () => {
158
- j((e) => !e), N(!1);
156
+ k((e) => !e), j(!1);
159
157
  }
160
158
  }),
161
159
  t.permissions.canReply && /* @__PURE__ */ m(x, {
162
160
  icon: /* @__PURE__ */ m(u, { size: 14 }),
163
- label: F("conversations.reply", "Reply"),
161
+ label: N("conversations.reply", "Reply"),
164
162
  onClick: () => C?.(t)
165
163
  }),
166
164
  t.permissions.canEdit && /* @__PURE__ */ m(x, {
167
165
  icon: /* @__PURE__ */ m(c, { size: 14 }),
168
- label: F("conversations.edit", "Edit"),
166
+ label: N("conversations.edit", "Edit"),
169
167
  onClick: () => T?.(t)
170
168
  }),
171
169
  t.permissions.canDelete && /* @__PURE__ */ m(x, {
172
170
  icon: /* @__PURE__ */ m(f, { size: 14 }),
173
- label: F("conversations.delete", "Delete"),
171
+ label: N("conversations.delete", "Delete"),
174
172
  onClick: () => E?.(t.id)
175
173
  }),
176
174
  /* @__PURE__ */ m(x, {
177
175
  icon: /* @__PURE__ */ m(s, { size: 14 }),
178
- label: F("conversations.more", "More"),
179
- pressed: M,
176
+ label: N("conversations.more", "More"),
177
+ pressed: A,
180
178
  onClick: () => {
181
- N((e) => !e), j(!1);
179
+ j((e) => !e), k(!1);
182
180
  }
183
181
  }),
184
- A && /* @__PURE__ */ m("div", {
182
+ O && /* @__PURE__ */ m("div", {
185
183
  className: "absolute top-full z-10 mt-1 flex items-center gap-1 rounded-lg border border-border bg-popover p-1 shadow-lg",
186
184
  children: [
187
185
  "👍",
@@ -192,22 +190,22 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
192
190
  ].map((e) => /* @__PURE__ */ m("button", {
193
191
  type: "button",
194
192
  onClick: () => {
195
- w?.(t.id, e), j(!1);
193
+ w?.(t.id, e), k(!1);
196
194
  },
197
195
  className: "rounded-md px-2 py-1 text-sm hover:bg-muted/80",
198
- "aria-label": `${F("conversations.reactWith", "React with")} ${e}`,
196
+ "aria-label": `${N("conversations.reactWith", "React with")} ${e}`,
199
197
  children: e
200
198
  }, e))
201
199
  }),
202
- M && t.permissions.canPin && D && /* @__PURE__ */ m("div", {
200
+ A && t.permissions.canPin && D && /* @__PURE__ */ m("div", {
203
201
  className: "absolute right-0 top-full z-10 mt-1 min-w-40 rounded-lg border border-border bg-popover p-1 shadow-lg",
204
202
  children: /* @__PURE__ */ h("button", {
205
203
  type: "button",
206
204
  onClick: () => {
207
- D(t.id, t.isPinned), N(!1);
205
+ D(t.id, t.isPinned), j(!1);
208
206
  },
209
207
  className: "flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-foreground hover:bg-muted/80",
210
- children: [/* @__PURE__ */ m(l, { size: 14 }), t.isPinned ? F("conversations.unpinMessage", "Unpin message") : F("conversations.pinMessage", "Pin message")]
208
+ children: [/* @__PURE__ */ m(l, { size: 14 }), t.isPinned ? N("conversations.unpinMessage", "Unpin message") : N("conversations.pinMessage", "Pin message")]
211
209
  })
212
210
  })
213
211
  ]
@@ -222,7 +220,7 @@ function x({ icon: e, label: t, onClick: n, pressed: r }) {
222
220
  "aria-label": t,
223
221
  "aria-pressed": r,
224
222
  title: t,
225
- className: "p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/80 transition-colors",
223
+ className: "flex min-h-9 min-w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted/80 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
226
224
  children: e
227
225
  });
228
226
  }
@@ -1 +1 @@
1
- {"version":3,"file":"MessageBubble.js","names":[],"sources":["../../../src/conversations/components/MessageBubble.tsx"],"sourcesContent":["import { memo, useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n FileText,\n Image as ImageIcon,\n Reply,\n MoreHorizontal,\n Pencil,\n Trash2,\n Pin,\n SmilePlus,\n Check,\n CheckCheck,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport type { Message } from \"../types\";\n\ninterface MessageBubbleProps {\n message: Message;\n isOwnMessage?: boolean;\n showAvatar?: boolean;\n showSender?: boolean;\n onReply?: (message: Message) => void;\n onReact?: (messageId: string, emoji: string) => void;\n onEdit?: (message: Message) => void;\n onDelete?: (messageId: string) => void;\n onTogglePin?: (messageId: string, isPinned: boolean) => void;\n}\n\n/**\n * Format a message timestamp for display\n */\nfunction formatMessageTime(dateString: string): string {\n const date = new Date(dateString);\n return date.toLocaleTimeString(undefined, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\n/**\n * Message status indicator\n */\nfunction MessageStatusIcon({\n status,\n failedLabel,\n}: {\n status: Message[\"status\"];\n failedLabel: string;\n}) {\n switch (status) {\n case \"SENDING\":\n return (\n <span className=\"w-3 h-3 rounded-full border border-muted-foreground animate-pulse\" />\n );\n case \"SENT\":\n return <Check size={12} className=\"text-muted-foreground\" />;\n case \"DELIVERED\":\n return <CheckCheck size={12} className=\"text-muted-foreground\" />;\n case \"READ\":\n return <CheckCheck size={12} className=\"text-accent-blue\" />;\n case \"FAILED\":\n return (\n <span className=\"text-[10px] text-destructive font-medium\">\n {failedLabel}\n </span>\n );\n default:\n return null;\n }\n}\n\nfunction isImageAttachment(mimeType: string): boolean {\n return mimeType.startsWith(\"image/\");\n}\n\n/**\n * MessageBubble - Displays a single message in a chat view.\n * Supports own messages (right-aligned) and other messages (left-aligned).\n * Shows sender info, content, timestamp, reactions, and action buttons.\n */\nexport const MessageBubble = memo(function MessageBubble({\n message,\n isOwnMessage = false,\n showAvatar = true,\n showSender = true,\n onReply,\n onReact,\n onEdit,\n onDelete,\n onTogglePin,\n}: MessageBubbleProps) {\n const [isHovered, setIsHovered] = useState(false);\n const [showReactionPicker, setShowReactionPicker] = useState(false);\n const [showMoreMenu, setShowMoreMenu] = useState(false);\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const {\n sender,\n content,\n type,\n status,\n reactions,\n attachments,\n isEdited,\n createdAt,\n isPinned,\n } = message;\n\n const isSystem = type === \"SYSTEM\";\n\n // System messages render differently\n if (isSystem) {\n return (\n <div className=\"flex justify-center my-2 px-4\">\n <span className=\"text-xs text-muted-foreground bg-muted/50 px-3 py-1 rounded-full\">\n {content}\n </span>\n </div>\n );\n }\n\n return (\n <div\n className={cn(\n \"group flex gap-2 px-4 py-1\",\n isOwnMessage ? \"flex-row-reverse\" : \"flex-row\",\n )}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n {/* Avatar */}\n {showAvatar ? (\n <div\n className={cn(\n \"flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5\",\n isOwnMessage\n ? \"bg-primary/10 text-primary\"\n : \"bg-muted text-muted-foreground\",\n )}\n >\n {sender.avatarUrl ? (\n <img\n src={sender.avatarUrl}\n alt={sender.name}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : (\n sender.name\n .split(\" \")\n .map((w) => w[0])\n .join(\"\")\n .toUpperCase()\n .slice(0, 2)\n )}\n </div>\n ) : (\n <div className=\"w-8 flex-shrink-0\" />\n )}\n\n {/* Message content */}\n <div\n className={cn(\n \"flex flex-col max-w-[85%] sm:max-w-[70%]\",\n isOwnMessage ? \"items-end\" : \"items-start\",\n )}\n >\n {/* Sender name + pin indicator */}\n {showSender && !isOwnMessage && (\n <div className=\"flex items-center gap-1.5 mb-0.5\">\n <span className=\"text-xs font-semibold text-foreground\">\n {sender.name}\n </span>\n {isPinned && <Pin size={10} className=\"text-status-warning-text\" />}\n </div>\n )}\n\n {/* Bubble */}\n <div\n className={cn(\n \"relative rounded-2xl px-3.5 py-2 text-sm leading-relaxed\",\n isOwnMessage\n ? \"bg-primary text-primary-foreground rounded-tr-md\"\n : \"bg-muted/70 text-foreground rounded-tl-md\",\n )}\n >\n {/* Text content */}\n <p className=\"whitespace-pre-wrap break-words\">{content}</p>\n\n {/* Attachments */}\n {attachments.length > 0 && (\n <div className=\"mt-2 grid gap-2\">\n {attachments.map((att) => {\n const attachmentClass = cn(\n \"flex min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-xs transition-colors\",\n isOwnMessage\n ? \"bg-primary-foreground/10 text-primary-foreground hover:bg-primary-foreground/15\"\n : \"bg-background/50 text-foreground hover:bg-background/70\",\n );\n const attachmentContent = (\n <>\n <div className=\"flex size-10 flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-background/40\">\n {att.thumbnail && isImageAttachment(att.mimeType) ? (\n <img\n src={att.thumbnail}\n alt={att.fileName}\n className=\"h-full w-full object-contain\"\n />\n ) : isImageAttachment(att.mimeType) ? (\n <ImageIcon size={16} />\n ) : (\n <FileText size={16} />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate font-medium\">{att.fileName}</div>\n <div className=\"truncate text-[10px] opacity-70\">\n {formatFileSize(att.fileSize)} - {att.mimeType}\n </div>\n </div>\n </>\n );\n\n return att.downloadUrl ? (\n <a\n key={att.id}\n href={att.downloadUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className={attachmentClass}\n title={att.fileName}\n >\n {attachmentContent}\n </a>\n ) : (\n <div\n key={att.id}\n className={attachmentClass}\n title={att.fileName}\n >\n {attachmentContent}\n </div>\n );\n })}\n </div>\n )}\n </div>\n\n {/* Reactions */}\n {reactions.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mt-1\">\n {reactions.map((reaction) => (\n <button\n key={reaction.emoji}\n type=\"button\"\n onClick={() => onReact?.(message.id, reaction.emoji)}\n className={cn(\n \"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs\",\n \"border transition-colors\",\n reaction.currentUserReacted\n ? \"bg-primary/10 border-primary/30 text-primary\"\n : \"bg-muted/50 border-border text-muted-foreground hover:bg-muted\",\n )}\n >\n <span>{reaction.emoji}</span>\n <span className=\"font-medium\">{reaction.count}</span>\n </button>\n ))}\n </div>\n )}\n\n {/* Timestamp + status row */}\n <div className=\"flex items-center gap-1.5 mt-0.5 px-1\">\n <span className=\"text-[10px] text-muted-foreground\">\n {formatMessageTime(createdAt)}\n </span>\n {isEdited && (\n <span className=\"text-[10px] text-muted-foreground italic\">\n {tr(\"conversations.edited\", \"edited\")}\n </span>\n )}\n {isOwnMessage && (\n <MessageStatusIcon\n status={status}\n failedLabel={tr(\"conversations.failed\", \"Failed\")}\n />\n )}\n </div>\n </div>\n\n {/* Action buttons (shown on hover) */}\n {(isHovered || showReactionPicker || showMoreMenu) && (\n <div\n className={cn(\n \"relative flex items-center gap-0.5 self-start mt-1\",\n \"animate-in fade-in-0 zoom-in-95 duration-150\",\n )}\n >\n {message.permissions.canReact && (\n <ActionButton\n icon={<SmilePlus size={14} />}\n label={tr(\"conversations.react\", \"React\")}\n pressed={showReactionPicker}\n onClick={() => {\n setShowReactionPicker((value) => !value);\n setShowMoreMenu(false);\n }}\n />\n )}\n {message.permissions.canReply && (\n <ActionButton\n icon={<Reply size={14} />}\n label={tr(\"conversations.reply\", \"Reply\")}\n onClick={() => onReply?.(message)}\n />\n )}\n {message.permissions.canEdit && (\n <ActionButton\n icon={<Pencil size={14} />}\n label={tr(\"conversations.edit\", \"Edit\")}\n onClick={() => onEdit?.(message)}\n />\n )}\n {message.permissions.canDelete && (\n <ActionButton\n icon={<Trash2 size={14} />}\n label={tr(\"conversations.delete\", \"Delete\")}\n onClick={() => onDelete?.(message.id)}\n />\n )}\n <ActionButton\n icon={<MoreHorizontal size={14} />}\n label={tr(\"conversations.more\", \"More\")}\n pressed={showMoreMenu}\n onClick={() => {\n setShowMoreMenu((value) => !value);\n setShowReactionPicker(false);\n }}\n />\n {showReactionPicker && (\n <div className=\"absolute top-full z-10 mt-1 flex items-center gap-1 rounded-lg border border-border bg-popover p-1 shadow-lg\">\n {[\"👍\", \"❤️\", \"🎉\", \"👀\", \"✅\"].map((emoji) => (\n <button\n key={emoji}\n type=\"button\"\n onClick={() => {\n onReact?.(message.id, emoji);\n setShowReactionPicker(false);\n }}\n className=\"rounded-md px-2 py-1 text-sm hover:bg-muted/80\"\n aria-label={`${tr(\"conversations.reactWith\", \"React with\")} ${emoji}`}\n >\n {emoji}\n </button>\n ))}\n </div>\n )}\n {showMoreMenu && message.permissions.canPin && onTogglePin && (\n <div className=\"absolute right-0 top-full z-10 mt-1 min-w-40 rounded-lg border border-border bg-popover p-1 shadow-lg\">\n <button\n type=\"button\"\n onClick={() => {\n onTogglePin(message.id, message.isPinned);\n setShowMoreMenu(false);\n }}\n className=\"flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-foreground hover:bg-muted/80\"\n >\n <Pin size={14} />\n {message.isPinned\n ? tr(\"conversations.unpinMessage\", \"Unpin message\")\n : tr(\"conversations.pinMessage\", \"Pin message\")}\n </button>\n </div>\n )}\n </div>\n )}\n </div>\n );\n});\n\n/**\n * Small icon button for message actions\n */\nfunction ActionButton({\n icon,\n label,\n onClick,\n pressed,\n}: {\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n pressed?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n aria-pressed={pressed}\n title={label}\n className=\"p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/80 transition-colors\"\n >\n {icon}\n </button>\n );\n}\n\n/**\n * Format file size in human-readable form\n */\nfunction formatFileSize(bytes: number): string {\n if (bytes === 0) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"MessageBubble.js","names":[],"sources":["../../../src/conversations/components/MessageBubble.tsx"],"sourcesContent":["import { memo, useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n FileText,\n Image as ImageIcon,\n Reply,\n MoreHorizontal,\n Pencil,\n Trash2,\n Pin,\n SmilePlus,\n Check,\n CheckCheck,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport type { Message } from \"../types\";\n\ninterface MessageBubbleProps {\n message: Message;\n isOwnMessage?: boolean;\n showAvatar?: boolean;\n showSender?: boolean;\n onReply?: (message: Message) => void;\n onReact?: (messageId: string, emoji: string) => void;\n onEdit?: (message: Message) => void;\n onDelete?: (messageId: string) => void;\n onTogglePin?: (messageId: string, isPinned: boolean) => void;\n}\n\n/**\n * Format a message timestamp for display\n */\nfunction formatMessageTime(dateString: string): string {\n const date = new Date(dateString);\n return date.toLocaleTimeString(undefined, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\n/**\n * Message status indicator\n */\nfunction MessageStatusIcon({\n status,\n failedLabel,\n}: {\n status: Message[\"status\"];\n failedLabel: string;\n}) {\n switch (status) {\n case \"SENDING\":\n return (\n <span className=\"w-3 h-3 rounded-full border border-muted-foreground animate-pulse\" />\n );\n case \"SENT\":\n return <Check size={12} className=\"text-muted-foreground\" />;\n case \"DELIVERED\":\n return <CheckCheck size={12} className=\"text-muted-foreground\" />;\n case \"READ\":\n return <CheckCheck size={12} className=\"text-primary\" />;\n case \"FAILED\":\n return (\n <span className=\"text-[10px] text-destructive font-medium\">\n {failedLabel}\n </span>\n );\n default:\n return null;\n }\n}\n\nfunction isImageAttachment(mimeType: string): boolean {\n return mimeType.startsWith(\"image/\");\n}\n\n/**\n * MessageBubble - Displays a single message in a chat view.\n * Supports own messages (right-aligned) and other messages (left-aligned).\n * Shows sender info, content, timestamp, reactions, and action buttons.\n */\nexport const MessageBubble = memo(function MessageBubble({\n message,\n isOwnMessage = false,\n showAvatar = true,\n showSender = true,\n onReply,\n onReact,\n onEdit,\n onDelete,\n onTogglePin,\n}: MessageBubbleProps) {\n const [showReactionPicker, setShowReactionPicker] = useState(false);\n const [showMoreMenu, setShowMoreMenu] = useState(false);\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const {\n sender,\n content,\n type,\n status,\n reactions,\n attachments,\n isEdited,\n createdAt,\n isPinned,\n } = message;\n\n const isSystem = type === \"SYSTEM\";\n\n // System messages render differently\n if (isSystem) {\n return (\n <div className=\"flex justify-center my-2 px-4\">\n <span className=\"text-xs text-muted-foreground bg-muted/50 px-3 py-1 rounded-full\">\n {content}\n </span>\n </div>\n );\n }\n\n return (\n <div\n className={cn(\n \"group flex gap-2 px-4 py-1\",\n isOwnMessage ? \"flex-row-reverse\" : \"flex-row\",\n )}\n >\n {/* Avatar */}\n {showAvatar ? (\n <div\n className={cn(\n \"flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5\",\n isOwnMessage\n ? \"bg-primary/10 text-primary\"\n : \"bg-muted text-muted-foreground\",\n )}\n >\n {sender.avatarUrl ? (\n <img\n src={sender.avatarUrl}\n alt={sender.name}\n className=\"w-full h-full rounded-full object-cover\"\n />\n ) : (\n sender.name\n .split(\" \")\n .map((w) => w[0])\n .join(\"\")\n .toUpperCase()\n .slice(0, 2)\n )}\n </div>\n ) : (\n <div className=\"w-8 flex-shrink-0\" />\n )}\n\n {/* Message content */}\n <div\n className={cn(\n \"flex flex-col max-w-[85%] sm:max-w-[70%]\",\n isOwnMessage ? \"items-end\" : \"items-start\",\n )}\n >\n {/* Sender name + pin indicator */}\n {showSender && !isOwnMessage && (\n <div className=\"flex items-center gap-1.5 mb-0.5\">\n <span className=\"text-xs font-semibold text-foreground\">\n {sender.name}\n </span>\n {isPinned && <Pin size={10} className=\"text-status-warning-text\" />}\n </div>\n )}\n\n {/* Bubble */}\n <div\n className={cn(\n \"relative rounded-2xl px-3.5 py-2 text-sm leading-relaxed\",\n isOwnMessage\n ? \"bg-primary text-primary-foreground rounded-tr-md\"\n : \"bg-muted/70 text-foreground rounded-tl-md\",\n )}\n >\n {/* Text content */}\n <p className=\"whitespace-pre-wrap break-words\">{content}</p>\n\n {/* Attachments */}\n {attachments.length > 0 && (\n <div className=\"mt-2 grid gap-2\">\n {attachments.map((att) => {\n const attachmentClass = cn(\n \"flex min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-xs transition-colors\",\n isOwnMessage\n ? \"bg-primary-foreground/10 text-primary-foreground hover:bg-primary-foreground/15\"\n : \"bg-background/50 text-foreground hover:bg-background/70\",\n );\n const attachmentContent = (\n <>\n <div className=\"flex size-10 flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-background/40\">\n {att.thumbnail && isImageAttachment(att.mimeType) ? (\n <img\n src={att.thumbnail}\n alt={att.fileName}\n className=\"h-full w-full object-contain\"\n />\n ) : isImageAttachment(att.mimeType) ? (\n <ImageIcon size={16} />\n ) : (\n <FileText size={16} />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate font-medium\">{att.fileName}</div>\n <div className=\"truncate text-[10px] opacity-70\">\n {formatFileSize(att.fileSize)} - {att.mimeType}\n </div>\n </div>\n </>\n );\n\n return att.downloadUrl ? (\n <a\n key={att.id}\n href={att.downloadUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className={attachmentClass}\n title={att.fileName}\n >\n {attachmentContent}\n </a>\n ) : (\n <div\n key={att.id}\n className={attachmentClass}\n title={att.fileName}\n >\n {attachmentContent}\n </div>\n );\n })}\n </div>\n )}\n </div>\n\n {/* Reactions */}\n {reactions.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mt-1\">\n {reactions.map((reaction) => (\n <button\n key={reaction.emoji}\n type=\"button\"\n onClick={() => onReact?.(message.id, reaction.emoji)}\n className={cn(\n \"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs\",\n \"border transition-colors\",\n reaction.currentUserReacted\n ? \"bg-primary/10 border-primary/30 text-primary\"\n : \"bg-muted/50 border-border text-muted-foreground hover:bg-muted\",\n )}\n >\n <span>{reaction.emoji}</span>\n <span className=\"font-medium\">{reaction.count}</span>\n </button>\n ))}\n </div>\n )}\n\n {/* Timestamp + status row */}\n <div className=\"flex items-center gap-1.5 mt-0.5 px-1\">\n <span className=\"text-[10px] text-muted-foreground\">\n {formatMessageTime(createdAt)}\n </span>\n {isEdited && (\n <span className=\"text-[10px] text-muted-foreground italic\">\n {tr(\"conversations.edited\", \"edited\")}\n </span>\n )}\n {isOwnMessage && (\n <MessageStatusIcon\n status={status}\n failedLabel={tr(\"conversations.failed\", \"Failed\")}\n />\n )}\n </div>\n </div>\n\n {/* Action buttons (shown on hover) */}\n <div\n className={cn(\n \"relative mt-1 flex items-center gap-0.5 self-start opacity-100 transition-opacity duration-150 sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100\",\n (showReactionPicker || showMoreMenu) && \"sm:opacity-100\",\n )}\n >\n {message.permissions.canReact && (\n <ActionButton\n icon={<SmilePlus size={14} />}\n label={tr(\"conversations.react\", \"React\")}\n pressed={showReactionPicker}\n onClick={() => {\n setShowReactionPicker((value) => !value);\n setShowMoreMenu(false);\n }}\n />\n )}\n {message.permissions.canReply && (\n <ActionButton\n icon={<Reply size={14} />}\n label={tr(\"conversations.reply\", \"Reply\")}\n onClick={() => onReply?.(message)}\n />\n )}\n {message.permissions.canEdit && (\n <ActionButton\n icon={<Pencil size={14} />}\n label={tr(\"conversations.edit\", \"Edit\")}\n onClick={() => onEdit?.(message)}\n />\n )}\n {message.permissions.canDelete && (\n <ActionButton\n icon={<Trash2 size={14} />}\n label={tr(\"conversations.delete\", \"Delete\")}\n onClick={() => onDelete?.(message.id)}\n />\n )}\n <ActionButton\n icon={<MoreHorizontal size={14} />}\n label={tr(\"conversations.more\", \"More\")}\n pressed={showMoreMenu}\n onClick={() => {\n setShowMoreMenu((value) => !value);\n setShowReactionPicker(false);\n }}\n />\n {showReactionPicker && (\n <div className=\"absolute top-full z-10 mt-1 flex items-center gap-1 rounded-lg border border-border bg-popover p-1 shadow-lg\">\n {[\"👍\", \"❤️\", \"🎉\", \"👀\", \"✅\"].map((emoji) => (\n <button\n key={emoji}\n type=\"button\"\n onClick={() => {\n onReact?.(message.id, emoji);\n setShowReactionPicker(false);\n }}\n className=\"rounded-md px-2 py-1 text-sm hover:bg-muted/80\"\n aria-label={`${tr(\"conversations.reactWith\", \"React with\")} ${emoji}`}\n >\n {emoji}\n </button>\n ))}\n </div>\n )}\n {showMoreMenu && message.permissions.canPin && onTogglePin && (\n <div className=\"absolute right-0 top-full z-10 mt-1 min-w-40 rounded-lg border border-border bg-popover p-1 shadow-lg\">\n <button\n type=\"button\"\n onClick={() => {\n onTogglePin(message.id, message.isPinned);\n setShowMoreMenu(false);\n }}\n className=\"flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-foreground hover:bg-muted/80\"\n >\n <Pin size={14} />\n {message.isPinned\n ? tr(\"conversations.unpinMessage\", \"Unpin message\")\n : tr(\"conversations.pinMessage\", \"Pin message\")}\n </button>\n </div>\n )}\n </div>\n </div>\n );\n});\n\n/**\n * Small icon button for message actions\n */\nfunction ActionButton({\n icon,\n label,\n onClick,\n pressed,\n}: {\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n pressed?: boolean;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n aria-pressed={pressed}\n title={label}\n className=\"flex min-h-9 min-w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted/80 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n {icon}\n </button>\n );\n}\n\n/**\n * Format file size in human-readable form\n */\nfunction formatFileSize(bytes: number): string {\n if (bytes === 0) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;\n}\n"],"mappings":"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"}
@@ -58,7 +58,7 @@ function g(t) {
58
58
  query: r,
59
59
  first: 25
60
60
  },
61
- skip: !n || !r
61
+ skip: !n || r.length < 2
62
62
  });
63
63
  return {
64
64
  conversations: i.data?.searchConversations.edges.map((e) => d(e.node)) ?? [],