@burdenoff/microfe-conversations 2026.830.1 → 2026.830.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conversations/components/ConversationRow.js +81 -81
- package/dist/conversations/components/ConversationRow.js.map +1 -1
- package/dist/conversations/components/MessageBubble.js +116 -113
- package/dist/conversations/components/MessageBubble.js.map +1 -1
- package/dist/conversations/components/PermissionGuard.js +13 -16
- package/dist/conversations/components/PermissionGuard.js.map +1 -1
- package/dist/conversations/index.js +1 -0
- package/dist/conversations/pages/ConversationAdminPages.js +64 -79
- package/dist/conversations/pages/ConversationAdminPages.js.map +1 -1
- package/dist/conversations/pages/ConversationDetailPage.js +120 -120
- package/dist/conversations/pages/ConversationDetailPage.js.map +1 -1
- package/dist/conversations/utils/i18n.js +3 -3
- package/dist/conversations/utils/i18n.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/translations/en.js +319 -0
- package/dist/translations/en.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { useTr as e } from "../utils/i18n.js";
|
|
2
|
+
import { cn as t } from "../utils/cn.js";
|
|
3
|
+
import { memo as n, useState as r } from "react";
|
|
4
|
+
import { Check as i, CheckCheck as a, FileText as o, Image as s, MoreHorizontal as c, Pencil as l, Pin as u, Reply as d, SmilePlus as f, Trash2 as p } from "lucide-react";
|
|
5
|
+
import { Fragment as m, jsx as h, jsxs as g } from "react/jsx-runtime";
|
|
6
6
|
//#region src/conversations/components/MessageBubble.tsx
|
|
7
7
|
function _(e) {
|
|
8
8
|
return new Date(e).toLocaleTimeString(void 0, {
|
|
@@ -12,20 +12,20 @@ function _(e) {
|
|
|
12
12
|
}
|
|
13
13
|
function v({ status: e, failedLabel: t }) {
|
|
14
14
|
switch (e) {
|
|
15
|
-
case "SENDING": return /* @__PURE__ */
|
|
16
|
-
case "SENT": return /* @__PURE__ */
|
|
15
|
+
case "SENDING": return /* @__PURE__ */ h("span", { className: "size-3 rounded-full border border-muted-foreground animate-pulse" });
|
|
16
|
+
case "SENT": return /* @__PURE__ */ h(i, {
|
|
17
17
|
size: 12,
|
|
18
18
|
className: "text-text-secondary"
|
|
19
19
|
});
|
|
20
|
-
case "DELIVERED": return /* @__PURE__ */
|
|
20
|
+
case "DELIVERED": return /* @__PURE__ */ h(a, {
|
|
21
21
|
size: 12,
|
|
22
22
|
className: "text-text-secondary"
|
|
23
23
|
});
|
|
24
|
-
case "READ": return /* @__PURE__ */
|
|
24
|
+
case "READ": return /* @__PURE__ */ h(a, {
|
|
25
25
|
size: 12,
|
|
26
26
|
className: "text-primary"
|
|
27
27
|
});
|
|
28
|
-
case "FAILED": return /* @__PURE__ */
|
|
28
|
+
case "FAILED": return /* @__PURE__ */ h("span", {
|
|
29
29
|
className: "text-[10px] text-status-error-text font-medium",
|
|
30
30
|
children: t
|
|
31
31
|
});
|
|
@@ -35,161 +35,164 @@ function v({ status: e, failedLabel: t }) {
|
|
|
35
35
|
function y(e) {
|
|
36
36
|
return e.startsWith("image/");
|
|
37
37
|
}
|
|
38
|
-
var b =
|
|
39
|
-
let [O, k] =
|
|
40
|
-
|
|
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, G = R === "SYSTEM", K = M || O || A;
|
|
43
|
-
return G ? /* @__PURE__ */ m("div", {
|
|
38
|
+
var b = n(function({ message: n, isOwnMessage: i = !1, showAvatar: a = !0, showSender: b = !0, onReply: C, onReact: w, onEdit: T, onDelete: E, onTogglePin: D }) {
|
|
39
|
+
let [O, k] = r(!1), [A, j] = r(!1), [M, N] = r(!1), P = e(), { sender: F, content: I, type: L, status: R, reactions: z, attachments: B, isEdited: V, createdAt: H, isPinned: U } = n, W = L === "SYSTEM", G = M || O || A;
|
|
40
|
+
return W ? /* @__PURE__ */ h("div", {
|
|
44
41
|
className: "flex justify-center my-2 px-4",
|
|
45
|
-
children: /* @__PURE__ */
|
|
42
|
+
children: /* @__PURE__ */ h("span", {
|
|
46
43
|
className: "text-xs text-text-secondary bg-bg-sunken/50 px-3 py-1 rounded-full",
|
|
47
|
-
children:
|
|
44
|
+
children: I
|
|
48
45
|
})
|
|
49
|
-
}) : /* @__PURE__ */
|
|
46
|
+
}) : /* @__PURE__ */ g("div", {
|
|
50
47
|
onMouseEnter: () => N(!0),
|
|
51
48
|
onMouseLeave: () => N(!1),
|
|
52
|
-
className:
|
|
49
|
+
className: t("group flex gap-2 px-4 py-1", i ? "flex-row-reverse" : "flex-row"),
|
|
53
50
|
children: [
|
|
54
|
-
|
|
55
|
-
className:
|
|
56
|
-
children:
|
|
57
|
-
src:
|
|
58
|
-
alt:
|
|
51
|
+
a ? /* @__PURE__ */ h("div", {
|
|
52
|
+
className: t("flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold mt-0.5", i ? "bg-action-primary-bg/10 text-primary" : "bg-bg-sunken text-text-secondary"),
|
|
53
|
+
children: F.avatarUrl ? /* @__PURE__ */ h("img", {
|
|
54
|
+
src: F.avatarUrl,
|
|
55
|
+
alt: F.name,
|
|
59
56
|
className: "w-full h-full rounded-full object-cover"
|
|
60
|
-
}) :
|
|
61
|
-
}) : /* @__PURE__ */
|
|
62
|
-
/* @__PURE__ */
|
|
63
|
-
className:
|
|
57
|
+
}) : F.name.split(" ").map((e) => e[0]).join("").toUpperCase().slice(0, 2)
|
|
58
|
+
}) : /* @__PURE__ */ h("div", { className: "w-8 flex-shrink-0" }),
|
|
59
|
+
/* @__PURE__ */ g("div", {
|
|
60
|
+
className: t("flex flex-col max-w-[85%] sm:max-w-[70%]", i ? "items-end" : "items-start"),
|
|
64
61
|
children: [
|
|
65
|
-
b && !
|
|
62
|
+
b && !i && /* @__PURE__ */ g("div", {
|
|
66
63
|
className: "flex items-center gap-1.5 mb-0.5",
|
|
67
|
-
children: [/* @__PURE__ */
|
|
64
|
+
children: [/* @__PURE__ */ h("span", {
|
|
68
65
|
className: "text-xs font-semibold text-text-primary",
|
|
69
|
-
children:
|
|
70
|
-
}),
|
|
66
|
+
children: F.name
|
|
67
|
+
}), U && /* @__PURE__ */ h(u, {
|
|
71
68
|
size: 10,
|
|
72
69
|
className: "text-status-warning-text"
|
|
73
70
|
})]
|
|
74
71
|
}),
|
|
75
|
-
/* @__PURE__ */
|
|
76
|
-
className:
|
|
77
|
-
children: [/* @__PURE__ */
|
|
72
|
+
/* @__PURE__ */ g("div", {
|
|
73
|
+
className: t("relative rounded-2xl px-3.5 py-2 text-sm leading-relaxed", i ? "bg-action-primary-bg text-action-primary-text rounded-tr-md" : "bg-bg-sunken/70 text-text-primary rounded-tl-md"),
|
|
74
|
+
children: [/* @__PURE__ */ h("p", {
|
|
78
75
|
className: "whitespace-pre-wrap break-words",
|
|
79
|
-
children:
|
|
80
|
-
}),
|
|
76
|
+
children: I
|
|
77
|
+
}), B.length > 0 && /* @__PURE__ */ h("div", {
|
|
81
78
|
className: "mt-2 grid gap-2",
|
|
82
|
-
children:
|
|
83
|
-
let n =
|
|
79
|
+
children: B.map((e) => {
|
|
80
|
+
let n = t("flex min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-xs transition-colors", i ? "bg-primary-foreground/10 text-action-primary-text hover:bg-primary-foreground/15" : "bg-bg-surface/50 text-text-primary hover:bg-bg-surface/70"), r = /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h("div", {
|
|
84
81
|
className: "flex size-10 flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-bg-surface/40",
|
|
85
|
-
children:
|
|
86
|
-
src:
|
|
87
|
-
alt:
|
|
82
|
+
children: e.thumbnail && y(e.mimeType) ? /* @__PURE__ */ h("img", {
|
|
83
|
+
src: e.thumbnail,
|
|
84
|
+
alt: e.fileName,
|
|
88
85
|
className: "h-full w-full object-contain"
|
|
89
|
-
}) : y(
|
|
90
|
-
}), /* @__PURE__ */
|
|
86
|
+
}) : y(e.mimeType) ? /* @__PURE__ */ h(s, { size: 16 }) : /* @__PURE__ */ h(o, { size: 16 })
|
|
87
|
+
}), /* @__PURE__ */ g("div", {
|
|
91
88
|
className: "min-w-0 flex-1",
|
|
92
|
-
children: [/* @__PURE__ */
|
|
89
|
+
children: [/* @__PURE__ */ h("div", {
|
|
93
90
|
className: "truncate font-medium",
|
|
94
|
-
children:
|
|
95
|
-
}), /* @__PURE__ */
|
|
91
|
+
children: e.fileName
|
|
92
|
+
}), /* @__PURE__ */ g("div", {
|
|
96
93
|
className: "truncate text-[10px] opacity-70",
|
|
97
94
|
children: [
|
|
98
|
-
S(
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
S(e.fileSize, {
|
|
96
|
+
B: P("conversations.fileSizeUnits.B", "B"),
|
|
97
|
+
KB: P("conversations.fileSizeUnits.KB", "KB"),
|
|
98
|
+
MB: P("conversations.fileSizeUnits.MB", "MB"),
|
|
99
|
+
GB: P("conversations.fileSizeUnits.GB", "GB")
|
|
100
|
+
}),
|
|
101
|
+
" ",
|
|
102
|
+
"- ",
|
|
103
|
+
e.mimeType
|
|
101
104
|
]
|
|
102
105
|
})]
|
|
103
106
|
})] });
|
|
104
|
-
return
|
|
105
|
-
href:
|
|
107
|
+
return e.downloadUrl ? /* @__PURE__ */ h("a", {
|
|
108
|
+
href: e.downloadUrl,
|
|
106
109
|
target: "_blank",
|
|
107
110
|
rel: "noreferrer",
|
|
108
111
|
className: n,
|
|
109
|
-
title:
|
|
110
|
-
children:
|
|
111
|
-
},
|
|
112
|
+
title: e.fileName,
|
|
113
|
+
children: r
|
|
114
|
+
}, e.id) : /* @__PURE__ */ h("div", {
|
|
112
115
|
className: n,
|
|
113
|
-
title:
|
|
114
|
-
children:
|
|
115
|
-
},
|
|
116
|
+
title: e.fileName,
|
|
117
|
+
children: r
|
|
118
|
+
}, e.id);
|
|
116
119
|
})
|
|
117
120
|
})]
|
|
118
121
|
}),
|
|
119
|
-
|
|
122
|
+
z.length > 0 && /* @__PURE__ */ h("div", {
|
|
120
123
|
className: "flex flex-wrap gap-1 mt-1",
|
|
121
|
-
children:
|
|
124
|
+
children: z.map((e) => /* @__PURE__ */ g("button", {
|
|
122
125
|
type: "button",
|
|
123
|
-
onClick: () => w?.(
|
|
124
|
-
className:
|
|
125
|
-
children: [/* @__PURE__ */
|
|
126
|
+
onClick: () => w?.(n.id, e.emoji),
|
|
127
|
+
className: t("inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs", "border transition-colors", e.currentUserReacted ? "bg-action-primary-bg/10 border-primary/30 text-primary" : "bg-bg-sunken/50 border-border-subtle text-text-secondary hover:bg-bg-sunken"),
|
|
128
|
+
children: [/* @__PURE__ */ h("span", { children: e.emoji }), /* @__PURE__ */ h("span", {
|
|
126
129
|
className: "font-medium",
|
|
127
|
-
children:
|
|
130
|
+
children: e.count
|
|
128
131
|
})]
|
|
129
|
-
},
|
|
132
|
+
}, e.emoji))
|
|
130
133
|
}),
|
|
131
|
-
/* @__PURE__ */
|
|
134
|
+
/* @__PURE__ */ g("div", {
|
|
132
135
|
className: "flex items-center gap-1.5 mt-0.5 px-1",
|
|
133
136
|
children: [
|
|
134
|
-
/* @__PURE__ */
|
|
137
|
+
/* @__PURE__ */ h("span", {
|
|
135
138
|
className: "text-[10px] text-text-secondary",
|
|
136
|
-
children: _(
|
|
139
|
+
children: _(H)
|
|
137
140
|
}),
|
|
138
|
-
|
|
141
|
+
V && /* @__PURE__ */ h("span", {
|
|
139
142
|
className: "text-[10px] text-text-secondary italic",
|
|
140
|
-
children:
|
|
143
|
+
children: P("conversations.edited", "edited")
|
|
141
144
|
}),
|
|
142
|
-
|
|
143
|
-
status:
|
|
144
|
-
failedLabel:
|
|
145
|
+
i && /* @__PURE__ */ h(v, {
|
|
146
|
+
status: R,
|
|
147
|
+
failedLabel: P("conversations.failed", "Failed")
|
|
145
148
|
})
|
|
146
149
|
]
|
|
147
150
|
})
|
|
148
151
|
]
|
|
149
152
|
}),
|
|
150
|
-
/* @__PURE__ */
|
|
153
|
+
/* @__PURE__ */ g("div", {
|
|
151
154
|
"data-tour": "conversation-message-actions",
|
|
152
155
|
role: "toolbar",
|
|
153
|
-
"aria-label":
|
|
156
|
+
"aria-label": P("conversations.messageActions", "Message actions"),
|
|
154
157
|
tabIndex: 0,
|
|
155
158
|
onFocus: () => N(!0),
|
|
156
159
|
onBlur: (e) => {
|
|
157
160
|
e.currentTarget.contains(e.relatedTarget) || N(!1);
|
|
158
161
|
},
|
|
159
|
-
className:
|
|
162
|
+
className: t("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-[var(--color-focus-ring)] focus:ring-offset-2 sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100", G && "sm:opacity-100"),
|
|
160
163
|
children: [
|
|
161
|
-
|
|
162
|
-
icon: /* @__PURE__ */
|
|
163
|
-
label:
|
|
164
|
+
n.permissions.canReact && /* @__PURE__ */ h(x, {
|
|
165
|
+
icon: /* @__PURE__ */ h(f, { size: 14 }),
|
|
166
|
+
label: P("conversations.react", "React"),
|
|
164
167
|
pressed: O,
|
|
165
168
|
onClick: () => {
|
|
166
169
|
k((e) => !e), j(!1);
|
|
167
170
|
}
|
|
168
171
|
}),
|
|
169
|
-
|
|
170
|
-
icon: /* @__PURE__ */
|
|
171
|
-
label:
|
|
172
|
-
onClick: () => C?.(
|
|
172
|
+
n.permissions.canReply && /* @__PURE__ */ h(x, {
|
|
173
|
+
icon: /* @__PURE__ */ h(d, { size: 14 }),
|
|
174
|
+
label: P("conversations.reply", "Reply"),
|
|
175
|
+
onClick: () => C?.(n)
|
|
173
176
|
}),
|
|
174
|
-
|
|
175
|
-
icon: /* @__PURE__ */
|
|
176
|
-
label:
|
|
177
|
-
onClick: () => T?.(
|
|
177
|
+
n.permissions.canEdit && /* @__PURE__ */ h(x, {
|
|
178
|
+
icon: /* @__PURE__ */ h(l, { size: 14 }),
|
|
179
|
+
label: P("conversations.edit", "Edit"),
|
|
180
|
+
onClick: () => T?.(n)
|
|
178
181
|
}),
|
|
179
|
-
|
|
180
|
-
icon: /* @__PURE__ */
|
|
181
|
-
label:
|
|
182
|
-
onClick: () => E?.(
|
|
182
|
+
n.permissions.canDelete && /* @__PURE__ */ h(x, {
|
|
183
|
+
icon: /* @__PURE__ */ h(p, { size: 14 }),
|
|
184
|
+
label: P("conversations.delete", "Delete"),
|
|
185
|
+
onClick: () => E?.(n.id)
|
|
183
186
|
}),
|
|
184
|
-
/* @__PURE__ */
|
|
185
|
-
icon: /* @__PURE__ */
|
|
186
|
-
label:
|
|
187
|
+
/* @__PURE__ */ h(x, {
|
|
188
|
+
icon: /* @__PURE__ */ h(c, { size: 14 }),
|
|
189
|
+
label: P("conversations.more", "More"),
|
|
187
190
|
pressed: A,
|
|
188
191
|
onClick: () => {
|
|
189
192
|
j((e) => !e), k(!1);
|
|
190
193
|
}
|
|
191
194
|
}),
|
|
192
|
-
O && /* @__PURE__ */
|
|
195
|
+
O && /* @__PURE__ */ h("div", {
|
|
193
196
|
className: "absolute top-full z-10 mt-1 flex items-center gap-1 rounded-lg border border-border-subtle bg-popover p-1 shadow-lg",
|
|
194
197
|
children: [
|
|
195
198
|
"👍",
|
|
@@ -197,25 +200,25 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
|
|
|
197
200
|
"🎉",
|
|
198
201
|
"👀",
|
|
199
202
|
"✅"
|
|
200
|
-
].map((e) => /* @__PURE__ */
|
|
203
|
+
].map((e) => /* @__PURE__ */ h("button", {
|
|
201
204
|
type: "button",
|
|
202
205
|
onClick: () => {
|
|
203
|
-
w?.(
|
|
206
|
+
w?.(n.id, e), k(!1);
|
|
204
207
|
},
|
|
205
208
|
className: "rounded-md px-2 py-1 text-sm hover:bg-bg-sunken/80",
|
|
206
|
-
"aria-label": `${
|
|
209
|
+
"aria-label": `${P("conversations.reactWith", "React with")} ${e}`,
|
|
207
210
|
children: e
|
|
208
211
|
}, e))
|
|
209
212
|
}),
|
|
210
|
-
A &&
|
|
213
|
+
A && n.permissions.canPin && D && /* @__PURE__ */ h("div", {
|
|
211
214
|
className: "absolute right-0 top-full z-10 mt-1 min-w-40 rounded-lg border border-border-subtle bg-popover p-1 shadow-lg",
|
|
212
|
-
children: /* @__PURE__ */
|
|
215
|
+
children: /* @__PURE__ */ g("button", {
|
|
213
216
|
type: "button",
|
|
214
217
|
onClick: () => {
|
|
215
|
-
D(
|
|
218
|
+
D(n.id, n.isPinned), j(!1);
|
|
216
219
|
},
|
|
217
220
|
className: "flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-text-primary hover:bg-bg-sunken/80",
|
|
218
|
-
children: [/* @__PURE__ */
|
|
221
|
+
children: [/* @__PURE__ */ h(u, { size: 14 }), n.isPinned ? P("conversations.unpinMessage", "Unpin message") : P("conversations.pinMessage", "Pin message")]
|
|
219
222
|
})
|
|
220
223
|
})
|
|
221
224
|
]
|
|
@@ -224,7 +227,7 @@ var b = t(function({ message: t, isOwnMessage: r = !1, showAvatar: i = !0, showS
|
|
|
224
227
|
});
|
|
225
228
|
});
|
|
226
229
|
function x({ icon: e, label: t, onClick: n, pressed: r }) {
|
|
227
|
-
return /* @__PURE__ */
|
|
230
|
+
return /* @__PURE__ */ h("button", {
|
|
228
231
|
type: "button",
|
|
229
232
|
onClick: n,
|
|
230
233
|
"aria-label": t,
|
|
@@ -234,15 +237,15 @@ function x({ icon: e, label: t, onClick: n, pressed: r }) {
|
|
|
234
237
|
children: e
|
|
235
238
|
});
|
|
236
239
|
}
|
|
237
|
-
function S(e) {
|
|
238
|
-
if (e === 0) return
|
|
239
|
-
let
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
],
|
|
245
|
-
return `${parseFloat((e /
|
|
240
|
+
function S(e, t) {
|
|
241
|
+
if (e === 0) return `0 ${t.B}`;
|
|
242
|
+
let n = 1024, r = [
|
|
243
|
+
t.B,
|
|
244
|
+
t.KB,
|
|
245
|
+
t.MB,
|
|
246
|
+
t.GB
|
|
247
|
+
], i = Math.floor(Math.log(e) / Math.log(n));
|
|
248
|
+
return `${parseFloat((e / n ** +i).toFixed(1))} ${r[i]}`;
|
|
246
249
|
}
|
|
247
250
|
//#endregion
|
|
248
251
|
export { b as MessageBubble };
|
|
@@ -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=\"size-3 rounded-full border border-muted-foreground animate-pulse\" />\n );\n case \"SENT\":\n return <Check size={12} className=\"text-text-secondary\" />;\n case \"DELIVERED\":\n return <CheckCheck size={12} className=\"text-text-secondary\" />;\n case \"READ\":\n return <CheckCheck size={12} className=\"text-primary\" />;\n case \"FAILED\":\n return (\n <span className=\"text-[10px] text-status-error-text 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-text-secondary bg-bg-sunken/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-action-primary-bg/10 text-primary\"\n : \"bg-bg-sunken text-text-secondary\",\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-text-primary\">\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-action-primary-bg text-action-primary-text rounded-tr-md\"\n : \"bg-bg-sunken/70 text-text-primary 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-action-primary-text hover:bg-primary-foreground/15\"\n : \"bg-bg-surface/50 text-text-primary hover:bg-bg-surface/70\",\n );\n const attachmentContent = (\n <>\n <div className=\"flex size-10 flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-bg-surface/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-action-primary-bg/10 border-primary/30 text-primary\"\n : \"bg-bg-sunken/50 border-border-subtle text-text-secondary hover:bg-bg-sunken\",\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-text-secondary\">\n {formatMessageTime(createdAt)}\n </span>\n {isEdited && (\n <span className=\"text-[10px] text-text-secondary 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-[var(--color-focus-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-subtle 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-bg-sunken/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-subtle 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-text-primary hover:bg-bg-sunken/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-text-secondary transition-colors hover:bg-bg-sunken/80 hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-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 {\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 { useTr } from \"../utils/i18n\";\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-text-secondary\" />;\n case \"DELIVERED\":\n return <CheckCheck size={12} className=\"text-text-secondary\" />;\n case \"READ\":\n return <CheckCheck size={12} className=\"text-primary\" />;\n case \"FAILED\":\n return (\n <span className=\"text-[10px] text-status-error-text 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 tr = useTr();\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-text-secondary bg-bg-sunken/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-action-primary-bg/10 text-primary\"\n : \"bg-bg-sunken text-text-secondary\",\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-text-primary\">\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-action-primary-bg text-action-primary-text rounded-tr-md\"\n : \"bg-bg-sunken/70 text-text-primary 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-action-primary-text hover:bg-primary-foreground/15\"\n : \"bg-bg-surface/50 text-text-primary hover:bg-bg-surface/70\",\n );\n const attachmentContent = (\n <>\n <div className=\"flex size-10 flex-shrink-0 items-center justify-center overflow-hidden rounded-md bg-bg-surface/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, {\n B: tr(\"conversations.fileSizeUnits.B\", \"B\"),\n KB: tr(\"conversations.fileSizeUnits.KB\", \"KB\"),\n MB: tr(\"conversations.fileSizeUnits.MB\", \"MB\"),\n GB: tr(\"conversations.fileSizeUnits.GB\", \"GB\"),\n })}{\" \"}\n - {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-action-primary-bg/10 border-primary/30 text-primary\"\n : \"bg-bg-sunken/50 border-border-subtle text-text-secondary hover:bg-bg-sunken\",\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-text-secondary\">\n {formatMessageTime(createdAt)}\n </span>\n {isEdited && (\n <span className=\"text-[10px] text-text-secondary 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-[var(--color-focus-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-subtle 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-bg-sunken/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-subtle 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-text-primary hover:bg-bg-sunken/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-text-secondary transition-colors hover:bg-bg-sunken/80 hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n {icon}\n </button>\n );\n}\n\n/**\n * Format file size in human-readable form\n */\nfunction formatFileSize(\n bytes: number,\n labels: { B: string; KB: string; MB: string; GB: string },\n): string {\n if (bytes === 0) return `0 ${labels.B}`;\n const k = 1024;\n const sizes = [labels.B, labels.KB, labels.MB, labels.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,23 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { useTr as e } from "../utils/i18n.js";
|
|
2
|
+
import { AccessDenied as t } from "@burdenoff/fe-libs/shared/components";
|
|
3
|
+
import { Loader2 as n } from "lucide-react";
|
|
4
|
+
import { useAuth as r, usePermissions as i } from "@burdenoff/fe-libs/shared/providers/shell";
|
|
5
|
+
import { Fragment as a, jsx as o } from "react/jsx-runtime";
|
|
6
6
|
//#region src/conversations/components/PermissionGuard.tsx
|
|
7
7
|
var s = ({ permission: s, children: c, allowUnavailablePermissions: l = !1 }) => {
|
|
8
|
-
let { isAuthenticated: u, tokens: d } =
|
|
9
|
-
|
|
10
|
-
return n === e ? t : n;
|
|
11
|
-
};
|
|
12
|
-
if (m) return /* @__PURE__ */ a("div", {
|
|
8
|
+
let { isAuthenticated: u, tokens: d } = r(), { error: f, hasPermission: p, isLoading: m, permissions: h, query: g, scopeId: _ } = i(), v = e();
|
|
9
|
+
if (m) return /* @__PURE__ */ o("div", {
|
|
13
10
|
className: "flex min-h-screen items-center justify-center bg-bg-canvas",
|
|
14
|
-
children: /* @__PURE__ */
|
|
11
|
+
children: /* @__PURE__ */ o(n, { className: "size-8 animate-spin text-primary" })
|
|
15
12
|
});
|
|
16
|
-
let
|
|
17
|
-
return !p(s) && !(l &&
|
|
18
|
-
title:
|
|
19
|
-
description:
|
|
20
|
-
}) : /* @__PURE__ */ a
|
|
13
|
+
let y = !h && !f && u && !!d?.accessToken && !!_ && g.status === "pending" && g.fetchStatus === "idle";
|
|
14
|
+
return !p(s) && !(l && y) ? /* @__PURE__ */ o(t, {
|
|
15
|
+
title: v("conversations.accessDenied", "Access Denied"),
|
|
16
|
+
description: v("conversations.noPermissionView", "You don't have permission to view this page.")
|
|
17
|
+
}) : /* @__PURE__ */ o(a, { children: c });
|
|
21
18
|
};
|
|
22
19
|
//#endregion
|
|
23
20
|
export { s as PermissionGuard };
|
|
@@ -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 {
|
|
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 { useTr } from \"../utils/i18n\";\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 tr = useTr();\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,IAAK,GAAO;AAElB,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"}
|