@burdenoff/microfe-conversations 2026.525.2 → 2026.527.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conversations/ConversationsUserRoutes.js +4 -3
- package/dist/conversations/ConversationsUserRoutes.js.map +1 -1
- package/dist/conversations/components/EmptyState.js +27 -0
- package/dist/conversations/components/EmptyState.js.map +1 -0
- package/dist/conversations/components/LazyPageBoundary.js +26 -42
- package/dist/conversations/components/LazyPageBoundary.js.map +1 -1
- package/dist/conversations/components/MessageBubble.js +3 -1
- package/dist/conversations/components/MessageBubble.js.map +1 -1
- package/dist/conversations/components/PageLayout.js +3 -39
- package/dist/conversations/components/PageLayout.js.map +1 -1
- package/dist/conversations/components/PageSection.js +25 -0
- package/dist/conversations/components/PageSection.js.map +1 -0
- package/dist/conversations/components/PermissionGuard.js +1 -1
- package/dist/conversations/components/PermissionGuard.js.map +1 -1
- package/dist/conversations/components/lazyRetry.js +23 -0
- package/dist/conversations/components/lazyRetry.js.map +1 -0
- package/dist/conversations/constants/permissions.js +0 -1
- package/dist/conversations/constants/permissions.js.map +1 -1
- package/dist/conversations/hooks/useMessageQueries.js.map +1 -1
- package/dist/conversations/hooks/useMessageSubscriptions.js.map +1 -1
- package/dist/conversations/pages/AdminConversationsLandingPage.js +6 -6
- package/dist/conversations/pages/AdminConversationsLandingPage.js.map +1 -1
- package/dist/conversations/pages/ConversationAdminPages.js +56 -49
- package/dist/conversations/pages/ConversationAdminPages.js.map +1 -1
- package/dist/conversations/pages/ConversationDetailPage.js +3 -2
- package/dist/conversations/pages/ConversationDetailPage.js.map +1 -1
- package/dist/conversations/pages/ConversationListPage.js +133 -136
- package/dist/conversations/pages/ConversationListPage.js.map +1 -1
- package/dist/conversations/pages/ConversationsLandingPage.js +50 -57
- package/dist/conversations/pages/ConversationsLandingPage.js.map +1 -1
- package/dist/conversations/pages/ConversationsSettingsPage.js +8 -5
- package/dist/conversations/pages/ConversationsSettingsPage.js.map +1 -1
- package/dist/conversations/pages/NewConversationPage.js +87 -88
- package/dist/conversations/pages/NewConversationPage.js.map +1 -1
- package/dist/conversations/providers/ConversationsProvider.js +1 -1
- package/dist/conversations/providers/ConversationsProvider.js.map +1 -1
- package/dist/conversations/utils/mappers.js.map +1 -1
- package/package.json +1 -1
- package/dist/__tests__/setup.d.ts +0 -2
- package/dist/__tests__/setup.d.ts.map +0 -1
- package/dist/conversations/ConversationsAdminRoot.d.ts +0 -19
- package/dist/conversations/ConversationsAdminRoot.d.ts.map +0 -1
- package/dist/conversations/ConversationsAdminRoutes.d.ts +0 -18
- package/dist/conversations/ConversationsAdminRoutes.d.ts.map +0 -1
- package/dist/conversations/ConversationsRoot.d.ts +0 -33
- package/dist/conversations/ConversationsRoot.d.ts.map +0 -1
- package/dist/conversations/ConversationsUserRoot.d.ts +0 -17
- package/dist/conversations/ConversationsUserRoot.d.ts.map +0 -1
- package/dist/conversations/ConversationsUserRoutes.d.ts +0 -12
- package/dist/conversations/ConversationsUserRoutes.d.ts.map +0 -1
- package/dist/conversations/components/ConversationCard.d.ts +0 -14
- package/dist/conversations/components/ConversationCard.d.ts.map +0 -1
- package/dist/conversations/components/LazyPageBoundary.d.ts +0 -25
- package/dist/conversations/components/LazyPageBoundary.d.ts.map +0 -1
- package/dist/conversations/components/MessageBubble.d.ts +0 -21
- package/dist/conversations/components/MessageBubble.d.ts.map +0 -1
- package/dist/conversations/components/PageLayout.d.ts +0 -57
- package/dist/conversations/components/PageLayout.d.ts.map +0 -1
- package/dist/conversations/components/PermissionGuard.d.ts +0 -21
- package/dist/conversations/components/PermissionGuard.d.ts.map +0 -1
- package/dist/conversations/constants/featureFlags.d.ts +0 -8
- package/dist/conversations/constants/featureFlags.d.ts.map +0 -1
- package/dist/conversations/constants/permissions.d.ts +0 -174
- package/dist/conversations/constants/permissions.d.ts.map +0 -1
- package/dist/conversations/hooks/useAdminFeatureFlags.d.ts +0 -8
- package/dist/conversations/hooks/useAdminFeatureFlags.d.ts.map +0 -1
- package/dist/conversations/hooks/useConversationQueries.d.ts +0 -83
- package/dist/conversations/hooks/useConversationQueries.d.ts.map +0 -1
- package/dist/conversations/hooks/useExportConversation.d.ts +0 -10
- package/dist/conversations/hooks/useExportConversation.d.ts.map +0 -1
- package/dist/conversations/hooks/useMessageQueries.d.ts +0 -85
- package/dist/conversations/hooks/useMessageQueries.d.ts.map +0 -1
- package/dist/conversations/hooks/useMessageSubscriptions.d.ts +0 -24
- package/dist/conversations/hooks/useMessageSubscriptions.d.ts.map +0 -1
- package/dist/conversations/hooks/useUserFeatureFlags.d.ts +0 -5
- package/dist/conversations/hooks/useUserFeatureFlags.d.ts.map +0 -1
- package/dist/conversations/index.d.ts +0 -31
- package/dist/conversations/index.d.ts.map +0 -1
- package/dist/conversations/pages/AdminConversationsLandingPage.d.ts +0 -9
- package/dist/conversations/pages/AdminConversationsLandingPage.d.ts.map +0 -1
- package/dist/conversations/pages/ConversationAdminPages.d.ts +0 -5
- package/dist/conversations/pages/ConversationAdminPages.d.ts.map +0 -1
- package/dist/conversations/pages/ConversationDetailPage.d.ts +0 -2
- package/dist/conversations/pages/ConversationDetailPage.d.ts.map +0 -1
- package/dist/conversations/pages/ConversationListPage.d.ts +0 -5
- package/dist/conversations/pages/ConversationListPage.d.ts.map +0 -1
- package/dist/conversations/pages/ConversationsLandingPage.d.ts +0 -9
- package/dist/conversations/pages/ConversationsLandingPage.d.ts.map +0 -1
- package/dist/conversations/pages/ConversationsSettingsPage.d.ts +0 -2
- package/dist/conversations/pages/ConversationsSettingsPage.d.ts.map +0 -1
- package/dist/conversations/pages/NewConversationPage.d.ts +0 -2
- package/dist/conversations/pages/NewConversationPage.d.ts.map +0 -1
- package/dist/conversations/providers/ConversationsProvider.d.ts +0 -13
- package/dist/conversations/providers/ConversationsProvider.d.ts.map +0 -1
- package/dist/conversations/store/conversationsStore.d.ts +0 -29
- package/dist/conversations/store/conversationsStore.d.ts.map +0 -1
- package/dist/conversations/types/index.d.ts +0 -352
- package/dist/conversations/types/index.d.ts.map +0 -1
- package/dist/conversations/utils/cn.d.ts +0 -8
- package/dist/conversations/utils/cn.d.ts.map +0 -1
- package/dist/conversations/utils/i18n.d.ts +0 -15
- package/dist/conversations/utils/i18n.d.ts.map +0 -1
- package/dist/conversations/utils/mappers.d.ts +0 -32
- package/dist/conversations/utils/mappers.d.ts.map +0 -1
- package/dist/conversations/utils/navigation.d.ts +0 -18
- package/dist/conversations/utils/navigation.d.ts.map +0 -1
- package/dist/dev/main.d.ts +0 -2
- package/dist/dev/main.d.ts.map +0 -1
- package/dist/generated/global-operations.d.ts +0 -2
- package/dist/generated/global-operations.d.ts.map +0 -1
- package/dist/generated/global-types.d.ts +0 -23527
- package/dist/generated/global-types.d.ts.map +0 -1
- package/dist/generated/wspace-operations.d.ts +0 -2008
- package/dist/generated/wspace-operations.d.ts.map +0 -1
- package/dist/generated/wspace-types.d.ts +0 -57851
- package/dist/generated/wspace-types.d.ts.map +0 -1
- package/dist/index.d.ts +0 -6
- package/dist/index.d.ts.map +0 -1
- package/dist/utils/nativeBridge.d.ts +0 -3
- package/dist/utils/nativeBridge.d.ts.map +0 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { lazyRetry as e } from "./components/lazyRetry.js";
|
|
2
|
+
import { LazyPageBoundary as t } from "./components/LazyPageBoundary.js";
|
|
2
3
|
import { Navigate as n, Route as r, Routes as i } from "react-router-dom";
|
|
3
4
|
import { jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
4
5
|
//#region src/conversations/ConversationsUserRoutes.tsx
|
|
5
|
-
var s =
|
|
6
|
+
var s = e(() => import("./pages/ConversationListPage.js").then((e) => ({ default: e.ConversationListPage }))), c = e(() => import("./pages/ConversationsLandingPage.js").then((e) => ({ default: e.ConversationsLandingPage }))), l = e(() => import("./pages/ConversationDetailPage.js").then((e) => ({ default: e.ConversationDetailPage }))), u = e(() => import("./pages/NewConversationPage.js").then((e) => ({ default: e.NewConversationPage }))), d = e(() => import("./pages/ConversationsSettingsPage.js").then((e) => ({ default: e.ConversationsSettingsPage })));
|
|
6
7
|
function f() {
|
|
7
|
-
return /* @__PURE__ */ a(
|
|
8
|
+
return /* @__PURE__ */ a(t, {
|
|
8
9
|
source: "user",
|
|
9
10
|
children: /* @__PURE__ */ o(i, { children: [
|
|
10
11
|
/* @__PURE__ */ a(r, {
|
|
@@ -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
|
|
1
|
+
{"version":3,"file":"ConversationsUserRoutes.js","names":[],"sources":["../../src/conversations/ConversationsUserRoutes.tsx"],"sourcesContent":["import { Navigate, Route, Routes } from \"react-router-dom\";\n\nimport { LazyPageBoundary } from \"./components/LazyPageBoundary\";\nimport { lazyRetry } from \"./components/lazyRetry\";\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":";;;;;AAMA,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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { jsx as t, jsxs as n } from "react/jsx-runtime";
|
|
3
|
+
//#region src/conversations/components/EmptyState.tsx
|
|
4
|
+
function r({ icon: r, title: i, description: a, action: o, className: s }) {
|
|
5
|
+
return /* @__PURE__ */ n("div", {
|
|
6
|
+
className: e("flex flex-col items-center justify-center", "py-12 sm:py-16 px-4", "text-center", "animate-in fade-in-0 zoom-in-95 duration-300", s),
|
|
7
|
+
children: [
|
|
8
|
+
r && /* @__PURE__ */ t("div", {
|
|
9
|
+
className: "mb-4 p-4 rounded-2xl bg-muted/50 text-muted-foreground",
|
|
10
|
+
children: r
|
|
11
|
+
}),
|
|
12
|
+
/* @__PURE__ */ t("h3", {
|
|
13
|
+
className: "text-lg font-semibold text-foreground mb-1",
|
|
14
|
+
children: i
|
|
15
|
+
}),
|
|
16
|
+
a && /* @__PURE__ */ t("p", {
|
|
17
|
+
className: "text-sm text-muted-foreground max-w-sm mb-4",
|
|
18
|
+
children: a
|
|
19
|
+
}),
|
|
20
|
+
o
|
|
21
|
+
]
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { r as EmptyState };
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=EmptyState.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EmptyState.js","names":[],"sources":["../../../src/conversations/components/EmptyState.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../utils/cn\";\n\n/**\n * EmptyState - A consistent empty state component\n */\nexport function EmptyState({\n icon,\n title,\n description,\n action,\n className,\n}: {\n icon?: ReactNode;\n title: string;\n description?: string;\n action?: ReactNode;\n className?: string;\n}) {\n return (\n <div\n className={cn(\n \"flex flex-col items-center justify-center\",\n \"py-12 sm:py-16 px-4\",\n \"text-center\",\n \"animate-in fade-in-0 zoom-in-95 duration-300\",\n className,\n )}\n >\n {icon && (\n <div className=\"mb-4 p-4 rounded-2xl bg-muted/50 text-muted-foreground\">\n {icon}\n </div>\n )}\n <h3 className=\"text-lg font-semibold text-foreground mb-1\">{title}</h3>\n {description && (\n <p className=\"text-sm text-muted-foreground max-w-sm mb-4\">\n {description}\n </p>\n )}\n {action}\n </div>\n );\n}\n"],"mappings":";;;AAMA,SAAgB,EAAW,EACzB,SACA,UACA,gBACA,WACA,gBAOC;AACD,QACE,kBAAC,OAAD;EACE,WAAW,EACT,6CACA,uBACA,eACA,gDACA,EACD;YAPH;GASG,KACC,kBAAC,OAAD;IAAK,WAAU;cACZ;IACG,CAAA;GAER,kBAAC,MAAD;IAAI,WAAU;cAA8C;IAAW,CAAA;GACtE,KACC,kBAAC,KAAD;IAAG,WAAU;cACV;IACC,CAAA;GAEL;GACG"}
|
|
@@ -1,39 +1,23 @@
|
|
|
1
1
|
import { useTr as e } from "../utils/i18n.js";
|
|
2
|
-
import {
|
|
2
|
+
import { isChunkLoadError as t } from "./lazyRetry.js";
|
|
3
|
+
import { Component as n, Suspense as r } from "react";
|
|
3
4
|
import { AlertTriangle as i, Loader2 as a, RefreshCw as o } from "lucide-react";
|
|
4
5
|
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
5
6
|
//#region src/conversations/components/LazyPageBoundary.tsx
|
|
6
|
-
function l(
|
|
7
|
-
if (!e) return !1;
|
|
8
|
-
if (e.name === "ChunkLoadError") return !0;
|
|
9
|
-
let t = e.message ?? "";
|
|
10
|
-
return t.includes("Loading chunk") || t.includes("Failed to fetch dynamically imported module") || t.includes("Importing a module script failed") || t.includes("Failed to fetch");
|
|
11
|
-
}
|
|
12
|
-
var u = 300;
|
|
13
|
-
function d(e) {
|
|
14
|
-
return r(() => e().catch((t) => {
|
|
15
|
-
if (t instanceof Error && l(t)) return new Promise((t, n) => {
|
|
16
|
-
setTimeout(() => {
|
|
17
|
-
e().then(t, n);
|
|
18
|
-
}, u);
|
|
19
|
-
});
|
|
20
|
-
throw t;
|
|
21
|
-
}));
|
|
22
|
-
}
|
|
23
|
-
function f() {
|
|
7
|
+
function l() {
|
|
24
8
|
return /* @__PURE__ */ s("div", {
|
|
25
9
|
className: "flex items-center justify-center min-h-[400px]",
|
|
26
|
-
children: /* @__PURE__ */ s(a, { className: "
|
|
10
|
+
children: /* @__PURE__ */ s(a, { className: "size-8 animate-spin text-primary" })
|
|
27
11
|
});
|
|
28
12
|
}
|
|
29
|
-
function
|
|
13
|
+
function u({ errorMessage: t, onRetry: n }) {
|
|
30
14
|
let r = e();
|
|
31
15
|
return /* @__PURE__ */ c("div", {
|
|
32
16
|
className: "flex flex-col items-center justify-center min-h-[400px] gap-4 text-center p-6",
|
|
33
17
|
children: [
|
|
34
18
|
/* @__PURE__ */ s("div", {
|
|
35
19
|
className: "p-3 rounded-full bg-destructive/10",
|
|
36
|
-
children: /* @__PURE__ */ s(i, { className: "
|
|
20
|
+
children: /* @__PURE__ */ s(i, { className: "size-8 text-destructive" })
|
|
37
21
|
}),
|
|
38
22
|
/* @__PURE__ */ c("div", {
|
|
39
23
|
className: "space-y-2",
|
|
@@ -49,7 +33,7 @@ function p({ errorMessage: t, onRetry: n }) {
|
|
|
49
33
|
type: "button",
|
|
50
34
|
onClick: n,
|
|
51
35
|
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",
|
|
52
|
-
children: [/* @__PURE__ */ s(o, { className: "
|
|
36
|
+
children: [/* @__PURE__ */ s(o, { className: "size-4" }), r("conversations.tryAgain", "Try Again")]
|
|
53
37
|
}),
|
|
54
38
|
t && /* @__PURE__ */ c("details", {
|
|
55
39
|
className: "mt-2 max-w-md w-full text-left",
|
|
@@ -64,30 +48,30 @@ function p({ errorMessage: t, onRetry: n }) {
|
|
|
64
48
|
]
|
|
65
49
|
});
|
|
66
50
|
}
|
|
67
|
-
var
|
|
68
|
-
function
|
|
69
|
-
return `${
|
|
51
|
+
var d = "conversations-lazyreload-";
|
|
52
|
+
function f(e) {
|
|
53
|
+
return `${d}${e ?? "default"}`;
|
|
70
54
|
}
|
|
71
|
-
function
|
|
55
|
+
function p(e) {
|
|
72
56
|
try {
|
|
73
57
|
return typeof sessionStorage > "u" ? !1 : sessionStorage.getItem(e) !== null;
|
|
74
58
|
} catch {
|
|
75
59
|
return !1;
|
|
76
60
|
}
|
|
77
61
|
}
|
|
78
|
-
function
|
|
62
|
+
function m(e) {
|
|
79
63
|
try {
|
|
80
64
|
if (typeof sessionStorage > "u") return;
|
|
81
65
|
sessionStorage.setItem(e, "1");
|
|
82
66
|
} catch {}
|
|
83
67
|
}
|
|
84
|
-
function
|
|
68
|
+
function h(e) {
|
|
85
69
|
try {
|
|
86
70
|
if (typeof sessionStorage > "u") return;
|
|
87
71
|
sessionStorage.removeItem(e);
|
|
88
72
|
} catch {}
|
|
89
73
|
}
|
|
90
|
-
var
|
|
74
|
+
var g = class extends n {
|
|
91
75
|
state = {
|
|
92
76
|
hasError: !1,
|
|
93
77
|
error: null,
|
|
@@ -100,20 +84,20 @@ var y = class extends t {
|
|
|
100
84
|
};
|
|
101
85
|
}
|
|
102
86
|
componentDidMount() {
|
|
103
|
-
this.state.hasError ||
|
|
87
|
+
this.state.hasError || h(f(this.props.source));
|
|
104
88
|
}
|
|
105
89
|
componentDidUpdate(e, t) {
|
|
106
|
-
t.hasError && !this.state.hasError &&
|
|
90
|
+
t.hasError && !this.state.hasError && h(f(this.props.source));
|
|
107
91
|
}
|
|
108
92
|
componentDidCatch(e, t) {
|
|
109
93
|
let n = this.props.source ? `[Conversations:${this.props.source}]` : "[Conversations]";
|
|
110
94
|
console.error(`${n} Lazy page load failed:`, e, t);
|
|
111
95
|
}
|
|
112
96
|
handleRetry = () => {
|
|
113
|
-
if (
|
|
114
|
-
let e =
|
|
115
|
-
if (
|
|
116
|
-
|
|
97
|
+
if (t(this.state.error)) {
|
|
98
|
+
let e = f(this.props.source);
|
|
99
|
+
if (p(e)) return;
|
|
100
|
+
m(e), typeof window < "u" && window.location.reload();
|
|
117
101
|
return;
|
|
118
102
|
}
|
|
119
103
|
this.setState((e) => ({
|
|
@@ -123,22 +107,22 @@ var y = class extends t {
|
|
|
123
107
|
}));
|
|
124
108
|
};
|
|
125
109
|
render() {
|
|
126
|
-
return this.state.hasError ? /* @__PURE__ */ s(
|
|
110
|
+
return this.state.hasError ? /* @__PURE__ */ s(u, {
|
|
127
111
|
errorMessage: this.state.error?.message ?? null,
|
|
128
112
|
onRetry: this.handleRetry
|
|
129
113
|
}) : this.props.children(this.state.retryKey);
|
|
130
114
|
}
|
|
131
115
|
};
|
|
132
|
-
function
|
|
133
|
-
return /* @__PURE__ */ s(
|
|
116
|
+
function _({ children: e, source: t }) {
|
|
117
|
+
return /* @__PURE__ */ s(g, {
|
|
134
118
|
source: t,
|
|
135
|
-
children: (t) => /* @__PURE__ */ s(
|
|
136
|
-
fallback: /* @__PURE__ */ s(
|
|
119
|
+
children: (t) => /* @__PURE__ */ s(r, {
|
|
120
|
+
fallback: /* @__PURE__ */ s(l, {}),
|
|
137
121
|
children: e
|
|
138
122
|
}, t)
|
|
139
123
|
});
|
|
140
124
|
}
|
|
141
125
|
//#endregion
|
|
142
|
-
export {
|
|
126
|
+
export { _ as LazyPageBoundary };
|
|
143
127
|
|
|
144
128
|
//# sourceMappingURL=LazyPageBoundary.js.map
|
|
@@ -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<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
|
+
{"version":3,"file":"LazyPageBoundary.js","names":[],"sources":["../../../src/conversations/components/LazyPageBoundary.tsx"],"sourcesContent":["import {\n Component,\n Suspense,\n type ErrorInfo,\n type ReactNode,\n} from \"react\";\nimport { Loader2, AlertTriangle, RefreshCw } from \"lucide-react\";\nimport { useTr } from \"../utils/i18n\";\nimport { isChunkLoadError } from \"./lazyRetry\";\n\n/**\n * Fallback spinner rendered while a lazy-loaded page chunk is resolving.\n */\nfunction PageLoader() {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <Loader2 className=\"size-8 animate-spin text-primary\" />\n </div>\n );\n}\n\nfunction LazyErrorUI({\n errorMessage,\n onRetry,\n}: {\n errorMessage: string | null;\n onRetry: () => void;\n}) {\n const tr = useTr();\n\n return (\n <div className=\"flex flex-col items-center justify-center min-h-[400px] gap-4 text-center p-6\">\n <div className=\"p-3 rounded-full bg-destructive/10\">\n <AlertTriangle className=\"size-8 text-destructive\" />\n </div>\n <div className=\"space-y-2\">\n <h3 className=\"text-lg font-semibold text-foreground\">\n {tr(\"conversations.failedToLoadPage\", \"Failed to load page\")}\n </h3>\n <p className=\"text-sm text-muted-foreground max-w-md\">\n {tr(\n \"conversations.pageChunkLoadFailed\",\n \"The page chunk could not be loaded. This may be due to a network issue.\",\n )}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors\"\n >\n <RefreshCw className=\"size-4\" />\n {tr(\"conversations.tryAgain\", \"Try Again\")}\n </button>\n {errorMessage && (\n <details className=\"mt-2 max-w-md w-full text-left\">\n <summary className=\"cursor-pointer text-xs text-muted-foreground hover:text-foreground select-none\">\n {tr(\"conversations.errorDetails\", \"Error details\")}\n </summary>\n <pre className=\"mt-2 p-2 rounded bg-muted text-[11px] text-muted-foreground whitespace-pre-wrap break-all\">\n {errorMessage}\n </pre>\n </details>\n )}\n </div>\n );\n}\n\ntype LazyErrorBoundaryProps = {\n source?: string;\n children: (retryKey: number) => ReactNode;\n};\n\ntype LazyErrorBoundaryState = {\n hasError: boolean;\n error: Error | null;\n retryKey: number;\n};\n\n/**\n * Session-scoped flag used to prevent a reload loop when a chunk-load\n * failure is persistent (broken deploy, CDN outage, missing hash file).\n * First chunk-load retry triggers a page reload and sets the flag; if\n * the page mounts again with the same error, `handleRetry` sees the\n * flag and skips the second reload so the user stays on the error UI\n * and can take a different action instead of looping indefinitely.\n */\nconst RELOAD_FLAG_PREFIX = \"conversations-lazyreload-\";\n\nfunction reloadFlagKey(source?: string): string {\n return `${RELOAD_FLAG_PREFIX}${source ?? \"default\"}`;\n}\n\nfunction readSessionFlag(key: string): boolean {\n try {\n if (typeof sessionStorage === \"undefined\") return false;\n return sessionStorage.getItem(key) !== null;\n } catch {\n return false;\n }\n}\n\nfunction setSessionFlag(key: string): void {\n try {\n if (typeof sessionStorage === \"undefined\") return;\n sessionStorage.setItem(key, \"1\");\n } catch {\n // sessionStorage access can throw in some privacy / sandboxed modes\n }\n}\n\nfunction clearSessionFlag(key: string): void {\n try {\n if (typeof sessionStorage === \"undefined\") return;\n sessionStorage.removeItem(key);\n } catch {\n // ignore\n }\n}\n\n/**\n * Error boundary for lazy-loaded page chunks.\n *\n * On retry it branches by error type:\n * - **Chunk-load failures** (`ChunkLoadError`, \"Loading chunk X failed\",\n * \"Failed to fetch dynamically imported module\", etc.) — the rejected\n * promise is cached on the `LazyExoticComponent` itself; the only\n * reliable recovery is a full page reload, which re-creates all lazy\n * refs from scratch.\n * - **Other render errors** — clear the error state and bump an internal\n * `retryKey`; the enclosing `LazyPageBoundary` keys its `<Suspense>`\n * on that counter so the subtree unmounts and remounts. This resets\n * component state and helps recover from transient render issues.\n *\n * Accepts an optional `source` label used in the console tag so user-\n * route and admin-route chunks can be distinguished in telemetry.\n */\nclass LazyErrorBoundary extends Component<\n LazyErrorBoundaryProps,\n LazyErrorBoundaryState\n> {\n state: LazyErrorBoundaryState = {\n hasError: false,\n error: null,\n retryKey: 0,\n };\n\n static getDerivedStateFromError(error: Error) {\n return { hasError: true, error };\n }\n\n componentDidMount() {\n // Subtree mounted without catching an error — the previous session\n // state (if any) is stale. Clear the reload-guard flag so a future\n // chunk failure can still trigger one reload before giving up.\n if (!this.state.hasError) {\n clearSessionFlag(reloadFlagKey(this.props.source));\n }\n }\n\n componentDidUpdate(\n _prevProps: LazyErrorBoundaryProps,\n prevState: LazyErrorBoundaryState,\n ) {\n // Error → recovered (state reset / retryKey bump → clean render)\n // also invalidates the reload-guard flag.\n if (prevState.hasError && !this.state.hasError) {\n clearSessionFlag(reloadFlagKey(this.props.source));\n }\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n const tag = this.props.source\n ? `[Conversations:${this.props.source}]`\n : \"[Conversations]\";\n // Log the raw error details (including the message) to the console\n // only — user-facing copy is always a localised fallback so that\n // bundler-internal strings like \"ChunkLoadError: Loading chunk 42 failed\"\n // never surface to end users.\n console.error(`${tag} Lazy page load failed:`, error, errorInfo);\n }\n\n handleRetry = () => {\n if (isChunkLoadError(this.state.error)) {\n // React.lazy caches rejected promises on the LazyExoticComponent\n // itself — keying Suspense or resetting boundary state does NOT\n // clear it. A full reload is the only reliable recovery.\n //\n // Guard against a reload loop: if the flag is already set we\n // reloaded on a prior iteration and the chunk still failed.\n // Leave the flag set and do nothing — the error UI is already\n // visible and `componentDidMount` / `componentDidUpdate` are\n // the only places that should clear the flag (they fire on a\n // successful render, signalling that the chunk is finally\n // healthy). Clearing here would let a subsequent click trigger\n // another reload and loop indefinitely.\n const flagKey = reloadFlagKey(this.props.source);\n if (readSessionFlag(flagKey)) {\n return;\n }\n setSessionFlag(flagKey);\n if (typeof window !== \"undefined\") {\n window.location.reload();\n }\n return;\n }\n this.setState((s) => ({\n hasError: false,\n error: null,\n retryKey: s.retryKey + 1,\n }));\n };\n\n render() {\n if (this.state.hasError) {\n // The primary copy is always the localised fallback so bundler-\n // internal strings (\"ChunkLoadError: Loading chunk 42 failed\",\n // \"Failed to fetch dynamically imported module\", etc.) never lead\n // the UI. The raw message is surfaced behind a collapsible\n // \"Error details\" affordance so support / dev users can still\n // diagnose persistent failures without reloading the tab.\n return (\n <LazyErrorUI\n errorMessage={this.state.error?.message ?? null}\n onRetry={this.handleRetry}\n />\n );\n }\n return this.props.children(this.state.retryKey);\n }\n}\n\n/**\n * Wraps a subtree with `<LazyErrorBoundary>` + `<Suspense>` so that any\n * `React.lazy()` / `lazyRetry()` page chunk below it gets a consistent\n * loading spinner and retry UI.\n *\n * The Suspense is keyed by the boundary's internal `retryKey` counter\n * so non-chunk render errors can be recovered by unmounting + remounting\n * the subtree. Chunk-load errors bypass this path and trigger a full\n * page reload (see `LazyErrorBoundary.handleRetry` — React.lazy's\n * promise cache cannot be cleared without creating a fresh lazy ref).\n */\nexport function LazyPageBoundary({\n children,\n source,\n}: {\n children: ReactNode;\n source?: string;\n}) {\n return (\n <LazyErrorBoundary source={source}>\n {(retryKey) => (\n <Suspense key={retryKey} fallback={<PageLoader />}>\n {children}\n </Suspense>\n )}\n </LazyErrorBoundary>\n );\n}\n"],"mappings":"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"}
|
|
@@ -12,7 +12,7 @@ function _(e) {
|
|
|
12
12
|
}
|
|
13
13
|
function v({ status: e, failedLabel: t }) {
|
|
14
14
|
switch (e) {
|
|
15
|
-
case "SENDING": return /* @__PURE__ */ m("span", { className: "
|
|
15
|
+
case "SENDING": return /* @__PURE__ */ m("span", { className: "size-3 rounded-full border border-muted-foreground animate-pulse" });
|
|
16
16
|
case "SENT": return /* @__PURE__ */ m(r, {
|
|
17
17
|
size: 12,
|
|
18
18
|
className: "text-muted-foreground"
|
|
@@ -149,6 +149,8 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
|
|
|
149
149
|
}),
|
|
150
150
|
/* @__PURE__ */ h("div", {
|
|
151
151
|
"data-tour": "conversation-message-actions",
|
|
152
|
+
role: "toolbar",
|
|
153
|
+
"aria-label": F("conversations.messageActions", "Message actions"),
|
|
152
154
|
tabIndex: 0,
|
|
153
155
|
onFocus: () => N(!0),
|
|
154
156
|
onBlur: (e) => {
|
|
@@ -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-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 [showActions, setShowActions] = 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 const actionsVisible = showActions || showReactionPicker || showMoreMenu;\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 onMouseEnter={() => setShowActions(true)}\n onMouseLeave={() => setShowActions(false)}\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 data-tour=\"conversation-message-actions\"\n tabIndex={0}\n onFocus={() => setShowActions(true)}\n onBlur={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget)) {\n setShowActions(false);\n }\n }}\n className={cn(\n \"relative mt-1 flex items-center gap-0.5 self-start rounded-md opacity-100 transition-opacity duration-150 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100\",\n actionsVisible && \"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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"}
|
|
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=\"size-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 [showActions, setShowActions] = 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 const actionsVisible = showActions || showReactionPicker || showMoreMenu;\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 onMouseEnter={() => setShowActions(true)}\n onMouseLeave={() => setShowActions(false)}\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 data-tour=\"conversation-message-actions\"\n role=\"toolbar\"\n aria-label={tr(\"conversations.messageActions\", \"Message actions\")}\n tabIndex={0}\n onFocus={() => setShowActions(true)}\n onBlur={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget)) {\n setShowActions(false);\n }\n }}\n className={cn(\n \"relative mt-1 flex items-center gap-0.5 self-start rounded-md opacity-100 transition-opacity duration-150 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100\",\n actionsVisible && \"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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"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import "./PageSection.js";
|
|
3
|
+
import "./EmptyState.js";
|
|
2
4
|
import { jsx as t, jsxs as n } from "react/jsx-runtime";
|
|
3
5
|
//#region src/conversations/components/PageLayout.tsx
|
|
4
6
|
var r = {
|
|
@@ -50,45 +52,7 @@ function i({ children: i, title: a, subtitle: o, icon: s, headerActions: c, head
|
|
|
50
52
|
})
|
|
51
53
|
});
|
|
52
54
|
}
|
|
53
|
-
function a({ children: r, title: i, description: a, actions: o, className: s }) {
|
|
54
|
-
return /* @__PURE__ */ n("section", {
|
|
55
|
-
className: e("space-y-4", s),
|
|
56
|
-
children: [(i || o) && /* @__PURE__ */ n("div", {
|
|
57
|
-
className: "flex items-center justify-between",
|
|
58
|
-
children: [/* @__PURE__ */ n("div", { children: [i && /* @__PURE__ */ t("h2", {
|
|
59
|
-
className: "text-lg font-semibold text-foreground",
|
|
60
|
-
children: i
|
|
61
|
-
}), a && /* @__PURE__ */ t("p", {
|
|
62
|
-
className: "text-sm text-muted-foreground",
|
|
63
|
-
children: a
|
|
64
|
-
})] }), o && /* @__PURE__ */ t("div", {
|
|
65
|
-
className: "flex items-center gap-2",
|
|
66
|
-
children: o
|
|
67
|
-
})]
|
|
68
|
-
}), r]
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
function o({ icon: r, title: i, description: a, action: o, className: s }) {
|
|
72
|
-
return /* @__PURE__ */ n("div", {
|
|
73
|
-
className: e("flex flex-col items-center justify-center", "py-12 sm:py-16 px-4", "text-center", "animate-in fade-in-0 zoom-in-95 duration-300", s),
|
|
74
|
-
children: [
|
|
75
|
-
r && /* @__PURE__ */ t("div", {
|
|
76
|
-
className: "mb-4 p-4 rounded-2xl bg-muted/50 text-muted-foreground",
|
|
77
|
-
children: r
|
|
78
|
-
}),
|
|
79
|
-
/* @__PURE__ */ t("h3", {
|
|
80
|
-
className: "text-lg font-semibold text-foreground mb-1",
|
|
81
|
-
children: i
|
|
82
|
-
}),
|
|
83
|
-
a && /* @__PURE__ */ t("p", {
|
|
84
|
-
className: "text-sm text-muted-foreground max-w-sm mb-4",
|
|
85
|
-
children: a
|
|
86
|
-
}),
|
|
87
|
-
o
|
|
88
|
-
]
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
55
|
//#endregion
|
|
92
|
-
export {
|
|
56
|
+
export { i as PageLayout };
|
|
93
57
|
|
|
94
58
|
//# sourceMappingURL=PageLayout.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/conversations/components/PageLayout.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../utils/cn\";\n\ninterface PageLayoutProps {\n children: ReactNode;\n /** Page title */\n title?: string;\n /** Page subtitle/description */\n subtitle?: string;\n /** Icon component to display next to title */\n icon?: ReactNode;\n /** Actions to display in header (buttons, etc.) */\n headerActions?: ReactNode;\n /** Additional header content below title */\n headerContent?: ReactNode;\n /** Maximum width of content: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full' */\n maxWidth?: \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\" | \"full\";\n /** Whether to show the header section */\n showHeader?: boolean;\n /** Additional CSS classes for the container */\n className?: string;\n /** Additional CSS classes for the content area */\n contentClassName?: string;\n /** Stable selector for guided tours and smoke tests */\n dataTour?: string;\n}\n\nconst maxWidthClasses = {\n sm: \"max-w-screen-sm\", // 640px\n md: \"max-w-screen-md\", // 768px\n lg: \"max-w-screen-lg\", // 1024px\n xl: \"max-w-screen-xl\", // 1280px\n \"2xl\": \"max-w-screen-2xl\", // 1536px\n \"3xl\": \"max-w-[1600px]\",\n full: \"max-w-full\",\n};\n\n/**\n * PageLayout - A consistent page wrapper with responsive design\n *\n * Features:\n * - Proper max-width constraints\n * - Responsive padding\n * - Animated entrance\n * - Consistent header styling\n */\nexport function PageLayout({\n children,\n title,\n subtitle,\n icon,\n headerActions,\n headerContent,\n maxWidth = \"xl\",\n showHeader = true,\n className,\n contentClassName,\n dataTour,\n}: PageLayoutProps) {\n return (\n <div\n data-tour={dataTour}\n className={cn(\n // Base styles - full width container with padding\n \"min-h-full w-full\",\n // Responsive horizontal padding\n \"px-4 sm:px-6 lg:px-8 xl:px-12\",\n // Responsive vertical padding\n \"py-6 sm:py-8 lg:py-10\",\n // Entrance animation\n \"animate-in fade-in-0 slide-in-from-bottom-4 duration-500\",\n className,\n )}\n >\n {/* Max-width constrained content container */}\n <div\n className={cn(\n \"mx-auto w-full\",\n maxWidthClasses[maxWidth],\n contentClassName,\n )}\n >\n {/* Page Header */}\n {showHeader && (title || headerActions) && (\n <header className=\"mb-6 sm:mb-8\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n {/* Title Section */}\n <div className=\"space-y-1\">\n {(title || icon) && (\n <div className=\"flex items-center gap-3\">\n {icon && (\n <div className=\"flex-shrink-0 p-2.5 rounded-xl bg-primary/10 text-primary\">\n {icon}\n </div>\n )}\n {title && (\n <h1 className=\"text-2xl sm:text-3xl font-bold tracking-tight text-foreground\">\n {title}\n </h1>\n )}\n </div>\n )}\n {subtitle && (\n <p className=\"text-sm sm:text-base text-muted-foreground max-w-2xl\">\n {subtitle}\n </p>\n )}\n </div>\n\n {/* Header Actions */}\n {headerActions && (\n <div className=\"flex items-center gap-2 flex-shrink-0\">\n {headerActions}\n </div>\n )}\n </div>\n\n {/* Additional Header Content */}\n {headerContent && <div className=\"mt-4\">{headerContent}</div>}\n </header>\n )}\n\n {/* Page content — must not use a <main> landmark here because the\n host shell (AppShellLayout) already renders one; nesting <main>\n elements is invalid HTML and an a11y violation. */}\n <div className=\"space-y-6\">{children}</div>\n </div>\n </div>\n );\n}\n\
|
|
1
|
+
{"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/conversations/components/PageLayout.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../utils/cn\";\n\ninterface PageLayoutProps {\n children: ReactNode;\n /** Page title */\n title?: string;\n /** Page subtitle/description */\n subtitle?: string;\n /** Icon component to display next to title */\n icon?: ReactNode;\n /** Actions to display in header (buttons, etc.) */\n headerActions?: ReactNode;\n /** Additional header content below title */\n headerContent?: ReactNode;\n /** Maximum width of content: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full' */\n maxWidth?: \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\" | \"full\";\n /** Whether to show the header section */\n showHeader?: boolean;\n /** Additional CSS classes for the container */\n className?: string;\n /** Additional CSS classes for the content area */\n contentClassName?: string;\n /** Stable selector for guided tours and smoke tests */\n dataTour?: string;\n}\n\nconst maxWidthClasses = {\n sm: \"max-w-screen-sm\", // 640px\n md: \"max-w-screen-md\", // 768px\n lg: \"max-w-screen-lg\", // 1024px\n xl: \"max-w-screen-xl\", // 1280px\n \"2xl\": \"max-w-screen-2xl\", // 1536px\n \"3xl\": \"max-w-[1600px]\",\n full: \"max-w-full\",\n};\n\n/**\n * PageLayout - A consistent page wrapper with responsive design\n *\n * Features:\n * - Proper max-width constraints\n * - Responsive padding\n * - Animated entrance\n * - Consistent header styling\n */\nexport function PageLayout({\n children,\n title,\n subtitle,\n icon,\n headerActions,\n headerContent,\n maxWidth = \"xl\",\n showHeader = true,\n className,\n contentClassName,\n dataTour,\n}: PageLayoutProps) {\n return (\n <div\n data-tour={dataTour}\n className={cn(\n // Base styles - full width container with padding\n \"min-h-full w-full\",\n // Responsive horizontal padding\n \"px-4 sm:px-6 lg:px-8 xl:px-12\",\n // Responsive vertical padding\n \"py-6 sm:py-8 lg:py-10\",\n // Entrance animation\n \"animate-in fade-in-0 slide-in-from-bottom-4 duration-500\",\n className,\n )}\n >\n {/* Max-width constrained content container */}\n <div\n className={cn(\n \"mx-auto w-full\",\n maxWidthClasses[maxWidth],\n contentClassName,\n )}\n >\n {/* Page Header */}\n {showHeader && (title || headerActions) && (\n <header className=\"mb-6 sm:mb-8\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n {/* Title Section */}\n <div className=\"space-y-1\">\n {(title || icon) && (\n <div className=\"flex items-center gap-3\">\n {icon && (\n <div className=\"flex-shrink-0 p-2.5 rounded-xl bg-primary/10 text-primary\">\n {icon}\n </div>\n )}\n {title && (\n <h1 className=\"text-2xl sm:text-3xl font-bold tracking-tight text-foreground\">\n {title}\n </h1>\n )}\n </div>\n )}\n {subtitle && (\n <p className=\"text-sm sm:text-base text-muted-foreground max-w-2xl\">\n {subtitle}\n </p>\n )}\n </div>\n\n {/* Header Actions */}\n {headerActions && (\n <div className=\"flex items-center gap-2 flex-shrink-0\">\n {headerActions}\n </div>\n )}\n </div>\n\n {/* Additional Header Content */}\n {headerContent && <div className=\"mt-4\">{headerContent}</div>}\n </header>\n )}\n\n {/* Page content — must not use a <main> landmark here because the\n host shell (AppShellLayout) already renders one; nesting <main>\n elements is invalid HTML and an a11y violation. */}\n <div className=\"space-y-6\">{children}</div>\n </div>\n </div>\n );\n}\n\nexport { PageSection } from \"./PageSection\";\nexport { EmptyState } from \"./EmptyState\";\n"],"mappings":";;;;;AA2BA,IAAM,IAAkB;CACtB,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,OAAO;CACP,OAAO;CACP,MAAM;CACP;AAWD,SAAgB,EAAW,EACzB,aACA,UACA,aACA,SACA,kBACA,kBACA,cAAW,MACX,gBAAa,IACb,cACA,qBACA,eACkB;AAClB,QACE,kBAAC,OAAD;EACE,aAAW;EACX,WAAW,EAET,qBAEA,iCAEA,yBAEA,4DACA,EACD;YAGD,kBAAC,OAAD;GACE,WAAW,EACT,kBACA,EAAgB,IAChB,EACD;aALH,CAQG,MAAe,KAAS,MACvB,kBAAC,UAAD;IAAQ,WAAU;cAAlB,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CAEE,kBAAC,OAAD;MAAK,WAAU;gBAAf,EACI,KAAS,MACT,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACG,KACC,kBAAC,OAAD;QAAK,WAAU;kBACZ;QACG,CAAA,EAEP,KACC,kBAAC,MAAD;QAAI,WAAU;kBACX;QACE,CAAA,CAEH;UAEP,KACC,kBAAC,KAAD;OAAG,WAAU;iBACV;OACC,CAAA,CAEF;SAGL,KACC,kBAAC,OAAD;MAAK,WAAU;gBACZ;MACG,CAAA,CAEJ;QAGL,KAAiB,kBAAC,OAAD;KAAK,WAAU;eAAQ;KAAoB,CAAA,CACtD;OAMX,kBAAC,OAAD;IAAK,WAAU;IAAa;IAAe,CAAA,CACvC;;EACF,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { jsx as t, jsxs as n } from "react/jsx-runtime";
|
|
3
|
+
//#region src/conversations/components/PageSection.tsx
|
|
4
|
+
function r({ children: r, title: i, description: a, actions: o, className: s }) {
|
|
5
|
+
return /* @__PURE__ */ n("section", {
|
|
6
|
+
className: e("space-y-4", s),
|
|
7
|
+
children: [(i || o) && /* @__PURE__ */ n("div", {
|
|
8
|
+
className: "flex items-center justify-between",
|
|
9
|
+
children: [/* @__PURE__ */ n("div", { children: [i && /* @__PURE__ */ t("h2", {
|
|
10
|
+
className: "text-lg font-semibold text-foreground",
|
|
11
|
+
children: i
|
|
12
|
+
}), a && /* @__PURE__ */ t("p", {
|
|
13
|
+
className: "text-sm text-muted-foreground",
|
|
14
|
+
children: a
|
|
15
|
+
})] }), o && /* @__PURE__ */ t("div", {
|
|
16
|
+
className: "flex items-center gap-2",
|
|
17
|
+
children: o
|
|
18
|
+
})]
|
|
19
|
+
}), r]
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { r as PageSection };
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=PageSection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PageSection.js","names":[],"sources":["../../../src/conversations/components/PageSection.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../utils/cn\";\n\n/**\n * PageSection - A section within a page with consistent spacing\n */\nexport function PageSection({\n children,\n title,\n description,\n actions,\n className,\n}: {\n children: ReactNode;\n title?: string;\n description?: string;\n actions?: ReactNode;\n className?: string;\n}) {\n return (\n <section className={cn(\"space-y-4\", className)}>\n {(title || actions) && (\n <div className=\"flex items-center justify-between\">\n <div>\n {title && (\n <h2 className=\"text-lg font-semibold text-foreground\">{title}</h2>\n )}\n {description && (\n <p className=\"text-sm text-muted-foreground\">{description}</p>\n )}\n </div>\n {actions && <div className=\"flex items-center gap-2\">{actions}</div>}\n </div>\n )}\n {children}\n </section>\n );\n}\n"],"mappings":";;;AAMA,SAAgB,EAAY,EAC1B,aACA,UACA,gBACA,YACA,gBAOC;AACD,QACE,kBAAC,WAAD;EAAS,WAAW,EAAG,aAAa,EAAU;YAA9C,EACI,KAAS,MACT,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACG,KACC,kBAAC,MAAD;IAAI,WAAU;cAAyC;IAAW,CAAA,EAEnE,KACC,kBAAC,KAAD;IAAG,WAAU;cAAiC;IAAgB,CAAA,CAE5D,EAAA,CAAA,EACL,KAAW,kBAAC,OAAD;IAAK,WAAU;cAA2B;IAAc,CAAA,CAChE;MAEP,EACO"}
|
|
@@ -11,7 +11,7 @@ var s = ({ permission: s, children: c, allowUnavailablePermissions: l = !1 }) =>
|
|
|
11
11
|
};
|
|
12
12
|
if (m) return /* @__PURE__ */ a("div", {
|
|
13
13
|
className: "flex min-h-screen items-center justify-center bg-bg-canvas",
|
|
14
|
-
children: /* @__PURE__ */ a(t, { className: "
|
|
14
|
+
children: /* @__PURE__ */ a(t, { className: "size-8 animate-spin text-primary" })
|
|
15
15
|
});
|
|
16
16
|
let b = !h && !f && u && !!d?.accessToken && !!_ && g.status === "pending" && g.fetchStatus === "idle";
|
|
17
17
|
return !p(s) && !(l && b) ? /* @__PURE__ */ a(e, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PermissionGuard.js","names":[],"sources":["../../../src/conversations/components/PermissionGuard.tsx"],"sourcesContent":["import type { FC, ReactNode } from \"react\";\nimport { Loader2 } from \"lucide-react\";\nimport {\n useAuth,\n usePermissions,\n} from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport { AccessDenied } from \"@burdenoff/fe-libs/shared/components\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport type { ConversationsPermission } from \"../constants/permissions\";\n\ninterface PermissionGuardProps {\n permission: ConversationsPermission;\n children: ReactNode;\n allowUnavailablePermissions?: boolean;\n}\n\n/**\n * Wraps page content with a permission check.\n * Shows a loading spinner while permissions resolve, then either\n * renders children (if permitted) or an AccessDenied screen.\n *\n * `allowUnavailablePermissions` is for user-facing routes whose backend\n * resolvers remain authoritative but whose host shell may not have hydrated\n * local RBAC context yet. It only permits rendering when permission data is\n * unavailable; explicit loaded denials still render AccessDenied.\n */\nexport const PermissionGuard: FC<PermissionGuardProps> = ({\n permission,\n children,\n allowUnavailablePermissions = false,\n}) => {\n const { isAuthenticated, tokens } = useAuth();\n const { error, hasPermission, isLoading, permissions, query, scopeId } =\n usePermissions();\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 if (isLoading) {\n return (\n <div className=\"flex min-h-screen items-center justify-center bg-bg-canvas\">\n <Loader2 className=\"
|
|
1
|
+
{"version":3,"file":"PermissionGuard.js","names":[],"sources":["../../../src/conversations/components/PermissionGuard.tsx"],"sourcesContent":["import type { FC, ReactNode } from \"react\";\nimport { Loader2 } from \"lucide-react\";\nimport {\n useAuth,\n usePermissions,\n} from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport { AccessDenied } from \"@burdenoff/fe-libs/shared/components\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport type { ConversationsPermission } from \"../constants/permissions\";\n\ninterface PermissionGuardProps {\n permission: ConversationsPermission;\n children: ReactNode;\n allowUnavailablePermissions?: boolean;\n}\n\n/**\n * Wraps page content with a permission check.\n * Shows a loading spinner while permissions resolve, then either\n * renders children (if permitted) or an AccessDenied screen.\n *\n * `allowUnavailablePermissions` is for user-facing routes whose backend\n * resolvers remain authoritative but whose host shell may not have hydrated\n * local RBAC context yet. It only permits rendering when permission data is\n * unavailable; explicit loaded denials still render AccessDenied.\n */\nexport const PermissionGuard: FC<PermissionGuardProps> = ({\n permission,\n children,\n allowUnavailablePermissions = false,\n}) => {\n const { isAuthenticated, tokens } = useAuth();\n const { error, hasPermission, isLoading, permissions, query, scopeId } =\n usePermissions();\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 if (isLoading) {\n return (\n <div className=\"flex min-h-screen items-center justify-center bg-bg-canvas\">\n <Loader2 className=\"size-8 animate-spin text-primary\" />\n </div>\n );\n }\n\n const permissionHydrationPending =\n !permissions &&\n !error &&\n isAuthenticated &&\n !!tokens?.accessToken &&\n !!scopeId &&\n query.status === \"pending\" &&\n query.fetchStatus === \"idle\";\n\n if (\n !hasPermission(permission) &&\n !(allowUnavailablePermissions && permissionHydrationPending)\n ) {\n return (\n <AccessDenied\n title={tr(\"conversations.accessDenied\", \"Access Denied\")}\n description={tr(\n \"conversations.noPermissionView\",\n \"You don't have permission to view this page.\",\n )}\n />\n );\n }\n\n return <>{children}</>;\n};\n"],"mappings":";;;;;;AA0BA,IAAa,KAA6C,EACxD,eACA,aACA,iCAA8B,SAC1B;CACJ,IAAM,EAAE,oBAAiB,cAAW,GAAS,EACvC,EAAE,UAAO,kBAAe,cAAW,gBAAa,UAAO,eAC3D,GAAgB,EACZ,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;;AAGpC,KAAI,EACF,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD,EAAS,WAAU,oCAAqC,CAAA;EACpD,CAAA;CAIV,IAAM,IACJ,CAAC,KACD,CAAC,KACD,KACA,CAAC,CAAC,GAAQ,eACV,CAAC,CAAC,KACF,EAAM,WAAW,aACjB,EAAM,gBAAgB;AAiBxB,QAdE,CAAC,EAAc,EAAW,IAC1B,EAAE,KAA+B,KAG/B,kBAAC,GAAD;EACE,OAAO,EAAG,8BAA8B,gBAAgB;EACxD,aAAa,EACX,kCACA,+CACD;EACD,CAAA,GAIC,kBAAA,GAAA,EAAG,aAAY,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { lazy as e } from "react";
|
|
2
|
+
//#region src/conversations/components/lazyRetry.ts
|
|
3
|
+
function t(e) {
|
|
4
|
+
if (!e) return !1;
|
|
5
|
+
if (e.name === "ChunkLoadError") return !0;
|
|
6
|
+
let t = e.message ?? "";
|
|
7
|
+
return t.includes("Loading chunk") || t.includes("Failed to fetch dynamically imported module") || t.includes("Importing a module script failed") || t.includes("Failed to fetch");
|
|
8
|
+
}
|
|
9
|
+
var n = 300;
|
|
10
|
+
function r(r) {
|
|
11
|
+
return e(() => r().catch((e) => {
|
|
12
|
+
if (e instanceof Error && t(e)) return new Promise((e, t) => {
|
|
13
|
+
setTimeout(() => {
|
|
14
|
+
r().then(e, t);
|
|
15
|
+
}, n);
|
|
16
|
+
});
|
|
17
|
+
throw e;
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { t as isChunkLoadError, r as lazyRetry };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=lazyRetry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lazyRetry.js","names":[],"sources":["../../../src/conversations/components/lazyRetry.ts"],"sourcesContent":["import {\n lazy,\n type ComponentType,\n type LazyExoticComponent,\n} from \"react\";\n\nfunction isChunkLoadError(error: Error | null | undefined): boolean {\n if (!error) return false;\n if (error.name === \"ChunkLoadError\") return true;\n const message = error.message ?? \"\";\n return (\n message.includes(\"Loading chunk\") ||\n message.includes(\"Failed to fetch dynamically imported module\") ||\n message.includes(\"Importing a module script failed\") ||\n message.includes(\"Failed to fetch\")\n );\n}\n\nconst LAZY_RETRY_DELAY_MS = 300;\n\nexport function lazyRetry<TProps extends object = object>(\n factory: () => Promise<{ default: ComponentType<TProps> }>,\n): LazyExoticComponent<ComponentType<TProps>> {\n return lazy(() =>\n factory().catch((err: unknown) => {\n if (err instanceof Error && isChunkLoadError(err)) {\n return new Promise<{ default: ComponentType<TProps> }>(\n (resolve, reject) => {\n setTimeout(() => {\n factory().then(resolve, reject);\n }, LAZY_RETRY_DELAY_MS);\n },\n );\n }\n throw err;\n }),\n );\n}\n\nexport { isChunkLoadError };\n"],"mappings":";;AAMA,SAAS,EAAiB,GAA0C;AAClE,KAAI,CAAC,EAAO,QAAO;AACnB,KAAI,EAAM,SAAS,iBAAkB,QAAO;CAC5C,IAAM,IAAU,EAAM,WAAW;AACjC,QACE,EAAQ,SAAS,gBAAgB,IACjC,EAAQ,SAAS,8CAA8C,IAC/D,EAAQ,SAAS,mCAAmC,IACpD,EAAQ,SAAS,kBAAkB;;AAIvC,IAAM,IAAsB;AAE5B,SAAgB,EACd,GAC4C;AAC5C,QAAO,QACL,GAAS,CAAC,OAAO,MAAiB;AAChC,MAAI,aAAe,SAAS,EAAiB,EAAI,CAC/C,QAAO,IAAI,SACR,GAAS,MAAW;AACnB,oBAAiB;AACf,OAAS,CAAC,KAAK,GAAS,EAAO;MAC9B,EAAoB;IAE1B;AAEH,QAAM;GACN,CACH"}
|
|
@@ -61,7 +61,6 @@ var e = {
|
|
|
61
61
|
SCHEDULED_COMMENT_DELETE: "scheduled-comment:delete",
|
|
62
62
|
SCHEDULED_COMMENT_EXECUTE: "scheduled-comment:execute"
|
|
63
63
|
};
|
|
64
|
-
e.CONVERSATION_READ, e.CONVERSATION_LIST, e.CONVERSATION_CREATE, e.CONVERSATION_UPDATE, e.MESSAGE_READ, e.MESSAGE_CREATE, e.MESSAGE_UPDATE, e.MESSAGE_DELETE, e.MESSAGE_REACT, e.MESSAGE_REPORT, e.MESSAGE_VOTE, e.MESSAGE_DRAFT_READ, e.MESSAGE_DRAFT_CREATE, e.MESSAGE_DRAFT_DELETE, e.PARTICIPANT_READ, e.PARTICIPANT_LEAVE, e.CONVERSATION_INVITE_READ, e.CONVERSATION_INVITE_CREATE, e.CONVERSATION_INVITE_JOIN, e.CALL_READ, e.CALL_CREATE, e.CALL_JOIN, e.CALL_LEAVE, e.CALL_UPDATE, e.APP_INSTALLATION_READ, e.PRESENCE_READ, e.PRESENCE_UPDATE, e.ENTITY_COMMENT_READ, e.ENTITY_COMMENT_CREATE, e.ENTITY_COMMENT_UPDATE, e.ENTITY_COMMENT_DELETE, e.COMMENT_ATTACHMENT_READ, e.COMMENT_ATTACHMENT_CREATE, e.COMMENT_ATTACHMENT_UPDATE, e.COMMENT_ATTACHMENT_DELETE, e.COMMENT_TAG_READ, e.SCHEDULED_COMMENT_READ, e.SCHEDULED_COMMENT_CREATE, e.SCHEDULED_COMMENT_UPDATE, e.SCHEDULED_COMMENT_DELETE, e.CONVERSATION_DELETE, e.CONVERSATION_ARCHIVE, e.CONVERSATION_UNARCHIVE, e.MESSAGE_PIN, e.MESSAGE_MODERATE, e.PARTICIPANT_ADD, e.PARTICIPANT_REMOVE, e.PARTICIPANT_UPDATE, e.CALL_END, e.CALL_RECORD, e.APP_INSTALLATION_CREATE, e.APP_INSTALLATION_UPDATE, e.APP_INSTALLATION_DELETE, e.APP_INSTALLATION_EXECUTE, e.ENTITY_COMMENT_ASSIGN, e.ENTITY_COMMENT_EXPORT, e.COMMENT_TAG_ASSIGN, e.COMMENT_TAG_UPDATE, e.COMMENT_TAG_DELETE, e.SCHEDULED_COMMENT_EXECUTE;
|
|
65
64
|
//#endregion
|
|
66
65
|
export { e as CONVERSATIONS_PERMISSIONS };
|
|
67
66
|
|