@burdenoff/microfe-conversations 2026.528.1 → 2026.529.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/ConversationsRoot.js +1 -1
- package/dist/conversations/ConversationsRoot.js.map +1 -1
- package/dist/conversations/ConversationsUserRoot.js +1 -1
- package/dist/conversations/ConversationsUserRoot.js.map +1 -1
- package/dist/conversations/ConversationsUserRoutes.js.map +1 -1
- package/dist/conversations/components/InboxLayout.js +89 -85
- package/dist/conversations/components/InboxLayout.js.map +1 -1
- package/dist/conversations/components/NewConversationDialog.js +285 -0
- package/dist/conversations/components/NewConversationDialog.js.map +1 -0
- package/dist/conversations/pages/ConversationsLandingPage.js +89 -69
- package/dist/conversations/pages/ConversationsLandingPage.js.map +1 -1
- package/dist/conversations/pages/NewConversationPage.js +19 -280
- package/dist/conversations/pages/NewConversationPage.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { useTr as e } from "../utils/i18n.js";
|
|
2
|
+
import { cn as t } from "../utils/cn.js";
|
|
3
|
+
import { useCreateConversation as n } from "../hooks/useConversationQueries.js";
|
|
4
|
+
import { nativeImpact as r, nativeNotify as i } from "../../utils/nativeBridge.js";
|
|
5
|
+
import { useCallback as a, useMemo as o, useState as s } from "react";
|
|
6
|
+
import { Globe as c, Hash as l, Lock as u, Megaphone as d, MessageCircle as ee, Shield as f, Users as p, X as m } from "lucide-react";
|
|
7
|
+
import { useEventBus as h } from "@burdenoff/fe-libs/shared/events";
|
|
8
|
+
import { jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
9
|
+
import { Dialog as v, DialogContent as y, DialogDescription as b, DialogFooter as x, DialogHeader as S, DialogTitle as C } from "@burdenoff/fe-libs/ui";
|
|
10
|
+
//#region src/conversations/components/NewConversationDialog.tsx
|
|
11
|
+
function te(e) {
|
|
12
|
+
if ("graphQLErrors" in e) {
|
|
13
|
+
let t = e.graphQLErrors;
|
|
14
|
+
if (t && t.length > 0 && t[0]?.message) return t[0].message;
|
|
15
|
+
}
|
|
16
|
+
return e.message;
|
|
17
|
+
}
|
|
18
|
+
function w({ open: w, onOpenChange: T, onCreated: E }) {
|
|
19
|
+
let { createConversation: D, loading: O, error: k } = n(), A = h(), j = e(), M = o(() => [
|
|
20
|
+
{
|
|
21
|
+
type: "DIRECT",
|
|
22
|
+
label: j("conversations.directMessage", "Direct Message"),
|
|
23
|
+
description: j("conversations.directMessageDescription", "One-on-one private conversation"),
|
|
24
|
+
icon: /* @__PURE__ */ g(ee, { size: 20 })
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: "GROUP",
|
|
28
|
+
label: j("conversations.groupChat", "Group Chat"),
|
|
29
|
+
description: j("conversations.groupChatDescription", "Invite multiple people to a group conversation"),
|
|
30
|
+
icon: /* @__PURE__ */ g(p, { size: 20 })
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
type: "CHANNEL",
|
|
34
|
+
label: j("conversations.channel", "Channel"),
|
|
35
|
+
description: j("conversations.channelDescription", "A topic-based channel that anyone can join"),
|
|
36
|
+
icon: /* @__PURE__ */ g(l, { size: 20 })
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: "ANNOUNCEMENT",
|
|
40
|
+
label: j("conversations.announcement", "Announcement"),
|
|
41
|
+
description: j("conversations.announcementDescription", "Broadcast messages to a large audience"),
|
|
42
|
+
icon: /* @__PURE__ */ g(d, { size: 20 })
|
|
43
|
+
}
|
|
44
|
+
], [j]), N = o(() => [
|
|
45
|
+
{
|
|
46
|
+
value: "PUBLIC",
|
|
47
|
+
label: j("conversations.public", "Public"),
|
|
48
|
+
description: j("conversations.publicDescription", "Anyone in the workspace can discover and join"),
|
|
49
|
+
icon: /* @__PURE__ */ g(c, { size: 16 })
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
value: "PRIVATE",
|
|
53
|
+
label: j("conversations.private", "Private"),
|
|
54
|
+
description: j("conversations.privateDescription", "Only invited members can access"),
|
|
55
|
+
icon: /* @__PURE__ */ g(u, { size: 16 })
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
value: "PROTECTED",
|
|
59
|
+
label: j("conversations.protected", "Protected"),
|
|
60
|
+
description: j("conversations.protectedDescription", "Discoverable but requires approval to join"),
|
|
61
|
+
icon: /* @__PURE__ */ g(f, { size: 16 })
|
|
62
|
+
}
|
|
63
|
+
], [j]), [P, F] = s("GROUP"), [I, L] = s(""), [R, z] = s(""), [B, V] = s("PRIVATE"), [H, U] = s(""), [W, G] = s([]), K = P === "DIRECT", q = !K, J = a(() => {
|
|
64
|
+
F("GROUP"), L(""), z(""), V("PRIVATE"), U(""), G([]);
|
|
65
|
+
}, []), Y = a(() => {
|
|
66
|
+
let e = H.trim();
|
|
67
|
+
!e || W.includes(e) || (G(K ? [e] : (t) => [...t, e]), U(""));
|
|
68
|
+
}, [
|
|
69
|
+
H,
|
|
70
|
+
W,
|
|
71
|
+
K
|
|
72
|
+
]), X = a((e) => {
|
|
73
|
+
G((t) => t.filter((t) => t !== e));
|
|
74
|
+
}, []), Z = P === "DIRECT" || P === "GROUP", Q = a((e) => {
|
|
75
|
+
e || J(), T(e);
|
|
76
|
+
}, [T, J]), ne = a(async () => {
|
|
77
|
+
if (!(Z && W.length === 0) && !(q && !I.trim())) {
|
|
78
|
+
r("light");
|
|
79
|
+
try {
|
|
80
|
+
let e = await D({
|
|
81
|
+
type: P,
|
|
82
|
+
title: q ? I.trim() : void 0,
|
|
83
|
+
description: R.trim() || void 0,
|
|
84
|
+
participantIds: W,
|
|
85
|
+
visibility: B
|
|
86
|
+
});
|
|
87
|
+
if (!e) {
|
|
88
|
+
i("error");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
i("success"), A.emit("conversations.conversation.created", {
|
|
92
|
+
conversationId: e.id,
|
|
93
|
+
conversationType: P,
|
|
94
|
+
source: "new-conversation-dialog"
|
|
95
|
+
}), J(), T(!1), E?.(e.id);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
throw i("error"), e;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}, [
|
|
101
|
+
A,
|
|
102
|
+
Z,
|
|
103
|
+
W,
|
|
104
|
+
q,
|
|
105
|
+
I,
|
|
106
|
+
D,
|
|
107
|
+
P,
|
|
108
|
+
R,
|
|
109
|
+
B,
|
|
110
|
+
J,
|
|
111
|
+
T,
|
|
112
|
+
E
|
|
113
|
+
]), $ = (!Z || W.length > 0) && (!q || I.trim().length > 0);
|
|
114
|
+
return /* @__PURE__ */ g(v, {
|
|
115
|
+
open: w,
|
|
116
|
+
onOpenChange: Q,
|
|
117
|
+
children: /* @__PURE__ */ _(y, {
|
|
118
|
+
className: "sm:max-w-[640px] max-h-[90vh] overflow-y-auto",
|
|
119
|
+
"data-tour": "new-conversation-dialog",
|
|
120
|
+
children: [
|
|
121
|
+
/* @__PURE__ */ _(S, { children: [/* @__PURE__ */ g(C, { children: j("conversations.newConversation", "New Conversation") }), /* @__PURE__ */ g(b, { children: j("conversations.newConversationSubtitle", "Start a new conversation with your team") })] }),
|
|
122
|
+
/* @__PURE__ */ _("div", {
|
|
123
|
+
className: "space-y-6",
|
|
124
|
+
children: [
|
|
125
|
+
/* @__PURE__ */ _("section", {
|
|
126
|
+
"data-tour": "conversation-type-selector",
|
|
127
|
+
className: "space-y-3",
|
|
128
|
+
children: [/* @__PURE__ */ g("h3", {
|
|
129
|
+
className: "text-sm font-semibold text-foreground",
|
|
130
|
+
children: j("conversations.conversationType", "Conversation Type")
|
|
131
|
+
}), /* @__PURE__ */ g("div", {
|
|
132
|
+
className: "grid grid-cols-1 sm:grid-cols-2 gap-3",
|
|
133
|
+
children: M.map((e) => /* @__PURE__ */ _("button", {
|
|
134
|
+
type: "button",
|
|
135
|
+
onClick: () => {
|
|
136
|
+
F(e.type), e.type === "DIRECT" && G((e) => e.slice(0, 1));
|
|
137
|
+
},
|
|
138
|
+
className: t("flex items-start gap-3 p-4 rounded-xl border text-left transition-all duration-150", P === e.type ? "border-primary bg-primary/5 ring-1 ring-primary/20" : "border-border hover:border-muted-foreground/30 hover:bg-muted/30"),
|
|
139
|
+
children: [/* @__PURE__ */ g("div", {
|
|
140
|
+
className: t("p-2 rounded-lg", P === e.type ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground"),
|
|
141
|
+
children: e.icon
|
|
142
|
+
}), /* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("p", {
|
|
143
|
+
className: "text-sm font-medium text-foreground",
|
|
144
|
+
children: e.label
|
|
145
|
+
}), /* @__PURE__ */ g("p", {
|
|
146
|
+
className: "text-xs text-muted-foreground mt-0.5",
|
|
147
|
+
children: e.description
|
|
148
|
+
})] })]
|
|
149
|
+
}, e.type))
|
|
150
|
+
})]
|
|
151
|
+
}),
|
|
152
|
+
!K && /* @__PURE__ */ _("section", {
|
|
153
|
+
"data-tour": "conversation-visibility-selector",
|
|
154
|
+
className: "space-y-3",
|
|
155
|
+
children: [
|
|
156
|
+
/* @__PURE__ */ g("h3", {
|
|
157
|
+
className: "text-sm font-semibold text-foreground",
|
|
158
|
+
children: j("conversations.visibility", "Visibility")
|
|
159
|
+
}),
|
|
160
|
+
/* @__PURE__ */ g("div", {
|
|
161
|
+
className: "flex gap-2 flex-wrap",
|
|
162
|
+
children: N.map((e) => /* @__PURE__ */ _("button", {
|
|
163
|
+
type: "button",
|
|
164
|
+
onClick: () => V(e.value),
|
|
165
|
+
className: t("flex items-center gap-2 px-4 py-2.5 rounded-lg border text-sm transition-all", B === e.value ? "border-primary bg-primary/5 text-primary font-medium" : "border-border text-muted-foreground hover:text-foreground hover:bg-muted/30"),
|
|
166
|
+
children: [e.icon, e.label]
|
|
167
|
+
}, e.value))
|
|
168
|
+
}),
|
|
169
|
+
/* @__PURE__ */ g("p", {
|
|
170
|
+
className: "text-xs text-muted-foreground",
|
|
171
|
+
children: N.find((e) => e.value === B)?.description
|
|
172
|
+
})
|
|
173
|
+
]
|
|
174
|
+
}),
|
|
175
|
+
q && /* @__PURE__ */ _("section", {
|
|
176
|
+
"data-tour": "conversation-create-details",
|
|
177
|
+
className: "space-y-3",
|
|
178
|
+
children: [/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ _("label", {
|
|
179
|
+
htmlFor: "conv-title",
|
|
180
|
+
className: "text-sm font-semibold text-foreground",
|
|
181
|
+
children: [
|
|
182
|
+
j("conversations.name", "Name"),
|
|
183
|
+
" ",
|
|
184
|
+
/* @__PURE__ */ g("span", {
|
|
185
|
+
className: "text-destructive",
|
|
186
|
+
children: "*"
|
|
187
|
+
})
|
|
188
|
+
]
|
|
189
|
+
}), /* @__PURE__ */ g("input", {
|
|
190
|
+
id: "conv-title",
|
|
191
|
+
type: "text",
|
|
192
|
+
value: I,
|
|
193
|
+
onChange: (e) => L(e.target.value),
|
|
194
|
+
placeholder: P === "CHANNEL" ? j("conversations.channelNamePlaceholder", "e.g., engineering-general") : j("conversations.conversationNamePlaceholder", "e.g., Product Launch Q1"),
|
|
195
|
+
maxLength: 200,
|
|
196
|
+
className: "mt-1.5 w-full px-3 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
197
|
+
})] }), /* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("label", {
|
|
198
|
+
htmlFor: "conv-desc",
|
|
199
|
+
className: "text-sm font-semibold text-foreground",
|
|
200
|
+
children: j("conversations.description", "Description")
|
|
201
|
+
}), /* @__PURE__ */ g("textarea", {
|
|
202
|
+
id: "conv-desc",
|
|
203
|
+
value: R,
|
|
204
|
+
onChange: (e) => z(e.target.value),
|
|
205
|
+
placeholder: j("conversations.descriptionPlaceholder", "What is this conversation about?"),
|
|
206
|
+
rows: 2,
|
|
207
|
+
maxLength: 1e3,
|
|
208
|
+
className: "mt-1.5 w-full px-3 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring resize-none"
|
|
209
|
+
})] })]
|
|
210
|
+
}),
|
|
211
|
+
/* @__PURE__ */ _("section", {
|
|
212
|
+
"data-tour": "conversation-participants",
|
|
213
|
+
className: "space-y-3",
|
|
214
|
+
children: [
|
|
215
|
+
/* @__PURE__ */ _("h3", {
|
|
216
|
+
className: "text-sm font-semibold text-foreground",
|
|
217
|
+
children: [K ? j("conversations.participantId", "Participant ID") : j("conversations.participantIds", "Participant IDs"), !Z && /* @__PURE__ */ _("span", {
|
|
218
|
+
className: "ml-1.5 text-xs font-normal text-muted-foreground",
|
|
219
|
+
children: [
|
|
220
|
+
"(",
|
|
221
|
+
j("conversations.optional", "optional"),
|
|
222
|
+
")"
|
|
223
|
+
]
|
|
224
|
+
})]
|
|
225
|
+
}),
|
|
226
|
+
W.length > 0 && /* @__PURE__ */ g("div", {
|
|
227
|
+
className: "flex flex-wrap gap-2",
|
|
228
|
+
children: W.map((e) => /* @__PURE__ */ _("span", {
|
|
229
|
+
className: "inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-primary/10 text-primary text-xs font-medium",
|
|
230
|
+
children: [e, /* @__PURE__ */ g("button", {
|
|
231
|
+
type: "button",
|
|
232
|
+
"aria-label": j("conversations.removeParticipant", "Remove participant"),
|
|
233
|
+
onClick: () => X(e),
|
|
234
|
+
className: "p-0.5 rounded-full hover:bg-primary/20",
|
|
235
|
+
children: /* @__PURE__ */ g(m, { size: 12 })
|
|
236
|
+
})]
|
|
237
|
+
}, e))
|
|
238
|
+
}),
|
|
239
|
+
/* @__PURE__ */ _("div", {
|
|
240
|
+
className: "flex items-center gap-2",
|
|
241
|
+
children: [/* @__PURE__ */ g("input", {
|
|
242
|
+
type: "text",
|
|
243
|
+
"aria-label": j("conversations.participantInputLabel", "Add participant"),
|
|
244
|
+
value: H,
|
|
245
|
+
onChange: (e) => U(e.target.value),
|
|
246
|
+
onKeyDown: (e) => {
|
|
247
|
+
e.key === "Enter" && (e.preventDefault(), Y());
|
|
248
|
+
},
|
|
249
|
+
placeholder: j("conversations.participantInputPlaceholder", "Enter user ID and press Enter"),
|
|
250
|
+
className: "w-full px-3 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
251
|
+
}), /* @__PURE__ */ g("button", {
|
|
252
|
+
type: "button",
|
|
253
|
+
onClick: Y,
|
|
254
|
+
className: "px-3 py-2 text-sm rounded-lg border border-border hover:bg-muted/50",
|
|
255
|
+
children: j("conversations.add", "Add")
|
|
256
|
+
})]
|
|
257
|
+
})
|
|
258
|
+
]
|
|
259
|
+
}),
|
|
260
|
+
k && /* @__PURE__ */ g("div", {
|
|
261
|
+
className: "text-xs text-destructive rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2",
|
|
262
|
+
children: te(k)
|
|
263
|
+
})
|
|
264
|
+
]
|
|
265
|
+
}),
|
|
266
|
+
/* @__PURE__ */ _(x, { children: [/* @__PURE__ */ g("button", {
|
|
267
|
+
type: "button",
|
|
268
|
+
onClick: () => Q(!1),
|
|
269
|
+
className: "px-4 py-2 text-sm font-medium rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors",
|
|
270
|
+
children: j("conversations.cancel", "Cancel")
|
|
271
|
+
}), /* @__PURE__ */ g("button", {
|
|
272
|
+
type: "button",
|
|
273
|
+
onClick: () => void ne(),
|
|
274
|
+
disabled: !$ || O,
|
|
275
|
+
className: t("inline-flex items-center gap-2 px-5 py-2 text-sm font-medium rounded-lg transition-all", $ && !O ? "bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm" : "bg-muted text-muted-foreground cursor-not-allowed"),
|
|
276
|
+
children: O ? j("conversations.creating", "Creating...") : j("conversations.createConversation", "Create Conversation")
|
|
277
|
+
})] })
|
|
278
|
+
]
|
|
279
|
+
})
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
//#endregion
|
|
283
|
+
export { w as NewConversationDialog };
|
|
284
|
+
|
|
285
|
+
//# sourceMappingURL=NewConversationDialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NewConversationDialog.js","names":[],"sources":["../../../src/conversations/components/NewConversationDialog.tsx"],"sourcesContent":["import { useState, useCallback, useMemo } from \"react\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@burdenoff/fe-libs/ui\";\nimport {\n Globe,\n Hash,\n Lock,\n Megaphone,\n MessageCircle,\n Shield,\n Users,\n X,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport { useTr } from \"../utils/i18n\";\nimport { useCreateConversation } from \"../hooks/useConversationQueries\";\nimport type { ConversationType, ConversationVisibility } from \"../types\";\nimport { nativeImpact, nativeNotify } from \"../../utils/nativeBridge\";\n\n/** Extract the first meaningful GraphQL error message from an ApolloError. */\nfunction getErrorMessage(error: Error): string {\n if (\"graphQLErrors\" in error) {\n const gqlErrors = (error as { graphQLErrors?: Array<{ message?: string }> })\n .graphQLErrors;\n if (gqlErrors && gqlErrors.length > 0 && gqlErrors[0]?.message) {\n return gqlErrors[0].message;\n }\n }\n return error.message;\n}\n\ninterface ConversationTypeOption {\n type: ConversationType;\n label: string;\n description: string;\n icon: React.ReactNode;\n}\n\nexport interface NewConversationDialogProps {\n /** Whether the dialog is open. */\n open: boolean;\n /** Open-state change handler. Fires with `false` on close (overlay click,\n * Escape, X button, Cancel). */\n onOpenChange: (open: boolean) => void;\n /**\n * Called after a conversation is successfully created. Receives the new\n * conversation id so the parent can navigate to the detail route.\n * The dialog also closes itself (via `onOpenChange(false)`) before this\n * fires, so the parent does not need to close the dialog manually.\n */\n onCreated?: (conversationId: string) => void;\n}\n\n/**\n * NewConversationDialog — modal form for creating a new conversation.\n *\n * Renders inside the canonical `<Dialog>` primitive from\n * `@burdenoff/fe-libs/ui` so it inherits theme tokens, focus management,\n * and overlay behaviour from the shell.\n *\n * Form state is owned locally; on successful submit the dialog clears\n * state, closes itself, and invokes `onCreated` with the new\n * conversation id.\n */\nexport function NewConversationDialog({\n open,\n onOpenChange,\n onCreated,\n}: NewConversationDialogProps) {\n const { createConversation, loading, error } = useCreateConversation();\n const bus = useEventBus();\n const tr = useTr();\n\n const CONVERSATION_TYPES: ConversationTypeOption[] = useMemo(\n () => [\n {\n type: \"DIRECT\" as ConversationType,\n label: tr(\"conversations.directMessage\", \"Direct Message\"),\n description: tr(\n \"conversations.directMessageDescription\",\n \"One-on-one private conversation\",\n ),\n icon: <MessageCircle size={20} />,\n },\n {\n type: \"GROUP\" as ConversationType,\n label: tr(\"conversations.groupChat\", \"Group Chat\"),\n description: tr(\n \"conversations.groupChatDescription\",\n \"Invite multiple people to a group conversation\",\n ),\n icon: <Users size={20} />,\n },\n {\n type: \"CHANNEL\" as ConversationType,\n label: tr(\"conversations.channel\", \"Channel\"),\n description: tr(\n \"conversations.channelDescription\",\n \"A topic-based channel that anyone can join\",\n ),\n icon: <Hash size={20} />,\n },\n {\n type: \"ANNOUNCEMENT\" as ConversationType,\n label: tr(\"conversations.announcement\", \"Announcement\"),\n description: tr(\n \"conversations.announcementDescription\",\n \"Broadcast messages to a large audience\",\n ),\n icon: <Megaphone size={20} />,\n },\n ],\n [tr],\n );\n\n const VISIBILITY_OPTIONS: Array<{\n value: ConversationVisibility;\n label: string;\n description: string;\n icon: React.ReactNode;\n }> = useMemo(\n () => [\n {\n value: \"PUBLIC\" as ConversationVisibility,\n label: tr(\"conversations.public\", \"Public\"),\n description: tr(\n \"conversations.publicDescription\",\n \"Anyone in the workspace can discover and join\",\n ),\n icon: <Globe size={16} />,\n },\n {\n value: \"PRIVATE\" as ConversationVisibility,\n label: tr(\"conversations.private\", \"Private\"),\n description: tr(\n \"conversations.privateDescription\",\n \"Only invited members can access\",\n ),\n icon: <Lock size={16} />,\n },\n {\n value: \"PROTECTED\" as ConversationVisibility,\n label: tr(\"conversations.protected\", \"Protected\"),\n description: tr(\n \"conversations.protectedDescription\",\n \"Discoverable but requires approval to join\",\n ),\n icon: <Shield size={16} />,\n },\n ],\n [tr],\n );\n\n const [selectedType, setSelectedType] = useState<ConversationType>(\"GROUP\");\n const [title, setTitle] = useState(\"\");\n const [description, setDescription] = useState(\"\");\n const [visibility, setVisibility] =\n useState<ConversationVisibility>(\"PRIVATE\");\n const [participantInput, setParticipantInput] = useState(\"\");\n const [participantIds, setParticipantIds] = useState<string[]>([]);\n\n const isDirectMessage = selectedType === \"DIRECT\";\n const needsTitle = !isDirectMessage;\n\n const resetForm = useCallback(() => {\n setSelectedType(\"GROUP\");\n setTitle(\"\");\n setDescription(\"\");\n setVisibility(\"PRIVATE\");\n setParticipantInput(\"\");\n setParticipantIds([]);\n }, []);\n\n const addParticipant = useCallback(() => {\n const value = participantInput.trim();\n if (!value || participantIds.includes(value)) return;\n if (isDirectMessage) {\n setParticipantIds([value]);\n } else {\n setParticipantIds((prev) => [...prev, value]);\n }\n setParticipantInput(\"\");\n }, [participantInput, participantIds, isDirectMessage]);\n\n const removeParticipant = useCallback((participantId: string) => {\n setParticipantIds((prev) => prev.filter((id) => id !== participantId));\n }, []);\n\n const requiresParticipants =\n selectedType === \"DIRECT\" || selectedType === \"GROUP\";\n\n const handleOpenChange = useCallback(\n (nextOpen: boolean) => {\n if (!nextOpen) {\n resetForm();\n }\n onOpenChange(nextOpen);\n },\n [onOpenChange, resetForm],\n );\n\n const handleCreate = useCallback(async () => {\n if (requiresParticipants && participantIds.length === 0) return;\n if (needsTitle && !title.trim()) return;\n\n nativeImpact(\"light\");\n try {\n const conversation = await createConversation({\n type: selectedType,\n title: needsTitle ? title.trim() : undefined,\n description: description.trim() || undefined,\n participantIds,\n visibility,\n });\n\n if (!conversation) {\n nativeNotify(\"error\");\n return;\n }\n nativeNotify(\"success\");\n bus.emit(\"conversations.conversation.created\", {\n conversationId: conversation.id,\n conversationType: selectedType,\n source: \"new-conversation-dialog\",\n });\n resetForm();\n onOpenChange(false);\n onCreated?.(conversation.id);\n } catch (error) {\n nativeNotify(\"error\");\n throw error;\n }\n }, [\n bus,\n requiresParticipants,\n participantIds,\n needsTitle,\n title,\n createConversation,\n selectedType,\n description,\n visibility,\n resetForm,\n onOpenChange,\n onCreated,\n ]);\n\n const canCreate =\n (!requiresParticipants || participantIds.length > 0) &&\n (!needsTitle || title.trim().length > 0);\n\n return (\n <Dialog open={open} onOpenChange={handleOpenChange}>\n <DialogContent\n className=\"sm:max-w-[640px] max-h-[90vh] overflow-y-auto\"\n data-tour=\"new-conversation-dialog\"\n >\n <DialogHeader>\n <DialogTitle>\n {tr(\"conversations.newConversation\", \"New Conversation\")}\n </DialogTitle>\n <DialogDescription>\n {tr(\n \"conversations.newConversationSubtitle\",\n \"Start a new conversation with your team\",\n )}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-6\">\n <section data-tour=\"conversation-type-selector\" className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n {tr(\"conversations.conversationType\", \"Conversation Type\")}\n </h3>\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-3\">\n {CONVERSATION_TYPES.map((option) => (\n <button\n key={option.type}\n type=\"button\"\n onClick={() => {\n setSelectedType(option.type);\n if (option.type === \"DIRECT\") {\n setParticipantIds((prev) => prev.slice(0, 1));\n }\n }}\n className={cn(\n \"flex items-start gap-3 p-4 rounded-xl border text-left transition-all duration-150\",\n selectedType === option.type\n ? \"border-primary bg-primary/5 ring-1 ring-primary/20\"\n : \"border-border hover:border-muted-foreground/30 hover:bg-muted/30\",\n )}\n >\n <div\n className={cn(\n \"p-2 rounded-lg\",\n selectedType === option.type\n ? \"bg-primary/10 text-primary\"\n : \"bg-muted text-muted-foreground\",\n )}\n >\n {option.icon}\n </div>\n <div>\n <p className=\"text-sm font-medium text-foreground\">\n {option.label}\n </p>\n <p className=\"text-xs text-muted-foreground mt-0.5\">\n {option.description}\n </p>\n </div>\n </button>\n ))}\n </div>\n </section>\n\n {!isDirectMessage && (\n <section\n data-tour=\"conversation-visibility-selector\"\n className=\"space-y-3\"\n >\n <h3 className=\"text-sm font-semibold text-foreground\">\n {tr(\"conversations.visibility\", \"Visibility\")}\n </h3>\n <div className=\"flex gap-2 flex-wrap\">\n {VISIBILITY_OPTIONS.map((option) => (\n <button\n key={option.value}\n type=\"button\"\n onClick={() => setVisibility(option.value)}\n className={cn(\n \"flex items-center gap-2 px-4 py-2.5 rounded-lg border text-sm transition-all\",\n visibility === option.value\n ? \"border-primary bg-primary/5 text-primary font-medium\"\n : \"border-border text-muted-foreground hover:text-foreground hover:bg-muted/30\",\n )}\n >\n {option.icon}\n {option.label}\n </button>\n ))}\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {\n VISIBILITY_OPTIONS.find(\n (option) => option.value === visibility,\n )?.description\n }\n </p>\n </section>\n )}\n\n {needsTitle && (\n <section\n data-tour=\"conversation-create-details\"\n className=\"space-y-3\"\n >\n <div>\n <label\n htmlFor=\"conv-title\"\n className=\"text-sm font-semibold text-foreground\"\n >\n {tr(\"conversations.name\", \"Name\")}{\" \"}\n <span className=\"text-destructive\">*</span>\n </label>\n <input\n id=\"conv-title\"\n type=\"text\"\n value={title}\n onChange={(event) => setTitle(event.target.value)}\n placeholder={\n selectedType === \"CHANNEL\"\n ? tr(\n \"conversations.channelNamePlaceholder\",\n \"e.g., engineering-general\",\n )\n : tr(\n \"conversations.conversationNamePlaceholder\",\n \"e.g., Product Launch Q1\",\n )\n }\n maxLength={200}\n className=\"mt-1.5 w-full px-3 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring\"\n />\n </div>\n <div>\n <label\n htmlFor=\"conv-desc\"\n className=\"text-sm font-semibold text-foreground\"\n >\n {tr(\"conversations.description\", \"Description\")}\n </label>\n <textarea\n id=\"conv-desc\"\n value={description}\n onChange={(event) => setDescription(event.target.value)}\n placeholder={tr(\n \"conversations.descriptionPlaceholder\",\n \"What is this conversation about?\",\n )}\n rows={2}\n maxLength={1000}\n className=\"mt-1.5 w-full px-3 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring resize-none\"\n />\n </div>\n </section>\n )}\n\n <section data-tour=\"conversation-participants\" className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-foreground\">\n {isDirectMessage\n ? tr(\"conversations.participantId\", \"Participant ID\")\n : tr(\"conversations.participantIds\", \"Participant IDs\")}\n {!requiresParticipants && (\n <span className=\"ml-1.5 text-xs font-normal text-muted-foreground\">\n ({tr(\"conversations.optional\", \"optional\")})\n </span>\n )}\n </h3>\n\n {participantIds.length > 0 && (\n <div className=\"flex flex-wrap gap-2\">\n {participantIds.map((participantId) => (\n <span\n key={participantId}\n className=\"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-primary/10 text-primary text-xs font-medium\"\n >\n {participantId}\n <button\n type=\"button\"\n aria-label={tr(\n \"conversations.removeParticipant\",\n \"Remove participant\",\n )}\n onClick={() => removeParticipant(participantId)}\n className=\"p-0.5 rounded-full hover:bg-primary/20\"\n >\n <X size={12} />\n </button>\n </span>\n ))}\n </div>\n )}\n\n <div className=\"flex items-center gap-2\">\n <input\n type=\"text\"\n aria-label={tr(\n \"conversations.participantInputLabel\",\n \"Add participant\",\n )}\n value={participantInput}\n onChange={(event) => setParticipantInput(event.target.value)}\n onKeyDown={(event) => {\n if (event.key !== \"Enter\") return;\n event.preventDefault();\n addParticipant();\n }}\n placeholder={tr(\n \"conversations.participantInputPlaceholder\",\n \"Enter user ID and press Enter\",\n )}\n className=\"w-full px-3 py-2 text-sm rounded-lg border border-border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring\"\n />\n <button\n type=\"button\"\n onClick={addParticipant}\n className=\"px-3 py-2 text-sm rounded-lg border border-border hover:bg-muted/50\"\n >\n {tr(\"conversations.add\", \"Add\")}\n </button>\n </div>\n </section>\n\n {error && (\n <div className=\"text-xs text-destructive rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2\">\n {getErrorMessage(error)}\n </div>\n )}\n </div>\n\n <DialogFooter>\n <button\n type=\"button\"\n onClick={() => handleOpenChange(false)}\n className=\"px-4 py-2 text-sm font-medium rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors\"\n >\n {tr(\"conversations.cancel\", \"Cancel\")}\n </button>\n <button\n type=\"button\"\n onClick={() => void handleCreate()}\n disabled={!canCreate || loading}\n className={cn(\n \"inline-flex items-center gap-2 px-5 py-2 text-sm font-medium rounded-lg transition-all\",\n canCreate && !loading\n ? \"bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm\"\n : \"bg-muted text-muted-foreground cursor-not-allowed\",\n )}\n >\n {loading\n ? tr(\"conversations.creating\", \"Creating...\")\n : tr(\"conversations.createConversation\", \"Create Conversation\")}\n </button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,109 +1,129 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
1
|
+
import { useConversations as e } from "../providers/ConversationsProvider.js";
|
|
2
|
+
import { joinPath as t } from "../utils/navigation.js";
|
|
3
|
+
import { useTr as n } from "../utils/i18n.js";
|
|
4
|
+
import { useConversationsStore as r } from "../store/conversationsStore.js";
|
|
5
|
+
import { PermissionGuard as i } from "../components/PermissionGuard.js";
|
|
6
|
+
import { CONVERSATIONS_PERMISSIONS as a } from "../constants/permissions.js";
|
|
7
|
+
import { ConversationListPage as o } from "./ConversationListPage.js";
|
|
8
|
+
import { InboxLayout as s } from "../components/InboxLayout.js";
|
|
9
|
+
import { NewConversationDialog as c } from "../components/NewConversationDialog.js";
|
|
10
|
+
import { useUserFeatureFlags as l } from "../hooks/useUserFeatureFlags.js";
|
|
11
|
+
import { Suspense as u, lazy as d, useCallback as f, useEffect as p, useMemo as m } from "react";
|
|
12
|
+
import { Filter as h, Loader2 as g, Pin as _ } from "lucide-react";
|
|
13
|
+
import { useNavigate as v, useParams as y, useSearchParams as b } from "react-router-dom";
|
|
14
|
+
import { jsx as x, jsxs as S } from "react/jsx-runtime";
|
|
15
|
+
import { PageContainer as C } from "@burdenoff/fe-libs/ui";
|
|
13
16
|
//#region src/conversations/pages/ConversationsLandingPage.tsx
|
|
14
|
-
var
|
|
17
|
+
var w = d(() => import("./ConversationDetailPage.js").then((e) => ({ default: e.ConversationDetailPage }))), T = "all", E = {
|
|
15
18
|
all: void 0,
|
|
16
19
|
direct: "direct",
|
|
17
20
|
groups: "group",
|
|
18
21
|
channels: "channel",
|
|
19
22
|
archived: "archived"
|
|
20
23
|
};
|
|
21
|
-
function
|
|
22
|
-
let [
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
function D() {
|
|
25
|
+
let [d, D] = b(), { conversationId: O } = y(), k = v(), { basePath: A } = e(), j = n(), { archivedEnabled: M } = l(), { searchQuery: N, setSearchQuery: P, conversationFilter: F, updateConversationFilter: I } = r(), L = d.get("new") === "true", R = f(() => {
|
|
26
|
+
D((e) => {
|
|
27
|
+
let t = new URLSearchParams(e);
|
|
28
|
+
return t.delete("new"), t;
|
|
29
|
+
}, { replace: !0 });
|
|
30
|
+
}, [D]), z = f((e) => {
|
|
31
|
+
e || R();
|
|
32
|
+
}, [R]), B = f((e) => {
|
|
33
|
+
k(t(A || "/conversations", e), { replace: !0 });
|
|
34
|
+
}, [A, k]), V = d.get("tab") ?? T, H = V === "archived" && !M ? T : V;
|
|
35
|
+
p(() => {
|
|
36
|
+
!M && V === "archived" && D((e) => {
|
|
25
37
|
let t = new URLSearchParams(e);
|
|
26
38
|
return t.delete("tab"), t;
|
|
27
39
|
}, { replace: !0 });
|
|
28
40
|
}, [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
M,
|
|
42
|
+
V,
|
|
43
|
+
D
|
|
32
44
|
]);
|
|
33
|
-
let
|
|
34
|
-
|
|
45
|
+
let U = f((e) => {
|
|
46
|
+
D((t) => {
|
|
35
47
|
let n = new URLSearchParams(t);
|
|
36
|
-
return e ===
|
|
48
|
+
return e === T ? n.delete("tab") : n.set("tab", e), n;
|
|
37
49
|
}, { replace: !0 });
|
|
38
|
-
}, [
|
|
39
|
-
if (
|
|
40
|
-
let e =
|
|
50
|
+
}, [D]), W = m(() => {
|
|
51
|
+
if (H === "archived") return [];
|
|
52
|
+
let e = F.hasUnread === !0, t = F.isPinned === !0;
|
|
41
53
|
return [{
|
|
42
54
|
id: "unread",
|
|
43
|
-
label:
|
|
55
|
+
label: j("conversations.unread", "Unread"),
|
|
44
56
|
active: e,
|
|
45
|
-
onToggle: () =>
|
|
46
|
-
icon: /* @__PURE__ */
|
|
57
|
+
onToggle: () => I({ hasUnread: e ? void 0 : !0 }),
|
|
58
|
+
icon: /* @__PURE__ */ x(h, {
|
|
47
59
|
size: 11,
|
|
48
60
|
"aria-hidden": "true"
|
|
49
61
|
})
|
|
50
62
|
}, {
|
|
51
63
|
id: "pinned",
|
|
52
|
-
label:
|
|
64
|
+
label: j("conversations.pinned", "Pinned"),
|
|
53
65
|
active: t,
|
|
54
|
-
onToggle: () =>
|
|
55
|
-
icon: /* @__PURE__ */ _
|
|
66
|
+
onToggle: () => I({ isPinned: t ? void 0 : !0 }),
|
|
67
|
+
icon: /* @__PURE__ */ x(_, {
|
|
56
68
|
size: 11,
|
|
57
69
|
"aria-hidden": "true"
|
|
58
70
|
})
|
|
59
71
|
}];
|
|
60
72
|
}, [
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
]),
|
|
67
|
-
return /* @__PURE__ */
|
|
68
|
-
permission:
|
|
73
|
+
H,
|
|
74
|
+
F.hasUnread,
|
|
75
|
+
F.isPinned,
|
|
76
|
+
j,
|
|
77
|
+
I
|
|
78
|
+
]), G = E[H];
|
|
79
|
+
return /* @__PURE__ */ x(i, {
|
|
80
|
+
permission: a.CONVERSATION_READ,
|
|
69
81
|
allowUnavailablePermissions: !0,
|
|
70
|
-
children: /* @__PURE__ */
|
|
82
|
+
children: /* @__PURE__ */ S(C, {
|
|
71
83
|
"data-tour": "conversations-landing",
|
|
72
84
|
padded: !1,
|
|
73
85
|
className: "h-full min-h-0",
|
|
74
|
-
children: [
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
activeTab: M,
|
|
80
|
-
onTabChange: N,
|
|
81
|
-
archivedEnabled: E,
|
|
82
|
-
searchQuery: D,
|
|
83
|
-
onSearchChange: O,
|
|
84
|
-
filterChips: P,
|
|
85
|
-
listSlot: /* @__PURE__ */ _(i, {
|
|
86
|
-
embedded: !0,
|
|
87
|
-
forcedFilterParam: F
|
|
86
|
+
children: [
|
|
87
|
+
/* @__PURE__ */ x("h1", {
|
|
88
|
+
id: "conversations-page-heading",
|
|
89
|
+
className: "sr-only",
|
|
90
|
+
children: j("conversations.title", "Conversations")
|
|
88
91
|
}),
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
/* @__PURE__ */ x(s, {
|
|
93
|
+
activeTab: H,
|
|
94
|
+
onTabChange: U,
|
|
95
|
+
archivedEnabled: M,
|
|
96
|
+
searchQuery: N,
|
|
97
|
+
onSearchChange: P,
|
|
98
|
+
filterChips: W,
|
|
99
|
+
listSlot: /* @__PURE__ */ x(o, {
|
|
100
|
+
embedded: !0,
|
|
101
|
+
forcedFilterParam: G
|
|
99
102
|
}),
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
detailSlot: O ? /* @__PURE__ */ x(u, {
|
|
104
|
+
fallback: /* @__PURE__ */ S("div", {
|
|
105
|
+
role: "status",
|
|
106
|
+
"aria-live": "polite",
|
|
107
|
+
className: "flex h-full items-center justify-center gap-2 text-sm text-text-secondary",
|
|
108
|
+
children: [/* @__PURE__ */ x(g, {
|
|
109
|
+
size: 16,
|
|
110
|
+
className: "animate-spin",
|
|
111
|
+
"aria-hidden": "true"
|
|
112
|
+
}), j("conversations.detailLoading", "Loading conversation…")]
|
|
113
|
+
}),
|
|
114
|
+
children: /* @__PURE__ */ x(w, {})
|
|
115
|
+
}) : void 0
|
|
116
|
+
}),
|
|
117
|
+
L && /* @__PURE__ */ x(c, {
|
|
118
|
+
open: !0,
|
|
119
|
+
onOpenChange: z,
|
|
120
|
+
onCreated: B
|
|
121
|
+
})
|
|
122
|
+
]
|
|
103
123
|
})
|
|
104
124
|
});
|
|
105
125
|
}
|
|
106
126
|
//#endregion
|
|
107
|
-
export {
|
|
127
|
+
export { D as ConversationsLandingPage };
|
|
108
128
|
|
|
109
129
|
//# sourceMappingURL=ConversationsLandingPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConversationsLandingPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationsLandingPage.tsx"],"sourcesContent":["import { Suspense, useCallback, useEffect, useMemo, lazy } from \"react\";\nimport { useParams, useSearchParams } from \"react-router-dom\";\nimport { PageContainer } from \"@burdenoff/fe-libs/ui\";\nimport { Pin, Filter, Loader2 } from \"lucide-react\";\nimport { ConversationListPage } from \"./ConversationListPage\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport {\n InboxLayout,\n type FilterChipDescriptor,\n type InboxTabId,\n} from \"../components/InboxLayout\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { useUserFeatureFlags } from \"../hooks/useUserFeatureFlags\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport { useTr } from \"../utils/i18n\";\n\n// Lazy-load the detail page so the inbox bundle stays small for users who\n// never open a conversation in this session. The detail subtree is the\n// heaviest by far (~19kB gz including MessageBubble).\nconst ConversationDetailPage = lazy(() =>\n import(\"./ConversationDetailPage\").then((m) => ({\n default: m.ConversationDetailPage,\n })),\n);\n\nconst DEFAULT_TAB_ID: InboxTabId = \"all\";\n\n// Maps the inbox tab id to the `forcedFilterParam` prop accepted by\n// ConversationListPage. The \"all\" tab passes undefined so the list shows\n// every conversation type. The archived tab triggers the BE state filter\n// (states: [ARCHIVED]) handled inside ConversationListPage.\nconst TAB_TO_LIST_FILTER: Record<\n InboxTabId,\n \"channel\" | \"direct\" | \"group\" | \"archived\" | undefined\n> = {\n all: undefined,\n direct: \"direct\",\n groups: \"group\",\n channels: \"channel\",\n archived: \"archived\",\n};\n\n/**\n * Landing page for `/conversations`.\n *\n * Renders the canonical inbox: two-pane shell with the conversation list on\n * the left and an empty detail panel on the right. The detail panel is\n * filled in by the router on `/conversations/:conversationId`; this page\n * itself only renders the chrome and the list.\n *\n * Tabs (All / Direct / Groups / Channels / Archived) are stored in the\n * `?tab=` URL search param so refreshing the page keeps the user on the\n * same view.\n */\nexport function ConversationsLandingPage() {\n const [params, setParams] = useSearchParams();\n const { conversationId } = useParams<{ conversationId?: string }>();\n const tr = useTr();\n const { archivedEnabled } = useUserFeatureFlags();\n const {\n searchQuery,\n setSearchQuery,\n conversationFilter,\n updateConversationFilter,\n } = useConversationsStore();\n\n const requestedTab = (params.get(\"tab\") ?? DEFAULT_TAB_ID) as InboxTabId;\n // Strip the archived tab when the feature flag is off — the side effect\n // below also lazily cleans the URL so a deep link doesn't leave the user\n // staring at an unknown tab.\n const activeTab: InboxTabId =\n requestedTab === \"archived\" && !archivedEnabled\n ? DEFAULT_TAB_ID\n : requestedTab;\n\n useEffect(() => {\n if (!archivedEnabled && requestedTab === \"archived\") {\n // Updater form reads the freshest params at flush time to preserve\n // concurrent query-param changes elsewhere in the tree.\n setParams(\n (prev) => {\n const next = new URLSearchParams(prev);\n next.delete(\"tab\");\n return next;\n },\n { replace: true },\n );\n }\n }, [archivedEnabled, requestedTab, setParams]);\n\n const handleTabChange = useCallback(\n (id: InboxTabId) => {\n setParams(\n (prev) => {\n const next = new URLSearchParams(prev);\n if (id === DEFAULT_TAB_ID) {\n next.delete(\"tab\");\n } else {\n next.set(\"tab\", id);\n }\n return next;\n },\n { replace: true },\n );\n },\n [setParams],\n );\n\n // Filter chips are only meaningful on tabs where they can hide items. On\n // the Archived tab the only sane secondary toggle is \"Pinned\" (unread\n // doesn't make sense for archived conversations); on the inbox tabs we\n // expose both unread + pinned.\n const filterChips = useMemo<FilterChipDescriptor[]>(() => {\n if (activeTab === \"archived\") return [];\n const unreadActive = conversationFilter.hasUnread === true;\n const pinnedActive = conversationFilter.isPinned === true;\n return [\n {\n id: \"unread\",\n label: tr(\"conversations.unread\", \"Unread\"),\n active: unreadActive,\n onToggle: () =>\n updateConversationFilter({\n hasUnread: unreadActive ? undefined : true,\n }),\n icon: <Filter size={11} aria-hidden=\"true\" />,\n },\n {\n id: \"pinned\",\n label: tr(\"conversations.pinned\", \"Pinned\"),\n active: pinnedActive,\n onToggle: () =>\n updateConversationFilter({\n isPinned: pinnedActive ? undefined : true,\n }),\n icon: <Pin size={11} aria-hidden=\"true\" />,\n },\n ];\n }, [\n activeTab,\n conversationFilter.hasUnread,\n conversationFilter.isPinned,\n tr,\n updateConversationFilter,\n ]);\n\n const forcedFilter = TAB_TO_LIST_FILTER[activeTab];\n\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <PageContainer\n data-tour=\"conversations-landing\"\n padded={false}\n className=\"h-full min-h-0\"\n >\n <h1 id=\"conversations-page-heading\" className=\"sr-only\">\n {tr(\"conversations.title\", \"Conversations\")}\n </h1>\n <InboxLayout\n activeTab={activeTab}\n onTabChange={handleTabChange}\n archivedEnabled={archivedEnabled}\n searchQuery={searchQuery}\n onSearchChange={setSearchQuery}\n filterChips={filterChips}\n listSlot={\n <ConversationListPage embedded forcedFilterParam={forcedFilter} />\n }\n detailSlot={\n conversationId ? (\n <Suspense\n fallback={\n <div\n role=\"status\"\n aria-live=\"polite\"\n className=\"flex h-full items-center justify-center gap-2 text-sm text-text-secondary\"\n >\n <Loader2\n size={16}\n className=\"animate-spin\"\n aria-hidden=\"true\"\n />\n {tr(\"conversations.detailLoading\", \"Loading conversation…\")}\n </div>\n }\n >\n <ConversationDetailPage />\n </Suspense>\n ) : undefined\n }\n />\n </PageContainer>\n </PermissionGuard>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAmBA,IAAM,IAAyB,QAC7B,OAAO,+BAA4B,MAAM,OAAO,EAC9C,SAAS,EAAE,wBACZ,EAAE,CACJ,EAEK,IAA6B,OAM7B,IAGF;CACF,KAAK,KAAA;CACL,QAAQ;CACR,QAAQ;CACR,UAAU;CACV,UAAU;CACX;AAcD,SAAgB,IAA2B;CACzC,IAAM,CAAC,GAAQ,KAAa,GAAiB,EACvC,EAAE,sBAAmB,GAAwC,EAC7D,IAAK,GAAO,EACZ,EAAE,uBAAoB,GAAqB,EAC3C,EACJ,gBACA,mBACA,uBACA,gCACE,GAAuB,EAErB,IAAgB,EAAO,IAAI,MAAM,IAAI,GAIrC,IACJ,MAAiB,cAAc,CAAC,IAC5B,IACA;AAEN,SAAgB;AACd,EAAI,CAAC,KAAmB,MAAiB,cAGvC,GACG,MAAS;GACR,IAAM,IAAO,IAAI,gBAAgB,EAAK;AAEtC,UADA,EAAK,OAAO,MAAM,EACX;KAET,EAAE,SAAS,IAAM,CAClB;IAEF;EAAC;EAAiB;EAAc;EAAU,CAAC;CAE9C,IAAM,IAAkB,GACrB,MAAmB;AAClB,KACG,MAAS;GACR,IAAM,IAAO,IAAI,gBAAgB,EAAK;AAMtC,UALI,MAAO,IACT,EAAK,OAAO,MAAM,GAElB,EAAK,IAAI,OAAO,EAAG,EAEd;KAET,EAAE,SAAS,IAAM,CAClB;IAEH,CAAC,EAAU,CACZ,EAMK,IAAc,QAAsC;AACxD,MAAI,MAAc,WAAY,QAAO,EAAE;EACvC,IAAM,IAAe,EAAmB,cAAc,IAChD,IAAe,EAAmB,aAAa;AACrD,SAAO,CACL;GACE,IAAI;GACJ,OAAO,EAAG,wBAAwB,SAAS;GAC3C,QAAQ;GACR,gBACE,EAAyB,EACvB,WAAW,IAAe,KAAA,IAAY,IACvC,CAAC;GACJ,MAAM,kBAAC,GAAD;IAAQ,MAAM;IAAI,eAAY;IAAS,CAAA;GAC9C,EACD;GACE,IAAI;GACJ,OAAO,EAAG,wBAAwB,SAAS;GAC3C,QAAQ;GACR,gBACE,EAAyB,EACvB,UAAU,IAAe,KAAA,IAAY,IACtC,CAAC;GACJ,MAAM,kBAAC,GAAD;IAAK,MAAM;IAAI,eAAY;IAAS,CAAA;GAC3C,CACF;IACA;EACD;EACA,EAAmB;EACnB,EAAmB;EACnB;EACA;EACD,CAAC,EAEI,IAAe,EAAmB;AAExC,QACE,kBAAC,GAAD;EACE,YAAY,EAA0B;EACtC,6BAAA;YAEA,kBAAC,GAAD;GACE,aAAU;GACV,QAAQ;GACR,WAAU;aAHZ,CAKE,kBAAC,MAAD;IAAI,IAAG;IAA6B,WAAU;cAC3C,EAAG,uBAAuB,gBAAgB;IACxC,CAAA,EACL,kBAAC,GAAD;IACa;IACX,aAAa;IACI;IACJ;IACb,gBAAgB;IACH;IACb,UACE,kBAAC,GAAD;KAAsB,UAAA;KAAS,mBAAmB;KAAgB,CAAA;IAEpE,YACE,IACE,kBAAC,GAAD;KACE,UACE,kBAAC,OAAD;MACE,MAAK;MACL,aAAU;MACV,WAAU;gBAHZ,CAKE,kBAAC,GAAD;OACE,MAAM;OACN,WAAU;OACV,eAAY;OACZ,CAAA,EACD,EAAG,+BAA+B,wBAAwB,CACvD;;eAGR,kBAAC,GAAD,EAA0B,CAAA;KACjB,CAAA,GACT,KAAA;IAEN,CAAA,CACY;;EACA,CAAA"}
|
|
1
|
+
{"version":3,"file":"ConversationsLandingPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationsLandingPage.tsx"],"sourcesContent":["import { Suspense, useCallback, useEffect, useMemo, lazy } from \"react\";\nimport { useNavigate, useParams, useSearchParams } from \"react-router-dom\";\nimport { PageContainer } from \"@burdenoff/fe-libs/ui\";\nimport { Pin, Filter, Loader2 } from \"lucide-react\";\nimport { ConversationListPage } from \"./ConversationListPage\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport {\n InboxLayout,\n type FilterChipDescriptor,\n type InboxTabId,\n} from \"../components/InboxLayout\";\nimport { NewConversationDialog } from \"../components/NewConversationDialog\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { useUserFeatureFlags } from \"../hooks/useUserFeatureFlags\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport { useTr } from \"../utils/i18n\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { joinPath } from \"../utils/navigation\";\n\n// Lazy-load the detail page so the inbox bundle stays small for users who\n// never open a conversation in this session. The detail subtree is the\n// heaviest by far (~19kB gz including MessageBubble).\nconst ConversationDetailPage = lazy(() =>\n import(\"./ConversationDetailPage\").then((m) => ({\n default: m.ConversationDetailPage,\n })),\n);\n\nconst DEFAULT_TAB_ID: InboxTabId = \"all\";\n\n// Maps the inbox tab id to the `forcedFilterParam` prop accepted by\n// ConversationListPage. The \"all\" tab passes undefined so the list shows\n// every conversation type. The archived tab triggers the BE state filter\n// (states: [ARCHIVED]) handled inside ConversationListPage.\nconst TAB_TO_LIST_FILTER: Record<\n InboxTabId,\n \"channel\" | \"direct\" | \"group\" | \"archived\" | undefined\n> = {\n all: undefined,\n direct: \"direct\",\n groups: \"group\",\n channels: \"channel\",\n archived: \"archived\",\n};\n\n/**\n * Landing page for `/conversations`.\n *\n * Renders the canonical inbox: two-pane shell with the conversation list on\n * the left and an empty detail panel on the right. The detail panel is\n * filled in by the router on `/conversations/:conversationId`; this page\n * itself only renders the chrome and the list.\n *\n * Tabs (All / Direct / Groups / Channels / Archived) are stored in the\n * `?tab=` URL search param so refreshing the page keeps the user on the\n * same view.\n */\nexport function ConversationsLandingPage() {\n const [params, setParams] = useSearchParams();\n const { conversationId } = useParams<{ conversationId?: string }>();\n const navigate = useNavigate();\n const { basePath } = useConversations();\n const tr = useTr();\n const { archivedEnabled } = useUserFeatureFlags();\n const {\n searchQuery,\n setSearchQuery,\n conversationFilter,\n updateConversationFilter,\n } = useConversationsStore();\n\n // `?new=true` opens the create-conversation dialog in-place. The dialog\n // is dismissed by either (a) the user closing it, in which case we drop\n // the param via setParams (replace), or (b) a successful create, in\n // which case we strip the param and navigate to the new detail route.\n const isNewDialogOpen = params.get(\"new\") === \"true\";\n\n const closeNewDialog = useCallback(() => {\n setParams(\n (prev) => {\n const next = new URLSearchParams(prev);\n next.delete(\"new\");\n return next;\n },\n { replace: true },\n );\n }, [setParams]);\n\n const handleNewDialogOpenChange = useCallback(\n (open: boolean) => {\n if (!open) closeNewDialog();\n },\n [closeNewDialog],\n );\n\n const handleNewDialogCreated = useCallback(\n (newConversationId: string) => {\n // The dialog closes itself before invoking onCreated; we just need\n // to route to the new conversation detail page (which lives under\n // the conversations basePath) without the lingering ?new=true.\n navigate(joinPath(basePath || \"/conversations\", newConversationId), {\n replace: true,\n });\n },\n [basePath, navigate],\n );\n\n const requestedTab = (params.get(\"tab\") ?? DEFAULT_TAB_ID) as InboxTabId;\n // Strip the archived tab when the feature flag is off — the side effect\n // below also lazily cleans the URL so a deep link doesn't leave the user\n // staring at an unknown tab.\n const activeTab: InboxTabId =\n requestedTab === \"archived\" && !archivedEnabled\n ? DEFAULT_TAB_ID\n : requestedTab;\n\n useEffect(() => {\n if (!archivedEnabled && requestedTab === \"archived\") {\n // Updater form reads the freshest params at flush time to preserve\n // concurrent query-param changes elsewhere in the tree.\n setParams(\n (prev) => {\n const next = new URLSearchParams(prev);\n next.delete(\"tab\");\n return next;\n },\n { replace: true },\n );\n }\n }, [archivedEnabled, requestedTab, setParams]);\n\n const handleTabChange = useCallback(\n (id: InboxTabId) => {\n setParams(\n (prev) => {\n const next = new URLSearchParams(prev);\n if (id === DEFAULT_TAB_ID) {\n next.delete(\"tab\");\n } else {\n next.set(\"tab\", id);\n }\n return next;\n },\n { replace: true },\n );\n },\n [setParams],\n );\n\n // Filter chips are only meaningful on tabs where they can hide items. On\n // the Archived tab the only sane secondary toggle is \"Pinned\" (unread\n // doesn't make sense for archived conversations); on the inbox tabs we\n // expose both unread + pinned.\n const filterChips = useMemo<FilterChipDescriptor[]>(() => {\n if (activeTab === \"archived\") return [];\n const unreadActive = conversationFilter.hasUnread === true;\n const pinnedActive = conversationFilter.isPinned === true;\n return [\n {\n id: \"unread\",\n label: tr(\"conversations.unread\", \"Unread\"),\n active: unreadActive,\n onToggle: () =>\n updateConversationFilter({\n hasUnread: unreadActive ? undefined : true,\n }),\n icon: <Filter size={11} aria-hidden=\"true\" />,\n },\n {\n id: \"pinned\",\n label: tr(\"conversations.pinned\", \"Pinned\"),\n active: pinnedActive,\n onToggle: () =>\n updateConversationFilter({\n isPinned: pinnedActive ? undefined : true,\n }),\n icon: <Pin size={11} aria-hidden=\"true\" />,\n },\n ];\n }, [\n activeTab,\n conversationFilter.hasUnread,\n conversationFilter.isPinned,\n tr,\n updateConversationFilter,\n ]);\n\n const forcedFilter = TAB_TO_LIST_FILTER[activeTab];\n\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <PageContainer\n data-tour=\"conversations-landing\"\n padded={false}\n className=\"h-full min-h-0\"\n >\n <h1 id=\"conversations-page-heading\" className=\"sr-only\">\n {tr(\"conversations.title\", \"Conversations\")}\n </h1>\n <InboxLayout\n activeTab={activeTab}\n onTabChange={handleTabChange}\n archivedEnabled={archivedEnabled}\n searchQuery={searchQuery}\n onSearchChange={setSearchQuery}\n filterChips={filterChips}\n listSlot={\n <ConversationListPage embedded forcedFilterParam={forcedFilter} />\n }\n detailSlot={\n conversationId ? (\n <Suspense\n fallback={\n <div\n role=\"status\"\n aria-live=\"polite\"\n className=\"flex h-full items-center justify-center gap-2 text-sm text-text-secondary\"\n >\n <Loader2\n size={16}\n className=\"animate-spin\"\n aria-hidden=\"true\"\n />\n {tr(\"conversations.detailLoading\", \"Loading conversation…\")}\n </div>\n }\n >\n <ConversationDetailPage />\n </Suspense>\n ) : undefined\n }\n />\n {isNewDialogOpen && (\n <NewConversationDialog\n open\n onOpenChange={handleNewDialogOpenChange}\n onCreated={handleNewDialogCreated}\n />\n )}\n </PageContainer>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|