@burdenoff/microfe-conversations 2026.527.1 → 2026.528.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conversations/ConversationsUserRoutes.js +6 -6
- package/dist/conversations/ConversationsUserRoutes.js.map +1 -1
- package/dist/conversations/components/ConversationRow.js +176 -0
- package/dist/conversations/components/ConversationRow.js.map +1 -0
- package/dist/conversations/components/EmptyState.js +10 -21
- package/dist/conversations/components/EmptyState.js.map +1 -1
- package/dist/conversations/components/InboxLayout.js +185 -0
- package/dist/conversations/components/InboxLayout.js.map +1 -0
- package/dist/conversations/components/LazyPageBoundary.js.map +1 -1
- package/dist/conversations/components/PageLayout.js +16 -34
- package/dist/conversations/components/PageLayout.js.map +1 -1
- package/dist/conversations/components/PageSection.js +10 -19
- package/dist/conversations/components/PageSection.js.map +1 -1
- package/dist/conversations/components/lazyRetry.js.map +1 -1
- package/dist/conversations/hooks/useConversationQueries.js +48 -34
- package/dist/conversations/hooks/useConversationQueries.js.map +1 -1
- package/dist/conversations/hooks/useMessageQueries.js.map +1 -1
- package/dist/conversations/pages/ConversationAdminPages.js.map +1 -1
- package/dist/conversations/pages/ConversationListPage.js +187 -226
- package/dist/conversations/pages/ConversationListPage.js.map +1 -1
- package/dist/conversations/pages/ConversationsLandingPage.js +93 -103
- package/dist/conversations/pages/ConversationsLandingPage.js.map +1 -1
- package/dist/conversations/pages/ConversationsSettingsPage.js +201 -182
- package/dist/conversations/pages/ConversationsSettingsPage.js.map +1 -1
- package/dist/generated/wspace-operations.js +72 -56
- package/dist/generated/wspace-operations.js.map +1 -1
- package/package.json +2 -2
- package/dist/conversations/components/ConversationCard.js +0 -119
- package/dist/conversations/components/ConversationCard.js.map +0 -1
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
import { useConversations as e } from "../providers/ConversationsProvider.js";
|
|
2
2
|
import { joinPath as t } from "../utils/navigation.js";
|
|
3
3
|
import { useTr as n } from "../utils/i18n.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { useEventBus as se } from "@burdenoff/fe-libs/shared/events";
|
|
17
|
-
import { jsx as w, jsxs as T } from "react/jsx-runtime";
|
|
4
|
+
import { useConversationsStore as r } from "../store/conversationsStore.js";
|
|
5
|
+
import { useGetConversations as i, useSearchConversations as a } from "../hooks/useConversationQueries.js";
|
|
6
|
+
import { useConversationStream as ee } from "../hooks/useMessageSubscriptions.js";
|
|
7
|
+
import { PermissionGuard as o } from "../components/PermissionGuard.js";
|
|
8
|
+
import { CONVERSATIONS_PERMISSIONS as s } from "../constants/permissions.js";
|
|
9
|
+
import { ConversationRow as c } from "../components/ConversationRow.js";
|
|
10
|
+
import { useCallback as te, useDeferredValue as ne, useEffect as l, useMemo as u, useState as d } from "react";
|
|
11
|
+
import { AlertCircle as f, Archive as p, Hash as m, MessageCircle as h, Pin as g, Users as _ } from "lucide-react";
|
|
12
|
+
import { useNavigate as v, useSearchParams as re } from "react-router-dom";
|
|
13
|
+
import { useEventBus as ie } from "@burdenoff/fe-libs/shared/events";
|
|
14
|
+
import { Fragment as y, jsx as b, jsxs as x } from "react/jsx-runtime";
|
|
15
|
+
import { EmptyState as S } from "@burdenoff/fe-libs/ui";
|
|
18
16
|
//#region src/conversations/pages/ConversationListPage.tsx
|
|
19
|
-
var
|
|
17
|
+
var C = {
|
|
20
18
|
state: "all",
|
|
21
19
|
type: "all",
|
|
22
20
|
visibility: "all",
|
|
@@ -24,19 +22,19 @@ var E = {
|
|
|
24
22
|
isPinned: void 0,
|
|
25
23
|
isMuted: void 0
|
|
26
24
|
};
|
|
27
|
-
function
|
|
28
|
-
let [n, r] =
|
|
29
|
-
return
|
|
25
|
+
function w(e, t) {
|
|
26
|
+
let [n, r] = d(e);
|
|
27
|
+
return l(() => {
|
|
30
28
|
let n = window.setTimeout(() => r(e), t);
|
|
31
29
|
return () => window.clearTimeout(n);
|
|
32
30
|
}, [t, e]), n;
|
|
33
31
|
}
|
|
34
|
-
function
|
|
35
|
-
let
|
|
36
|
-
|
|
37
|
-
let e =
|
|
32
|
+
function T({ forcedFilterParam: d, embedded: T = !1 } = {}) {
|
|
33
|
+
let E = v(), { basePath: D, onConversationSelect: O } = e(), k = ie(), A = n(), { searchQuery: ae, conversationFilter: j, updateConversationFilter: M, activeConversationId: N, setActiveConversationId: P } = r(), [F] = re(), I = ne(w(ae, 250)), L = I.trim(), R = L.length >= 2;
|
|
34
|
+
l(() => {
|
|
35
|
+
let e = d ?? F.get("filter");
|
|
38
36
|
if (!e) {
|
|
39
|
-
|
|
37
|
+
M(C);
|
|
40
38
|
return;
|
|
41
39
|
}
|
|
42
40
|
let t = {
|
|
@@ -46,234 +44,197 @@ function O({ forcedFilterParam: c } = {}) {
|
|
|
46
44
|
archived: "ARCHIVED"
|
|
47
45
|
}[e.toLowerCase()];
|
|
48
46
|
if (!t) {
|
|
49
|
-
|
|
47
|
+
M(C);
|
|
50
48
|
return;
|
|
51
49
|
}
|
|
52
|
-
|
|
53
|
-
...
|
|
50
|
+
M(t === "ARCHIVED" ? {
|
|
51
|
+
...C,
|
|
54
52
|
state: "ARCHIVED"
|
|
55
53
|
} : {
|
|
56
|
-
...
|
|
54
|
+
...C,
|
|
57
55
|
type: t
|
|
58
|
-
})
|
|
56
|
+
});
|
|
59
57
|
}, [
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
d,
|
|
59
|
+
F,
|
|
60
|
+
M
|
|
63
61
|
]);
|
|
64
|
-
let
|
|
62
|
+
let z = u(() => {
|
|
63
|
+
if (!(j.state === "all" || j.state === "ACTIVE")) return [j.state];
|
|
64
|
+
}, [j.state]), { conversations: B, loading: V, error: H } = i({
|
|
65
65
|
first: 50,
|
|
66
|
-
types:
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
types: j.type === "all" ? void 0 : [j.type],
|
|
67
|
+
states: z,
|
|
68
|
+
hasUnread: j.hasUnread,
|
|
69
|
+
isPinned: j.isPinned
|
|
69
70
|
});
|
|
70
|
-
|
|
71
|
-
let { conversations:
|
|
72
|
-
if (
|
|
71
|
+
ee();
|
|
72
|
+
let { conversations: U, loading: W, error: G } = a(I), K = R ? U : B, q = u(() => K.filter((e) => e.isPinned), [K]), J = u(() => K.filter((e) => !e.isPinned), [K]), Y = te((e) => {
|
|
73
|
+
if (P(e.id), O?.(e), k.emit("conversations.conversation.opened", {
|
|
73
74
|
conversationId: e.id,
|
|
74
75
|
conversationType: e.type
|
|
75
76
|
}), e.sourceContext?.contextHref) try {
|
|
76
77
|
let t = new URL(e.sourceContext.contextHref);
|
|
77
78
|
if (t.origin === window.location.origin) {
|
|
78
|
-
|
|
79
|
+
E(`${t.pathname}${t.search}${t.hash}`);
|
|
79
80
|
return;
|
|
80
81
|
}
|
|
81
82
|
} catch {}
|
|
82
|
-
|
|
83
|
+
E(t(D || "/conversations", e.id));
|
|
83
84
|
}, [
|
|
85
|
+
D,
|
|
86
|
+
k,
|
|
87
|
+
E,
|
|
84
88
|
O,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
"
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
className: "flex flex-
|
|
170
|
-
children: [
|
|
171
|
-
/* @__PURE__ */
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
/* @__PURE__ */ w(A, {
|
|
182
|
-
label: N("conversations.groups", "Groups"),
|
|
183
|
-
active: R.type === "GROUP",
|
|
184
|
-
onClick: () => z({ type: "GROUP" })
|
|
185
|
-
}),
|
|
186
|
-
/* @__PURE__ */ w(A, {
|
|
187
|
-
label: N("conversations.channels", "Channels"),
|
|
188
|
-
active: R.type === "CHANNEL",
|
|
189
|
-
onClick: () => z({ type: "CHANNEL" })
|
|
190
|
-
}),
|
|
191
|
-
/* @__PURE__ */ w("div", { className: "w-px h-6 bg-border self-center" }),
|
|
192
|
-
/* @__PURE__ */ w(A, {
|
|
193
|
-
label: N("conversations.unread", "Unread"),
|
|
194
|
-
icon: /* @__PURE__ */ w(re, { size: 12 }),
|
|
195
|
-
active: R.hasUnread === !0,
|
|
196
|
-
onClick: () => z({ hasUnread: R.hasUnread ? void 0 : !0 })
|
|
197
|
-
}),
|
|
198
|
-
/* @__PURE__ */ w(A, {
|
|
199
|
-
label: N("conversations.pinned", "Pinned"),
|
|
200
|
-
icon: /* @__PURE__ */ w(y, { size: 12 }),
|
|
201
|
-
active: R.isPinned === !0,
|
|
202
|
-
onClick: () => z({ isPinned: R.isPinned ? void 0 : !0 })
|
|
89
|
+
P
|
|
90
|
+
]), X = V || W, Z = H ?? G, Q = d === "archived" || j.state === "ARCHIVED", $ = u(() => {
|
|
91
|
+
if (R) return {
|
|
92
|
+
title: A("conversations.empty.search.title", "No matches"),
|
|
93
|
+
description: `${A("conversations.empty.search.description", "No conversations matching")} "${L}"`,
|
|
94
|
+
icon: /* @__PURE__ */ b(h, {
|
|
95
|
+
size: 28,
|
|
96
|
+
className: "text-text-secondary"
|
|
97
|
+
})
|
|
98
|
+
};
|
|
99
|
+
if (Q) return {
|
|
100
|
+
title: A("conversations.empty.archived.title", "No archived conversations"),
|
|
101
|
+
description: A("conversations.empty.archived.description", "Archived conversations are hidden from your main inbox. Move conversations here from the row actions menu."),
|
|
102
|
+
icon: /* @__PURE__ */ b(p, {
|
|
103
|
+
size: 28,
|
|
104
|
+
className: "text-text-secondary"
|
|
105
|
+
})
|
|
106
|
+
};
|
|
107
|
+
switch (d) {
|
|
108
|
+
case "direct": return {
|
|
109
|
+
title: A("conversations.empty.direct.title", "No direct conversations yet"),
|
|
110
|
+
description: A("conversations.empty.direct.description", "Start one with a workspace member from the New conversation button above."),
|
|
111
|
+
icon: /* @__PURE__ */ b(h, {
|
|
112
|
+
size: 28,
|
|
113
|
+
className: "text-text-secondary"
|
|
114
|
+
})
|
|
115
|
+
};
|
|
116
|
+
case "group": return {
|
|
117
|
+
title: A("conversations.empty.groups.title", "Create a group"),
|
|
118
|
+
description: A("conversations.empty.groups.description", "Groups let you chat with multiple people in one thread. Tap New to start one."),
|
|
119
|
+
icon: /* @__PURE__ */ b(_, {
|
|
120
|
+
size: 28,
|
|
121
|
+
className: "text-text-secondary"
|
|
122
|
+
})
|
|
123
|
+
};
|
|
124
|
+
case "channel": return {
|
|
125
|
+
title: A("conversations.empty.channels.title", "Subscribe to a channel"),
|
|
126
|
+
description: A("conversations.empty.channels.description", "Channels are topic-based discussions visible to everyone who joins them."),
|
|
127
|
+
icon: /* @__PURE__ */ b(m, {
|
|
128
|
+
size: 28,
|
|
129
|
+
className: "text-text-secondary"
|
|
130
|
+
})
|
|
131
|
+
};
|
|
132
|
+
default: return {
|
|
133
|
+
title: A("conversations.empty.all.title", "No conversations yet"),
|
|
134
|
+
description: A("conversations.empty.all.description", "Start your first conversation with someone in this workspace."),
|
|
135
|
+
icon: /* @__PURE__ */ b(h, {
|
|
136
|
+
size: 28,
|
|
137
|
+
className: "text-text-secondary"
|
|
138
|
+
})
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}, [
|
|
142
|
+
d,
|
|
143
|
+
Q,
|
|
144
|
+
R,
|
|
145
|
+
A,
|
|
146
|
+
L
|
|
147
|
+
]);
|
|
148
|
+
return T ? /* @__PURE__ */ b(o, {
|
|
149
|
+
permission: s.CONVERSATION_READ,
|
|
150
|
+
allowUnavailablePermissions: !0,
|
|
151
|
+
children: /* @__PURE__ */ b("div", {
|
|
152
|
+
"data-tour": "conversation-list",
|
|
153
|
+
className: "flex h-full min-h-0 w-full flex-col",
|
|
154
|
+
"aria-live": "polite",
|
|
155
|
+
children: X ? /* @__PURE__ */ b("div", {
|
|
156
|
+
role: "status",
|
|
157
|
+
className: "px-4 py-8 text-sm text-text-secondary",
|
|
158
|
+
children: A("conversations.loading", "Loading conversations...")
|
|
159
|
+
}) : Z ? /* @__PURE__ */ b(S, {
|
|
160
|
+
icon: /* @__PURE__ */ b(f, {
|
|
161
|
+
size: 28,
|
|
162
|
+
className: "text-status-error-text",
|
|
163
|
+
"aria-hidden": "true"
|
|
164
|
+
}),
|
|
165
|
+
title: A("conversations.couldNotLoad", "Could not load conversations"),
|
|
166
|
+
description: Z.message
|
|
167
|
+
}) : K.length === 0 ? /* @__PURE__ */ b(S, {
|
|
168
|
+
icon: $.icon,
|
|
169
|
+
title: $.title,
|
|
170
|
+
description: $.description
|
|
171
|
+
}) : /* @__PURE__ */ x("div", {
|
|
172
|
+
"data-tour": "conversation-list-results",
|
|
173
|
+
className: "flex flex-col gap-0.5 px-2 py-2",
|
|
174
|
+
children: [q.length > 0 && /* @__PURE__ */ x(y, { children: [
|
|
175
|
+
/* @__PURE__ */ x("div", {
|
|
176
|
+
className: "flex items-center gap-1.5 px-2 pt-2 pb-1",
|
|
177
|
+
children: [/* @__PURE__ */ b(g, {
|
|
178
|
+
size: 11,
|
|
179
|
+
className: "text-text-secondary",
|
|
180
|
+
"aria-hidden": "true"
|
|
181
|
+
}), /* @__PURE__ */ b("span", {
|
|
182
|
+
className: "text-[10px] font-semibold uppercase tracking-wider text-text-secondary",
|
|
183
|
+
children: A("conversations.pinned", "Pinned")
|
|
184
|
+
})]
|
|
203
185
|
}),
|
|
204
|
-
/* @__PURE__ */
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
186
|
+
q.map((e) => /* @__PURE__ */ b(c, {
|
|
187
|
+
conversation: e,
|
|
188
|
+
isActive: N === e.id,
|
|
189
|
+
onClick: Y
|
|
190
|
+
}, e.id)),
|
|
191
|
+
J.length > 0 && /* @__PURE__ */ b("div", {
|
|
192
|
+
className: "flex items-center gap-1.5 px-2 pt-3 pb-1",
|
|
193
|
+
children: /* @__PURE__ */ b("span", {
|
|
194
|
+
className: "text-[10px] font-semibold uppercase tracking-wider text-text-secondary",
|
|
195
|
+
children: A("conversations.tabs.all", "All")
|
|
196
|
+
})
|
|
209
197
|
})
|
|
210
|
-
]
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}) : J.length === 0 ? /* @__PURE__ */ w(i, {
|
|
220
|
-
icon: /* @__PURE__ */ w(v, { size: 32 }),
|
|
221
|
-
title: N("conversations.noConversations", "No conversations found"),
|
|
222
|
-
description: K ? `${N("conversations.noMatchingConversations", "No conversations matching")} "${P}"` : N("conversations.startConversationPrompt", "Start a new conversation to get going")
|
|
223
|
-
}) : /* @__PURE__ */ T("div", {
|
|
224
|
-
"data-tour": "conversation-list-results",
|
|
225
|
-
className: "space-y-1",
|
|
226
|
-
children: [Y.length > 0 && /* @__PURE__ */ T("div", { children: [/* @__PURE__ */ T("div", {
|
|
227
|
-
className: "flex items-center gap-1.5 px-4 py-2",
|
|
228
|
-
children: [/* @__PURE__ */ w(y, {
|
|
229
|
-
size: 12,
|
|
230
|
-
className: "text-muted-foreground"
|
|
231
|
-
}), /* @__PURE__ */ w("span", {
|
|
232
|
-
className: "text-xs font-medium text-muted-foreground uppercase tracking-wider",
|
|
233
|
-
children: N("conversations.pinned", "Pinned")
|
|
234
|
-
})]
|
|
235
|
-
}), Y.map((e) => /* @__PURE__ */ w(u, {
|
|
236
|
-
conversation: e,
|
|
237
|
-
isActive: B === e.id,
|
|
238
|
-
onClick: Q
|
|
239
|
-
}, e.id))] }), X.length > 0 && /* @__PURE__ */ T("div", { children: [Y.length > 0 && /* @__PURE__ */ w("div", {
|
|
240
|
-
className: "flex items-center gap-1.5 px-4 py-2 mt-2",
|
|
241
|
-
children: /* @__PURE__ */ w("span", {
|
|
242
|
-
className: "text-xs font-medium text-muted-foreground uppercase tracking-wider",
|
|
243
|
-
children: N("conversations.allConversations", "All Conversations")
|
|
244
|
-
})
|
|
245
|
-
}), X.map((e) => /* @__PURE__ */ w(u, {
|
|
246
|
-
conversation: e,
|
|
247
|
-
isActive: B === e.id,
|
|
248
|
-
onClick: Q
|
|
249
|
-
}, e.id))] })]
|
|
250
|
-
})]
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
function k({ active: e, icon: t, label: n, onClick: i }) {
|
|
254
|
-
return /* @__PURE__ */ w("button", {
|
|
255
|
-
type: "button",
|
|
256
|
-
onClick: i,
|
|
257
|
-
"aria-label": n,
|
|
258
|
-
"aria-pressed": e,
|
|
259
|
-
className: r("flex min-h-10 min-w-10 items-center justify-center p-2 transition-colors", e ? "bg-primary/10 text-primary" : "text-muted-foreground hover:text-foreground"),
|
|
260
|
-
children: t
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
function A({ label: e, icon: t, active: n, onClick: i }) {
|
|
264
|
-
return /* @__PURE__ */ w(c, {
|
|
265
|
-
permission: l.CONVERSATION_READ,
|
|
198
|
+
] }), J.map((e) => /* @__PURE__ */ b(c, {
|
|
199
|
+
conversation: e,
|
|
200
|
+
isActive: N === e.id,
|
|
201
|
+
onClick: Y
|
|
202
|
+
}, e.id))]
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
}) : /* @__PURE__ */ b(o, {
|
|
206
|
+
permission: s.CONVERSATION_READ,
|
|
266
207
|
allowUnavailablePermissions: !0,
|
|
267
|
-
children: /* @__PURE__ */
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
"
|
|
271
|
-
|
|
272
|
-
|
|
208
|
+
children: /* @__PURE__ */ b("div", {
|
|
209
|
+
"data-tour": "conversation-list-standalone",
|
|
210
|
+
className: "flex h-full min-h-0 w-full flex-col bg-bg-surface",
|
|
211
|
+
children: X ? /* @__PURE__ */ b("div", {
|
|
212
|
+
className: "px-4 py-8 text-sm text-text-secondary",
|
|
213
|
+
children: A("conversations.loading", "Loading conversations...")
|
|
214
|
+
}) : Z ? /* @__PURE__ */ b(S, {
|
|
215
|
+
icon: /* @__PURE__ */ b(f, {
|
|
216
|
+
size: 28,
|
|
217
|
+
className: "text-status-error-text",
|
|
218
|
+
"aria-hidden": "true"
|
|
219
|
+
}),
|
|
220
|
+
title: A("conversations.couldNotLoad", "Could not load conversations"),
|
|
221
|
+
description: Z.message
|
|
222
|
+
}) : K.length === 0 ? /* @__PURE__ */ b(S, {
|
|
223
|
+
icon: $.icon,
|
|
224
|
+
title: $.title,
|
|
225
|
+
description: $.description
|
|
226
|
+
}) : /* @__PURE__ */ b("div", {
|
|
227
|
+
className: "flex flex-col gap-0.5 px-2 py-2",
|
|
228
|
+
children: K.map((e) => /* @__PURE__ */ b(c, {
|
|
229
|
+
conversation: e,
|
|
230
|
+
isActive: N === e.id,
|
|
231
|
+
onClick: Y
|
|
232
|
+
}, e.id))
|
|
233
|
+
})
|
|
273
234
|
})
|
|
274
235
|
});
|
|
275
236
|
}
|
|
276
237
|
//#endregion
|
|
277
|
-
export {
|
|
238
|
+
export { T as ConversationListPage };
|
|
278
239
|
|
|
279
240
|
//# sourceMappingURL=ConversationListPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConversationListPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationListPage.tsx"],"sourcesContent":["import {\n useMemo,\n useState,\n useCallback,\n useDeferredValue,\n useEffect,\n} from \"react\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport { useNavigate, useSearchParams } from \"react-router-dom\";\nimport {\n Archive,\n Filter,\n LayoutGrid,\n LayoutList,\n MessageCircle,\n Pin,\n Plus,\n Rows3,\n Search,\n SlidersHorizontal,\n X,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport { ConversationCard } from \"../components/ConversationCard\";\nimport { EmptyState, PageLayout } from \"../components/PageLayout\";\nimport {\n useSearchConversations,\n useGetConversations,\n} from \"../hooks/useConversationQueries\";\nimport { useConversationStream } from \"../hooks/useMessageSubscriptions\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport type { Conversation, ConversationFilter } from \"../types\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { useTr } from \"../utils/i18n\";\nimport { joinPath } from \"../utils/navigation\";\n\n// Default filter shape applied whenever the `?filter=` URL param is absent\n// or unrecognised. Covers every dimension on `ConversationFilter` except\n// `searchQuery` — the text search is independent of URL-driven filters and\n// must not be wiped when the URL flips between filter values.\n//\n// The boolean toggles (hasUnread / isPinned / isMuted) are explicitly set\n// to `undefined` here (rather than omitted). Zustand's `set` merges\n// shallowly — the store's `updateConversationFilter` does\n// `{ ...currentFilter, ...partial }` — so an omitted key would preserve the\n// previous value, leaving a toggle like `hasUnread: true` sticky across a\n// URL that drops the filter param. Typing the constant as\n// `Partial<ConversationFilter>` widens the type so the explicit\n// `undefined` entries are accepted as `boolean | undefined` rather than\n// narrowed to the literal `undefined` under `as const`.\nconst FILTER_DEFAULTS: Partial<ConversationFilter> = {\n state: \"all\",\n type: \"all\",\n visibility: \"all\",\n hasUnread: undefined,\n isPinned: undefined,\n isMuted: undefined,\n};\n\nfunction useDebouncedValue<T>(value: T, delayMs: number): T {\n const [debouncedValue, setDebouncedValue] = useState(value);\n\n useEffect(() => {\n const timeoutId = window.setTimeout(\n () => setDebouncedValue(value),\n delayMs,\n );\n return () => window.clearTimeout(timeoutId);\n }, [delayMs, value]);\n\n return debouncedValue;\n}\n\nexport interface ConversationListPageProps {\n forcedFilterParam?: \"channel\" | \"direct\" | \"group\" | \"archived\";\n}\n\nexport function ConversationListPage({\n forcedFilterParam,\n}: ConversationListPageProps = {}) {\n const navigate = useNavigate();\n const { basePath, onConversationSelect } = useConversations();\n const bus = useEventBus();\n const tr = useTr();\n const {\n searchQuery,\n setSearchQuery,\n viewMode,\n setViewMode,\n conversationFilter,\n updateConversationFilter,\n activeConversationId,\n setActiveConversationId,\n } = useConversationsStore();\n const [showFilters, setShowFilters] = useState(false);\n const [searchParams] = useSearchParams();\n const debouncedSearchQuery = useDebouncedValue(searchQuery, 250);\n const deferredSearchQuery = useDeferredValue(debouncedSearchQuery);\n const trimmedSearchQuery = deferredSearchQuery.trim();\n const isSearchActive = trimmedSearchQuery.length >= 2;\n\n // Sync URL filter param (?filter=channel|direct|group|archived) to the store\n // on mount / URL change. When the param is absent or unrecognised, reset\n // every filter dimension to its module-level default (`FILTER_DEFAULTS`) —\n // but deliberately do NOT touch `searchQuery`. The list's text search is\n // independent of URL-driven filters; zeroing it here would wipe in-progress\n // searches every time the URL flips between filter values or drops the\n // param entirely. Using `updateConversationFilter` with an explicit\n // full-defaults payload (instead of the store's `resetConversationFilter`)\n // keeps the merge exhaustive for filter dimensions while leaving\n // `searchQuery` alone.\n useEffect(() => {\n const filterParam = forcedFilterParam ?? searchParams.get(\"filter\");\n\n if (!filterParam) {\n updateConversationFilter(FILTER_DEFAULTS);\n setShowFilters(false);\n return;\n }\n\n const filterMap: Record<string, string> = {\n channel: \"CHANNEL\",\n direct: \"DIRECT\",\n group: \"GROUP\",\n archived: \"ARCHIVED\",\n };\n\n const mapped = filterMap[filterParam.toLowerCase()];\n if (!mapped) {\n updateConversationFilter(FILTER_DEFAULTS);\n setShowFilters(false);\n return;\n }\n\n // Start from a clean filter baseline (still preserving searchQuery) then\n // apply the URL-driven dimension, so switching between ?filter=direct and\n // ?filter=archived can't mix the boolean toggles from the previous URL.\n if (mapped === \"ARCHIVED\") {\n updateConversationFilter({ ...FILTER_DEFAULTS, state: \"ARCHIVED\" });\n } else {\n updateConversationFilter({\n ...FILTER_DEFAULTS,\n type: mapped as \"CHANNEL\" | \"DIRECT\" | \"GROUP\",\n });\n }\n setShowFilters(true);\n }, [forcedFilterParam, searchParams, updateConversationFilter]);\n\n const {\n conversations: baseConversations,\n loading,\n error,\n } = useGetConversations({\n first: 50,\n types:\n conversationFilter.type === \"all\" ? undefined : [conversationFilter.type],\n hasUnread: conversationFilter.hasUnread,\n isPinned: conversationFilter.isPinned,\n });\n\n // Live conversation lifecycle stream — keeps the list current without polling.\n // Apollo cache updates are handled inside the hook (append on add, evict on\n // delete, normalisation handles updates).\n useConversationStream();\n\n const {\n conversations: searchedConversations,\n loading: searchLoading,\n error: searchError,\n } = useSearchConversations(deferredSearchQuery);\n\n const conversations = isSearchActive ? searchedConversations : baseConversations;\n\n const filteredConversations = useMemo(() => {\n if (conversationFilter.state === \"all\") return conversations;\n return conversations.filter(\n (conversation) => conversation.state === conversationFilter.state,\n );\n }, [conversations, conversationFilter.state]);\n\n const pinnedConversations = useMemo(\n () => filteredConversations.filter((conversation) => conversation.isPinned),\n [filteredConversations],\n );\n\n const unpinnedConversations = useMemo(\n () =>\n filteredConversations.filter((conversation) => !conversation.isPinned),\n [filteredConversations],\n );\n\n const totalUnread = useMemo(\n () =>\n filteredConversations.reduce(\n (count, conversation) => count + conversation.unreadCount,\n 0,\n ),\n [filteredConversations],\n );\n\n const handleConversationClick = useCallback(\n (conversation: Conversation) => {\n setActiveConversationId(conversation.id);\n onConversationSelect?.(conversation);\n bus.emit(\"conversations.conversation.opened\", {\n conversationId: conversation.id,\n conversationType: conversation.type,\n });\n\n if (conversation.sourceContext?.contextHref) {\n try {\n const url = new URL(conversation.sourceContext.contextHref);\n if (url.origin === window.location.origin) {\n navigate(`${url.pathname}${url.search}${url.hash}`);\n return;\n }\n } catch {\n // Fall back to conversation detail below.\n }\n }\n\n navigate(joinPath(basePath || \"/conversations\", conversation.id));\n },\n [basePath, bus, navigate, onConversationSelect, setActiveConversationId],\n );\n\n const isLoading = loading || searchLoading;\n const activeError = error ?? searchError;\n const subtitle = [\n `${filteredConversations.length} ${tr(\"conversations.countLabel\", \"conversations\")}`,\n totalUnread > 0\n ? `${totalUnread} ${tr(\"conversations.unreadCountLabel\", \"unread\")}`\n : null,\n ]\n .filter(Boolean)\n .join(\" · \");\n\n return (\n <PageLayout\n title={tr(\"conversations.title\", \"Conversations\")}\n subtitle={subtitle}\n icon={<MessageCircle size={24} />}\n maxWidth=\"2xl\"\n headerActions={\n <button\n data-tour=\"conversation-new-button\"\n type=\"button\"\n onClick={() =>\n navigate(joinPath(basePath || \"/conversations\", \"new\"))\n }\n aria-label={tr(\n \"conversations.createNewConversation\",\n \"Create a new conversation\",\n )}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors shadow-sm\"\n >\n <Plus size={16} />\n {tr(\"conversations.newConversation\", \"New Conversation\")}\n </button>\n }\n >\n <div data-tour=\"conversation-list-header\" className=\"space-y-3\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <div className=\"relative min-w-[14rem] flex-1\">\n <Search\n size={16}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n />\n <input\n data-tour=\"conversation-search\"\n aria-label={tr(\"conversations.search\", \"Search conversations\")}\n type=\"text\"\n placeholder={tr(\n \"conversations.search\",\n \"Search conversations...\",\n )}\n value={searchQuery}\n onChange={(event) => setSearchQuery(event.target.value)}\n className=\"w-full pl-9 pr-8 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 {searchQuery && (\n <button\n type=\"button\"\n onClick={() => setSearchQuery(\"\")}\n aria-label={tr(\n \"conversations.clearSearch\",\n \"Clear conversation search\",\n )}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 p-0.5 rounded text-muted-foreground hover:text-foreground\"\n >\n <X size={14} />\n </button>\n )}\n </div>\n\n <button\n data-tour=\"conversation-filters\"\n type=\"button\"\n onClick={() => setShowFilters((state) => !state)}\n aria-label={\n showFilters\n ? tr(\"conversations.hideFilters\", \"Hide conversation filters\")\n : tr(\"conversations.showFilters\", \"Show conversation filters\")\n }\n aria-pressed={showFilters}\n className={cn(\n \"p-2 rounded-lg border transition-colors\",\n showFilters\n ? \"bg-primary/10 border-primary/30 text-primary\"\n : \"border-border text-muted-foreground hover:text-foreground hover:bg-muted/50\",\n )}\n >\n <SlidersHorizontal size={16} />\n </button>\n\n <div\n data-tour=\"conversation-view-modes\"\n className=\"flex items-center border border-border rounded-lg overflow-hidden\"\n >\n <ViewModeButton\n active={viewMode === \"list\"}\n icon={<LayoutList size={14} />}\n label={tr(\"conversations.listView\", \"List view\")}\n onClick={() => setViewMode(\"list\")}\n />\n <ViewModeButton\n active={viewMode === \"grid\"}\n icon={<LayoutGrid size={14} />}\n label={tr(\"conversations.gridView\", \"Grid view\")}\n onClick={() => setViewMode(\"grid\")}\n />\n <ViewModeButton\n active={viewMode === \"compact\"}\n icon={<Rows3 size={14} />}\n label={tr(\"conversations.compactView\", \"Compact view\")}\n onClick={() => setViewMode(\"compact\")}\n />\n </div>\n </div>\n\n {showFilters && (\n <div className=\"flex flex-wrap gap-2 animate-in fade-in-0 slide-in-from-top-2 duration-200\">\n <FilterChip\n label={tr(\"conversations.all\", \"All\")}\n active={conversationFilter.type === \"all\"}\n onClick={() => updateConversationFilter({ type: \"all\" })}\n />\n <FilterChip\n label={tr(\"conversations.direct\", \"Direct\")}\n active={conversationFilter.type === \"DIRECT\"}\n onClick={() => updateConversationFilter({ type: \"DIRECT\" })}\n />\n <FilterChip\n label={tr(\"conversations.groups\", \"Groups\")}\n active={conversationFilter.type === \"GROUP\"}\n onClick={() => updateConversationFilter({ type: \"GROUP\" })}\n />\n <FilterChip\n label={tr(\"conversations.channels\", \"Channels\")}\n active={conversationFilter.type === \"CHANNEL\"}\n onClick={() => updateConversationFilter({ type: \"CHANNEL\" })}\n />\n <div className=\"w-px h-6 bg-border self-center\" />\n <FilterChip\n label={tr(\"conversations.unread\", \"Unread\")}\n icon={<Filter size={12} />}\n active={conversationFilter.hasUnread === true}\n onClick={() =>\n updateConversationFilter({\n hasUnread: conversationFilter.hasUnread ? undefined : true,\n })\n }\n />\n <FilterChip\n label={tr(\"conversations.pinned\", \"Pinned\")}\n icon={<Pin size={12} />}\n active={conversationFilter.isPinned === true}\n onClick={() =>\n updateConversationFilter({\n isPinned: conversationFilter.isPinned ? undefined : true,\n })\n }\n />\n <FilterChip\n label={tr(\"conversations.archived\", \"Archived\")}\n icon={<Archive size={12} />}\n active={conversationFilter.state === \"ARCHIVED\"}\n onClick={() =>\n updateConversationFilter({\n state:\n conversationFilter.state === \"ARCHIVED\"\n ? \"all\"\n : \"ARCHIVED\",\n })\n }\n />\n </div>\n )}\n </div>\n\n {isLoading ? (\n <div className=\"px-4 py-8 text-sm text-muted-foreground\">\n {tr(\"conversations.loading\", \"Loading conversations...\")}\n </div>\n ) : activeError ? (\n <EmptyState\n icon={<MessageCircle size={32} />}\n title={tr(\n \"conversations.couldNotLoad\",\n \"Could not load conversations\",\n )}\n description={activeError.message}\n />\n ) : filteredConversations.length === 0 ? (\n <EmptyState\n icon={<MessageCircle size={32} />}\n title={tr(\"conversations.noConversations\", \"No conversations found\")}\n description={\n isSearchActive\n ? `${tr(\"conversations.noMatchingConversations\", \"No conversations matching\")} \"${searchQuery}\"`\n : tr(\n \"conversations.startConversationPrompt\",\n \"Start a new conversation to get going\",\n )\n }\n />\n ) : (\n <div data-tour=\"conversation-list-results\" className=\"space-y-1\">\n {pinnedConversations.length > 0 && (\n <div>\n <div className=\"flex items-center gap-1.5 px-4 py-2\">\n <Pin size={12} className=\"text-muted-foreground\" />\n <span className=\"text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr(\"conversations.pinned\", \"Pinned\")}\n </span>\n </div>\n {pinnedConversations.map((conversation) => (\n <ConversationCard\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n </div>\n )}\n\n {unpinnedConversations.length > 0 && (\n <div>\n {pinnedConversations.length > 0 && (\n <div className=\"flex items-center gap-1.5 px-4 py-2 mt-2\">\n <span className=\"text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr(\"conversations.allConversations\", \"All Conversations\")}\n </span>\n </div>\n )}\n {unpinnedConversations.map((conversation) => (\n <ConversationCard\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n </div>\n )}\n </div>\n )}\n </PageLayout>\n );\n}\n\nfunction ViewModeButton({\n active,\n icon,\n label,\n onClick,\n}: {\n active: boolean;\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n aria-pressed={active}\n className={cn(\n \"flex min-h-10 min-w-10 items-center justify-center p-2 transition-colors\",\n active\n ? \"bg-primary/10 text-primary\"\n : \"text-muted-foreground hover:text-foreground\",\n )}\n >\n {icon}\n </button>\n );\n}\n\nfunction FilterChip({\n label,\n icon,\n active,\n onClick,\n}: {\n label: string;\n icon?: React.ReactNode;\n active: boolean;\n onClick: () => void;\n}) {\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <button\n type=\"button\"\n onClick={onClick}\n aria-pressed={active}\n className={cn(\n \"inline-flex min-h-9 items-center gap-1 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors\",\n active\n ? \"bg-primary/10 border-primary/30 text-primary\"\n : \"bg-background border-border text-muted-foreground hover:text-foreground hover:bg-muted/50\",\n )}\n >\n {icon}\n {label}\n </button>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ConversationListPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationListPage.tsx"],"sourcesContent":["import {\n useDeferredValue,\n useEffect,\n useMemo,\n useState,\n useCallback,\n} from \"react\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport { useNavigate, useSearchParams } from \"react-router-dom\";\nimport { EmptyState } from \"@burdenoff/fe-libs/ui\";\nimport {\n Archive,\n AlertCircle,\n Hash,\n MessageCircle,\n Pin,\n Users,\n} from \"lucide-react\";\nimport { ConversationRow } from \"../components/ConversationRow\";\nimport {\n useSearchConversations,\n useGetConversations,\n} from \"../hooks/useConversationQueries\";\nimport { useConversationStream } from \"../hooks/useMessageSubscriptions\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport type {\n Conversation,\n ConversationFilter,\n ConversationState,\n} from \"../types\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { useTr } from \"../utils/i18n\";\nimport { joinPath } from \"../utils/navigation\";\n\n// Default filter shape applied whenever the `?filter=` URL param is absent\n// or unrecognised. Covers every dimension on `ConversationFilter` except\n// `searchQuery` — the text search is independent of URL-driven filters and\n// must not be wiped when the URL flips between filter values.\n//\n// The boolean toggles (hasUnread / isPinned / isMuted) are explicitly set\n// to `undefined` here (rather than omitted). Zustand's `set` merges\n// shallowly — the store's `updateConversationFilter` does\n// `{ ...currentFilter, ...partial }` — so an omitted key would preserve the\n// previous value, leaving a toggle like `hasUnread: true` sticky across a\n// URL that drops the filter param. Typing the constant as\n// `Partial<ConversationFilter>` widens the type so the explicit\n// `undefined` entries are accepted as `boolean | undefined` rather than\n// narrowed to the literal `undefined` under `as const`.\nconst FILTER_DEFAULTS: Partial<ConversationFilter> = {\n state: \"all\",\n type: \"all\",\n visibility: \"all\",\n hasUnread: undefined,\n isPinned: undefined,\n isMuted: undefined,\n};\n\nfunction useDebouncedValue<T>(value: T, delayMs: number): T {\n const [debouncedValue, setDebouncedValue] = useState(value);\n\n useEffect(() => {\n const timeoutId = window.setTimeout(\n () => setDebouncedValue(value),\n delayMs,\n );\n return () => window.clearTimeout(timeoutId);\n }, [delayMs, value]);\n\n return debouncedValue;\n}\n\nexport interface ConversationListPageProps {\n /**\n * Filter dimension forced by the parent inbox shell (driven by the\n * currently active tab). Mutually exclusive with the `?filter=` URL\n * param. When set, the URL param is ignored and the store filter is\n * driven by the prop on every change.\n */\n forcedFilterParam?: \"channel\" | \"direct\" | \"group\" | \"archived\";\n /**\n * When true the page renders bare-list chrome (no own header/search/\n * filter chips) so it can be embedded inside `InboxLayout`. Defaults to\n * false for backwards compatibility with the legacy `/conversations/list`\n * route.\n */\n embedded?: boolean;\n}\n\nexport function ConversationListPage({\n forcedFilterParam,\n embedded = false,\n}: ConversationListPageProps = {}) {\n const navigate = useNavigate();\n const { basePath, onConversationSelect } = useConversations();\n const bus = useEventBus();\n const tr = useTr();\n const {\n searchQuery,\n conversationFilter,\n updateConversationFilter,\n activeConversationId,\n setActiveConversationId,\n } = useConversationsStore();\n const [searchParams] = useSearchParams();\n const debouncedSearchQuery = useDebouncedValue(searchQuery, 250);\n const deferredSearchQuery = useDeferredValue(debouncedSearchQuery);\n const trimmedSearchQuery = deferredSearchQuery.trim();\n const isSearchActive = trimmedSearchQuery.length >= 2;\n\n // Sync URL filter param (?filter=channel|direct|group|archived) to the store\n // on mount / URL change. When the param is absent or unrecognised, reset\n // every filter dimension to its module-level default (`FILTER_DEFAULTS`) —\n // but deliberately do NOT touch `searchQuery`. The list's text search is\n // independent of URL-driven filters; zeroing it here would wipe in-progress\n // searches every time the URL flips between filter values or drops the\n // param entirely.\n useEffect(() => {\n const filterParam = forcedFilterParam ?? searchParams.get(\"filter\");\n\n if (!filterParam) {\n updateConversationFilter(FILTER_DEFAULTS);\n return;\n }\n\n const filterMap: Record<string, string> = {\n channel: \"CHANNEL\",\n direct: \"DIRECT\",\n group: \"GROUP\",\n archived: \"ARCHIVED\",\n };\n\n const mapped = filterMap[filterParam.toLowerCase()];\n if (!mapped) {\n updateConversationFilter(FILTER_DEFAULTS);\n return;\n }\n\n if (mapped === \"ARCHIVED\") {\n updateConversationFilter({ ...FILTER_DEFAULTS, state: \"ARCHIVED\" });\n } else {\n updateConversationFilter({\n ...FILTER_DEFAULTS,\n type: mapped as \"CHANNEL\" | \"DIRECT\" | \"GROUP\",\n });\n }\n }, [forcedFilterParam, searchParams, updateConversationFilter]);\n\n // Map filter.state (\"all\" | \"ACTIVE\" | \"ARCHIVED\" | \"DELETED\") to the\n // GraphQL `states: [ConversationState!]` filter. When the user is on the\n // Archived tab we send `states: [ARCHIVED]`; otherwise we omit the filter\n // so the backend default (ACTIVE only) kicks in.\n const stateFilter = useMemo<ConversationState[] | undefined>(() => {\n if (\n conversationFilter.state === \"all\" ||\n conversationFilter.state === \"ACTIVE\"\n ) {\n return undefined;\n }\n return [conversationFilter.state as ConversationState];\n }, [conversationFilter.state]);\n\n const {\n conversations: baseConversations,\n loading,\n error,\n } = useGetConversations({\n first: 50,\n types:\n conversationFilter.type === \"all\" ? undefined : [conversationFilter.type],\n states: stateFilter,\n hasUnread: conversationFilter.hasUnread,\n isPinned: conversationFilter.isPinned,\n });\n\n // Live conversation lifecycle stream — keeps the list current without polling.\n // Apollo cache updates are handled inside the hook (append on add, evict on\n // delete, normalisation handles updates).\n useConversationStream();\n\n const {\n conversations: searchedConversations,\n loading: searchLoading,\n error: searchError,\n } = useSearchConversations(deferredSearchQuery);\n\n const conversations = isSearchActive\n ? searchedConversations\n : baseConversations;\n\n // When the BE is queried with states:[ARCHIVED] the inbox tab is \"archived\",\n // so we skip the client-side state filter to avoid double-filtering.\n // For the inbox view the BE returns ACTIVE by default; no extra filter\n // needed here either.\n const filteredConversations = conversations;\n\n const pinnedConversations = useMemo(\n () => filteredConversations.filter((conversation) => conversation.isPinned),\n [filteredConversations],\n );\n\n const unpinnedConversations = useMemo(\n () =>\n filteredConversations.filter((conversation) => !conversation.isPinned),\n [filteredConversations],\n );\n\n const handleConversationClick = useCallback(\n (conversation: Conversation) => {\n setActiveConversationId(conversation.id);\n onConversationSelect?.(conversation);\n bus.emit(\"conversations.conversation.opened\", {\n conversationId: conversation.id,\n conversationType: conversation.type,\n });\n\n if (conversation.sourceContext?.contextHref) {\n try {\n const url = new URL(conversation.sourceContext.contextHref);\n if (url.origin === window.location.origin) {\n navigate(`${url.pathname}${url.search}${url.hash}`);\n return;\n }\n } catch {\n // Fall back to conversation detail below.\n }\n }\n\n navigate(joinPath(basePath || \"/conversations\", conversation.id));\n },\n [basePath, bus, navigate, onConversationSelect, setActiveConversationId],\n );\n\n const isLoading = loading || searchLoading;\n const activeError = error ?? searchError;\n const isArchivedTab =\n forcedFilterParam === \"archived\" || conversationFilter.state === \"ARCHIVED\";\n\n // Empty-state copy is tab-aware so users understand why a list is blank.\n const emptyState = useMemo(() => {\n if (isSearchActive) {\n return {\n title: tr(\"conversations.empty.search.title\", \"No matches\"),\n description: `${tr(\n \"conversations.empty.search.description\",\n \"No conversations matching\",\n )} \"${trimmedSearchQuery}\"`,\n icon: <MessageCircle size={28} className=\"text-text-secondary\" />,\n };\n }\n if (isArchivedTab) {\n return {\n title: tr(\n \"conversations.empty.archived.title\",\n \"No archived conversations\",\n ),\n description: tr(\n \"conversations.empty.archived.description\",\n \"Archived conversations are hidden from your main inbox. Move conversations here from the row actions menu.\",\n ),\n icon: <Archive size={28} className=\"text-text-secondary\" />,\n };\n }\n switch (forcedFilterParam) {\n case \"direct\":\n return {\n title: tr(\n \"conversations.empty.direct.title\",\n \"No direct conversations yet\",\n ),\n description: tr(\n \"conversations.empty.direct.description\",\n \"Start one with a workspace member from the New conversation button above.\",\n ),\n icon: <MessageCircle size={28} className=\"text-text-secondary\" />,\n };\n case \"group\":\n return {\n title: tr(\"conversations.empty.groups.title\", \"Create a group\"),\n description: tr(\n \"conversations.empty.groups.description\",\n \"Groups let you chat with multiple people in one thread. Tap New to start one.\",\n ),\n icon: <Users size={28} className=\"text-text-secondary\" />,\n };\n case \"channel\":\n return {\n title: tr(\n \"conversations.empty.channels.title\",\n \"Subscribe to a channel\",\n ),\n description: tr(\n \"conversations.empty.channels.description\",\n \"Channels are topic-based discussions visible to everyone who joins them.\",\n ),\n icon: <Hash size={28} className=\"text-text-secondary\" />,\n };\n default:\n return {\n title: tr(\"conversations.empty.all.title\", \"No conversations yet\"),\n description: tr(\n \"conversations.empty.all.description\",\n \"Start your first conversation with someone in this workspace.\",\n ),\n icon: <MessageCircle size={28} className=\"text-text-secondary\" />,\n };\n }\n }, [\n forcedFilterParam,\n isArchivedTab,\n isSearchActive,\n tr,\n trimmedSearchQuery,\n ]);\n\n // ─── Embedded mode: bare list, no own chrome (used inside InboxLayout) ───\n if (embedded) {\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <div\n data-tour=\"conversation-list\"\n className=\"flex h-full min-h-0 w-full flex-col\"\n aria-live=\"polite\"\n >\n {isLoading ? (\n <div\n role=\"status\"\n className=\"px-4 py-8 text-sm text-text-secondary\"\n >\n {tr(\"conversations.loading\", \"Loading conversations...\")}\n </div>\n ) : activeError ? (\n <EmptyState\n icon={\n <AlertCircle\n size={28}\n className=\"text-status-error-text\"\n aria-hidden=\"true\"\n />\n }\n title={tr(\n \"conversations.couldNotLoad\",\n \"Could not load conversations\",\n )}\n description={activeError.message}\n />\n ) : filteredConversations.length === 0 ? (\n <EmptyState\n icon={emptyState.icon}\n title={emptyState.title}\n description={emptyState.description}\n />\n ) : (\n <div\n data-tour=\"conversation-list-results\"\n className=\"flex flex-col gap-0.5 px-2 py-2\"\n >\n {pinnedConversations.length > 0 && (\n <>\n <div className=\"flex items-center gap-1.5 px-2 pt-2 pb-1\">\n <Pin\n size={11}\n className=\"text-text-secondary\"\n aria-hidden=\"true\"\n />\n <span className=\"text-[10px] font-semibold uppercase tracking-wider text-text-secondary\">\n {tr(\"conversations.pinned\", \"Pinned\")}\n </span>\n </div>\n {pinnedConversations.map((conversation) => (\n <ConversationRow\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n {unpinnedConversations.length > 0 && (\n <div className=\"flex items-center gap-1.5 px-2 pt-3 pb-1\">\n <span className=\"text-[10px] font-semibold uppercase tracking-wider text-text-secondary\">\n {tr(\"conversations.tabs.all\", \"All\")}\n </span>\n </div>\n )}\n </>\n )}\n {unpinnedConversations.map((conversation) => (\n <ConversationRow\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n </div>\n )}\n </div>\n </PermissionGuard>\n );\n }\n\n // ─── Standalone mode (legacy /conversations/list route) ───\n // Retains the original look but uses semantic tokens.\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <div\n data-tour=\"conversation-list-standalone\"\n className=\"flex h-full min-h-0 w-full flex-col bg-bg-surface\"\n >\n {isLoading ? (\n <div className=\"px-4 py-8 text-sm text-text-secondary\">\n {tr(\"conversations.loading\", \"Loading conversations...\")}\n </div>\n ) : activeError ? (\n <EmptyState\n icon={\n <AlertCircle\n size={28}\n className=\"text-status-error-text\"\n aria-hidden=\"true\"\n />\n }\n title={tr(\n \"conversations.couldNotLoad\",\n \"Could not load conversations\",\n )}\n description={activeError.message}\n />\n ) : filteredConversations.length === 0 ? (\n <EmptyState\n icon={emptyState.icon}\n title={emptyState.title}\n description={emptyState.description}\n />\n ) : (\n <div className=\"flex flex-col gap-0.5 px-2 py-2\">\n {filteredConversations.map((conversation) => (\n <ConversationRow\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n </div>\n )}\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|