@burdenoff/microfe-conversations 2026.525.3 → 2026.527.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conversations/ConversationsUserRoutes.js +4 -3
- package/dist/conversations/ConversationsUserRoutes.js.map +1 -1
- package/dist/conversations/components/EmptyState.js +27 -0
- package/dist/conversations/components/EmptyState.js.map +1 -0
- package/dist/conversations/components/LazyPageBoundary.js +26 -42
- package/dist/conversations/components/LazyPageBoundary.js.map +1 -1
- package/dist/conversations/components/MessageBubble.js +3 -1
- package/dist/conversations/components/MessageBubble.js.map +1 -1
- package/dist/conversations/components/PageLayout.js +3 -39
- package/dist/conversations/components/PageLayout.js.map +1 -1
- package/dist/conversations/components/PageSection.js +25 -0
- package/dist/conversations/components/PageSection.js.map +1 -0
- package/dist/conversations/components/PermissionGuard.js +1 -1
- package/dist/conversations/components/PermissionGuard.js.map +1 -1
- package/dist/conversations/components/lazyRetry.js +23 -0
- package/dist/conversations/components/lazyRetry.js.map +1 -0
- package/dist/conversations/constants/permissions.js +0 -1
- package/dist/conversations/constants/permissions.js.map +1 -1
- package/dist/conversations/hooks/useMessageQueries.js.map +1 -1
- package/dist/conversations/hooks/useMessageSubscriptions.js.map +1 -1
- package/dist/conversations/pages/AdminConversationsLandingPage.js +6 -6
- package/dist/conversations/pages/AdminConversationsLandingPage.js.map +1 -1
- package/dist/conversations/pages/ConversationAdminPages.js +56 -49
- package/dist/conversations/pages/ConversationAdminPages.js.map +1 -1
- package/dist/conversations/pages/ConversationDetailPage.js +3 -2
- package/dist/conversations/pages/ConversationDetailPage.js.map +1 -1
- package/dist/conversations/pages/ConversationListPage.js +133 -136
- package/dist/conversations/pages/ConversationListPage.js.map +1 -1
- package/dist/conversations/pages/ConversationsLandingPage.js +50 -57
- package/dist/conversations/pages/ConversationsLandingPage.js.map +1 -1
- package/dist/conversations/pages/ConversationsSettingsPage.js +8 -5
- package/dist/conversations/pages/ConversationsSettingsPage.js.map +1 -1
- package/dist/conversations/pages/NewConversationPage.js +87 -88
- package/dist/conversations/pages/NewConversationPage.js.map +1 -1
- package/dist/conversations/providers/ConversationsProvider.js +1 -1
- package/dist/conversations/providers/ConversationsProvider.js.map +1 -1
- package/dist/conversations/utils/mappers.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,8 @@ 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
4
|
import { cn as r } from "../utils/cn.js";
|
|
5
|
-
import { EmptyState as i
|
|
5
|
+
import { EmptyState as i } from "../components/EmptyState.js";
|
|
6
|
+
import { PageLayout as ee } from "../components/PageLayout.js";
|
|
6
7
|
import { useConversationsStore as a } from "../store/conversationsStore.js";
|
|
7
8
|
import { useGetConversations as o, useSearchConversations as te } from "../hooks/useConversationQueries.js";
|
|
8
9
|
import { useConversationStream as s } from "../hooks/useMessageSubscriptions.js";
|
|
@@ -10,12 +11,12 @@ import { PermissionGuard as c } from "../components/PermissionGuard.js";
|
|
|
10
11
|
import { CONVERSATIONS_PERMISSIONS as l } from "../constants/permissions.js";
|
|
11
12
|
import { ConversationCard as u } from "../components/ConversationCard.js";
|
|
12
13
|
import { useCallback as ne, useDeferredValue as d, useEffect as f, useMemo as p, useState as m } from "react";
|
|
13
|
-
import { Archive as
|
|
14
|
-
import { useNavigate as
|
|
15
|
-
import { useEventBus as
|
|
16
|
-
import { jsx as
|
|
14
|
+
import { Archive as h, Filter as re, LayoutGrid as g, LayoutList as _, MessageCircle as v, Pin as y, Plus as b, Rows3 as ie, Search as x, SlidersHorizontal as ae, X as S } from "lucide-react";
|
|
15
|
+
import { useNavigate as C, useSearchParams as oe } from "react-router-dom";
|
|
16
|
+
import { useEventBus as se } from "@burdenoff/fe-libs/shared/events";
|
|
17
|
+
import { jsx as w, jsxs as T } from "react/jsx-runtime";
|
|
17
18
|
//#region src/conversations/pages/ConversationListPage.tsx
|
|
18
|
-
var
|
|
19
|
+
var E = {
|
|
19
20
|
state: "all",
|
|
20
21
|
type: "all",
|
|
21
22
|
visibility: "all",
|
|
@@ -23,19 +24,19 @@ var w = {
|
|
|
23
24
|
isPinned: void 0,
|
|
24
25
|
isMuted: void 0
|
|
25
26
|
};
|
|
26
|
-
function
|
|
27
|
+
function D(e, t) {
|
|
27
28
|
let [n, r] = m(e);
|
|
28
29
|
return f(() => {
|
|
29
30
|
let n = window.setTimeout(() => r(e), t);
|
|
30
31
|
return () => window.clearTimeout(n);
|
|
31
32
|
}, [t, e]), n;
|
|
32
33
|
}
|
|
33
|
-
function
|
|
34
|
-
let l =
|
|
34
|
+
function O({ forcedFilterParam: c } = {}) {
|
|
35
|
+
let l = C(), { basePath: O, onConversationSelect: j } = e(), M = se(), N = n(), { searchQuery: P, setSearchQuery: F, viewMode: I, setViewMode: L, conversationFilter: R, updateConversationFilter: z, activeConversationId: B, setActiveConversationId: V } = a(), [H, U] = m(!1), [W] = oe(), G = d(D(P, 250)), K = G.trim().length >= 2;
|
|
35
36
|
f(() => {
|
|
36
|
-
let e = c ??
|
|
37
|
+
let e = c ?? W.get("filter");
|
|
37
38
|
if (!e) {
|
|
38
|
-
|
|
39
|
+
z(E), U(!1);
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
41
42
|
let t = {
|
|
@@ -45,34 +46,30 @@ function E({ forcedFilterParam: c } = {}) {
|
|
|
45
46
|
archived: "ARCHIVED"
|
|
46
47
|
}[e.toLowerCase()];
|
|
47
48
|
if (!t) {
|
|
48
|
-
|
|
49
|
+
z(E), U(!1);
|
|
49
50
|
return;
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
-
...
|
|
52
|
+
z(t === "ARCHIVED" ? {
|
|
53
|
+
...E,
|
|
53
54
|
state: "ARCHIVED"
|
|
54
55
|
} : {
|
|
55
|
-
...
|
|
56
|
+
...E,
|
|
56
57
|
type: t
|
|
57
|
-
}),
|
|
58
|
+
}), U(!0);
|
|
58
59
|
}, [
|
|
59
60
|
c,
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
W,
|
|
62
|
+
z
|
|
62
63
|
]);
|
|
63
|
-
let { conversations:
|
|
64
|
+
let { conversations: ce, loading: le, error: ue } = o({
|
|
64
65
|
first: 50,
|
|
65
|
-
types:
|
|
66
|
-
hasUnread:
|
|
67
|
-
isPinned:
|
|
66
|
+
types: R.type === "all" ? void 0 : [R.type],
|
|
67
|
+
hasUnread: R.hasUnread,
|
|
68
|
+
isPinned: R.isPinned
|
|
68
69
|
});
|
|
69
70
|
s();
|
|
70
|
-
let { conversations:
|
|
71
|
-
|
|
72
|
-
K,
|
|
73
|
-
G
|
|
74
|
-
]), J = p(() => I.state === "all" ? q : q.filter((e) => e.state === I.state), [q, I.state]), Y = p(() => J.filter((e) => e.isPinned), [J]), X = p(() => J.filter((e) => !e.isPinned), [J]), Z = p(() => J.reduce((e, t) => e + t.unreadCount, 0), [J]), Q = ne((e) => {
|
|
75
|
-
if (z(e.id), k?.(e), A.emit("conversations.conversation.opened", {
|
|
71
|
+
let { conversations: de, loading: fe, error: pe } = te(G), q = K ? de : ce, J = p(() => R.state === "all" ? q : q.filter((e) => e.state === R.state), [q, R.state]), Y = p(() => J.filter((e) => e.isPinned), [J]), X = p(() => J.filter((e) => !e.isPinned), [J]), Z = p(() => J.reduce((e, t) => e + t.unreadCount, 0), [J]), Q = ne((e) => {
|
|
72
|
+
if (V(e.id), j?.(e), M.emit("conversations.conversation.opened", {
|
|
76
73
|
conversationId: e.id,
|
|
77
74
|
conversationType: e.type
|
|
78
75
|
}), e.sourceContext?.contextHref) try {
|
|
@@ -82,179 +79,179 @@ function E({ forcedFilterParam: c } = {}) {
|
|
|
82
79
|
return;
|
|
83
80
|
}
|
|
84
81
|
} catch {}
|
|
85
|
-
l(t(
|
|
82
|
+
l(t(O || "/conversations", e.id));
|
|
86
83
|
}, [
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
O,
|
|
85
|
+
M,
|
|
89
86
|
l,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
]), me =
|
|
93
|
-
return /* @__PURE__ */
|
|
94
|
-
title:
|
|
87
|
+
j,
|
|
88
|
+
V
|
|
89
|
+
]), me = le || fe, $ = ue ?? pe, he = [`${J.length} ${N("conversations.countLabel", "conversations")}`, Z > 0 ? `${Z} ${N("conversations.unreadCountLabel", "unread")}` : null].filter(Boolean).join(" · ");
|
|
90
|
+
return /* @__PURE__ */ T(ee, {
|
|
91
|
+
title: N("conversations.title", "Conversations"),
|
|
95
92
|
subtitle: he,
|
|
96
|
-
icon: /* @__PURE__ */
|
|
93
|
+
icon: /* @__PURE__ */ w(v, { size: 24 }),
|
|
97
94
|
maxWidth: "2xl",
|
|
98
|
-
headerActions: /* @__PURE__ */
|
|
95
|
+
headerActions: /* @__PURE__ */ T("button", {
|
|
99
96
|
"data-tour": "conversation-new-button",
|
|
100
97
|
type: "button",
|
|
101
|
-
onClick: () => l(t(
|
|
102
|
-
"aria-label":
|
|
98
|
+
onClick: () => l(t(O || "/conversations", "new")),
|
|
99
|
+
"aria-label": N("conversations.createNewConversation", "Create a new conversation"),
|
|
103
100
|
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",
|
|
104
|
-
children: [/* @__PURE__ */
|
|
101
|
+
children: [/* @__PURE__ */ w(b, { size: 16 }), N("conversations.newConversation", "New Conversation")]
|
|
105
102
|
}),
|
|
106
|
-
children: [/* @__PURE__ */
|
|
103
|
+
children: [/* @__PURE__ */ T("div", {
|
|
107
104
|
"data-tour": "conversation-list-header",
|
|
108
105
|
className: "space-y-3",
|
|
109
|
-
children: [/* @__PURE__ */
|
|
106
|
+
children: [/* @__PURE__ */ T("div", {
|
|
110
107
|
className: "flex flex-wrap items-center gap-2",
|
|
111
108
|
children: [
|
|
112
|
-
/* @__PURE__ */
|
|
109
|
+
/* @__PURE__ */ T("div", {
|
|
113
110
|
className: "relative min-w-[14rem] flex-1",
|
|
114
111
|
children: [
|
|
115
|
-
/* @__PURE__ */
|
|
112
|
+
/* @__PURE__ */ w(x, {
|
|
116
113
|
size: 16,
|
|
117
114
|
className: "absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
|
|
118
115
|
}),
|
|
119
|
-
/* @__PURE__ */
|
|
116
|
+
/* @__PURE__ */ w("input", {
|
|
120
117
|
"data-tour": "conversation-search",
|
|
121
|
-
"aria-label":
|
|
118
|
+
"aria-label": N("conversations.search", "Search conversations"),
|
|
122
119
|
type: "text",
|
|
123
|
-
placeholder:
|
|
124
|
-
value:
|
|
125
|
-
onChange: (e) =>
|
|
120
|
+
placeholder: N("conversations.search", "Search conversations..."),
|
|
121
|
+
value: P,
|
|
122
|
+
onChange: (e) => F(e.target.value),
|
|
126
123
|
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"
|
|
127
124
|
}),
|
|
128
|
-
|
|
125
|
+
P && /* @__PURE__ */ w("button", {
|
|
129
126
|
type: "button",
|
|
130
|
-
onClick: () =>
|
|
131
|
-
"aria-label":
|
|
127
|
+
onClick: () => F(""),
|
|
128
|
+
"aria-label": N("conversations.clearSearch", "Clear conversation search"),
|
|
132
129
|
className: "absolute right-2 top-1/2 -translate-y-1/2 p-0.5 rounded text-muted-foreground hover:text-foreground",
|
|
133
|
-
children: /* @__PURE__ */ S
|
|
130
|
+
children: /* @__PURE__ */ w(S, { size: 14 })
|
|
134
131
|
})
|
|
135
132
|
]
|
|
136
133
|
}),
|
|
137
|
-
/* @__PURE__ */
|
|
134
|
+
/* @__PURE__ */ w("button", {
|
|
138
135
|
"data-tour": "conversation-filters",
|
|
139
136
|
type: "button",
|
|
140
|
-
onClick: () =>
|
|
141
|
-
"aria-label":
|
|
142
|
-
"aria-pressed":
|
|
143
|
-
className: r("p-2 rounded-lg border transition-colors",
|
|
144
|
-
children: /* @__PURE__ */
|
|
137
|
+
onClick: () => U((e) => !e),
|
|
138
|
+
"aria-label": H ? N("conversations.hideFilters", "Hide conversation filters") : N("conversations.showFilters", "Show conversation filters"),
|
|
139
|
+
"aria-pressed": H,
|
|
140
|
+
className: r("p-2 rounded-lg border transition-colors", H ? "bg-primary/10 border-primary/30 text-primary" : "border-border text-muted-foreground hover:text-foreground hover:bg-muted/50"),
|
|
141
|
+
children: /* @__PURE__ */ w(ae, { size: 16 })
|
|
145
142
|
}),
|
|
146
|
-
/* @__PURE__ */
|
|
143
|
+
/* @__PURE__ */ T("div", {
|
|
147
144
|
"data-tour": "conversation-view-modes",
|
|
148
145
|
className: "flex items-center border border-border rounded-lg overflow-hidden",
|
|
149
146
|
children: [
|
|
150
|
-
/* @__PURE__ */
|
|
151
|
-
active:
|
|
152
|
-
icon: /* @__PURE__ */
|
|
153
|
-
label:
|
|
154
|
-
onClick: () =>
|
|
147
|
+
/* @__PURE__ */ w(k, {
|
|
148
|
+
active: I === "list",
|
|
149
|
+
icon: /* @__PURE__ */ w(_, { size: 14 }),
|
|
150
|
+
label: N("conversations.listView", "List view"),
|
|
151
|
+
onClick: () => L("list")
|
|
155
152
|
}),
|
|
156
|
-
/* @__PURE__ */
|
|
157
|
-
active:
|
|
158
|
-
icon: /* @__PURE__ */
|
|
159
|
-
label:
|
|
160
|
-
onClick: () =>
|
|
153
|
+
/* @__PURE__ */ w(k, {
|
|
154
|
+
active: I === "grid",
|
|
155
|
+
icon: /* @__PURE__ */ w(g, { size: 14 }),
|
|
156
|
+
label: N("conversations.gridView", "Grid view"),
|
|
157
|
+
onClick: () => L("grid")
|
|
161
158
|
}),
|
|
162
|
-
/* @__PURE__ */
|
|
163
|
-
active:
|
|
164
|
-
icon: /* @__PURE__ */
|
|
165
|
-
label:
|
|
166
|
-
onClick: () =>
|
|
159
|
+
/* @__PURE__ */ w(k, {
|
|
160
|
+
active: I === "compact",
|
|
161
|
+
icon: /* @__PURE__ */ w(ie, { size: 14 }),
|
|
162
|
+
label: N("conversations.compactView", "Compact view"),
|
|
163
|
+
onClick: () => L("compact")
|
|
167
164
|
})
|
|
168
165
|
]
|
|
169
166
|
})
|
|
170
167
|
]
|
|
171
|
-
}),
|
|
168
|
+
}), H && /* @__PURE__ */ T("div", {
|
|
172
169
|
className: "flex flex-wrap gap-2 animate-in fade-in-0 slide-in-from-top-2 duration-200",
|
|
173
170
|
children: [
|
|
174
|
-
/* @__PURE__ */
|
|
175
|
-
label:
|
|
176
|
-
active:
|
|
177
|
-
onClick: () =>
|
|
171
|
+
/* @__PURE__ */ w(A, {
|
|
172
|
+
label: N("conversations.all", "All"),
|
|
173
|
+
active: R.type === "all",
|
|
174
|
+
onClick: () => z({ type: "all" })
|
|
178
175
|
}),
|
|
179
|
-
/* @__PURE__ */
|
|
180
|
-
label:
|
|
181
|
-
active:
|
|
182
|
-
onClick: () =>
|
|
176
|
+
/* @__PURE__ */ w(A, {
|
|
177
|
+
label: N("conversations.direct", "Direct"),
|
|
178
|
+
active: R.type === "DIRECT",
|
|
179
|
+
onClick: () => z({ type: "DIRECT" })
|
|
183
180
|
}),
|
|
184
|
-
/* @__PURE__ */
|
|
185
|
-
label:
|
|
186
|
-
active:
|
|
187
|
-
onClick: () =>
|
|
181
|
+
/* @__PURE__ */ w(A, {
|
|
182
|
+
label: N("conversations.groups", "Groups"),
|
|
183
|
+
active: R.type === "GROUP",
|
|
184
|
+
onClick: () => z({ type: "GROUP" })
|
|
188
185
|
}),
|
|
189
|
-
/* @__PURE__ */
|
|
190
|
-
label:
|
|
191
|
-
active:
|
|
192
|
-
onClick: () =>
|
|
186
|
+
/* @__PURE__ */ w(A, {
|
|
187
|
+
label: N("conversations.channels", "Channels"),
|
|
188
|
+
active: R.type === "CHANNEL",
|
|
189
|
+
onClick: () => z({ type: "CHANNEL" })
|
|
193
190
|
}),
|
|
194
|
-
/* @__PURE__ */
|
|
195
|
-
/* @__PURE__ */
|
|
196
|
-
label:
|
|
197
|
-
icon: /* @__PURE__ */
|
|
198
|
-
active:
|
|
199
|
-
onClick: () =>
|
|
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 })
|
|
200
197
|
}),
|
|
201
|
-
/* @__PURE__ */
|
|
202
|
-
label:
|
|
203
|
-
icon: /* @__PURE__ */
|
|
204
|
-
active:
|
|
205
|
-
onClick: () =>
|
|
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 })
|
|
206
203
|
}),
|
|
207
|
-
/* @__PURE__ */
|
|
208
|
-
label:
|
|
209
|
-
icon: /* @__PURE__ */
|
|
210
|
-
active:
|
|
211
|
-
onClick: () =>
|
|
204
|
+
/* @__PURE__ */ w(A, {
|
|
205
|
+
label: N("conversations.archived", "Archived"),
|
|
206
|
+
icon: /* @__PURE__ */ w(h, { size: 12 }),
|
|
207
|
+
active: R.state === "ARCHIVED",
|
|
208
|
+
onClick: () => z({ state: R.state === "ARCHIVED" ? "all" : "ARCHIVED" })
|
|
212
209
|
})
|
|
213
210
|
]
|
|
214
211
|
})]
|
|
215
|
-
}), me ? /* @__PURE__ */
|
|
212
|
+
}), me ? /* @__PURE__ */ w("div", {
|
|
216
213
|
className: "px-4 py-8 text-sm text-muted-foreground",
|
|
217
|
-
children:
|
|
218
|
-
}) : $ ? /* @__PURE__ */
|
|
219
|
-
icon: /* @__PURE__ */
|
|
220
|
-
title:
|
|
214
|
+
children: N("conversations.loading", "Loading conversations...")
|
|
215
|
+
}) : $ ? /* @__PURE__ */ w(i, {
|
|
216
|
+
icon: /* @__PURE__ */ w(v, { size: 32 }),
|
|
217
|
+
title: N("conversations.couldNotLoad", "Could not load conversations"),
|
|
221
218
|
description: $.message
|
|
222
|
-
}) : J.length === 0 ? /* @__PURE__ */
|
|
223
|
-
icon: /* @__PURE__ */
|
|
224
|
-
title:
|
|
225
|
-
description:
|
|
226
|
-
}) : /* @__PURE__ */
|
|
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", {
|
|
227
224
|
"data-tour": "conversation-list-results",
|
|
228
225
|
className: "space-y-1",
|
|
229
|
-
children: [Y.length > 0 && /* @__PURE__ */
|
|
226
|
+
children: [Y.length > 0 && /* @__PURE__ */ T("div", { children: [/* @__PURE__ */ T("div", {
|
|
230
227
|
className: "flex items-center gap-1.5 px-4 py-2",
|
|
231
|
-
children: [/* @__PURE__ */
|
|
228
|
+
children: [/* @__PURE__ */ w(y, {
|
|
232
229
|
size: 12,
|
|
233
230
|
className: "text-muted-foreground"
|
|
234
|
-
}), /* @__PURE__ */
|
|
231
|
+
}), /* @__PURE__ */ w("span", {
|
|
235
232
|
className: "text-xs font-medium text-muted-foreground uppercase tracking-wider",
|
|
236
|
-
children:
|
|
233
|
+
children: N("conversations.pinned", "Pinned")
|
|
237
234
|
})]
|
|
238
|
-
}), Y.map((e) => /* @__PURE__ */
|
|
235
|
+
}), Y.map((e) => /* @__PURE__ */ w(u, {
|
|
239
236
|
conversation: e,
|
|
240
|
-
isActive:
|
|
237
|
+
isActive: B === e.id,
|
|
241
238
|
onClick: Q
|
|
242
|
-
}, e.id))] }), X.length > 0 && /* @__PURE__ */
|
|
239
|
+
}, e.id))] }), X.length > 0 && /* @__PURE__ */ T("div", { children: [Y.length > 0 && /* @__PURE__ */ w("div", {
|
|
243
240
|
className: "flex items-center gap-1.5 px-4 py-2 mt-2",
|
|
244
|
-
children: /* @__PURE__ */
|
|
241
|
+
children: /* @__PURE__ */ w("span", {
|
|
245
242
|
className: "text-xs font-medium text-muted-foreground uppercase tracking-wider",
|
|
246
|
-
children:
|
|
243
|
+
children: N("conversations.allConversations", "All Conversations")
|
|
247
244
|
})
|
|
248
|
-
}), X.map((e) => /* @__PURE__ */
|
|
245
|
+
}), X.map((e) => /* @__PURE__ */ w(u, {
|
|
249
246
|
conversation: e,
|
|
250
|
-
isActive:
|
|
247
|
+
isActive: B === e.id,
|
|
251
248
|
onClick: Q
|
|
252
249
|
}, e.id))] })]
|
|
253
250
|
})]
|
|
254
251
|
});
|
|
255
252
|
}
|
|
256
|
-
function
|
|
257
|
-
return /* @__PURE__ */
|
|
253
|
+
function k({ active: e, icon: t, label: n, onClick: i }) {
|
|
254
|
+
return /* @__PURE__ */ w("button", {
|
|
258
255
|
type: "button",
|
|
259
256
|
onClick: i,
|
|
260
257
|
"aria-label": n,
|
|
@@ -263,11 +260,11 @@ function D({ active: e, icon: t, label: n, onClick: i }) {
|
|
|
263
260
|
children: t
|
|
264
261
|
});
|
|
265
262
|
}
|
|
266
|
-
function
|
|
267
|
-
return /* @__PURE__ */
|
|
263
|
+
function A({ label: e, icon: t, active: n, onClick: i }) {
|
|
264
|
+
return /* @__PURE__ */ w(c, {
|
|
268
265
|
permission: l.CONVERSATION_READ,
|
|
269
266
|
allowUnavailablePermissions: !0,
|
|
270
|
-
children: /* @__PURE__ */
|
|
267
|
+
children: /* @__PURE__ */ T("button", {
|
|
271
268
|
type: "button",
|
|
272
269
|
onClick: i,
|
|
273
270
|
"aria-pressed": n,
|
|
@@ -277,6 +274,6 @@ function O({ label: e, icon: t, active: n, onClick: i }) {
|
|
|
277
274
|
});
|
|
278
275
|
}
|
|
279
276
|
//#endregion
|
|
280
|
-
export {
|
|
277
|
+
export { O as ConversationListPage };
|
|
281
278
|
|
|
282
279
|
//# 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 = useMemo(\n () => (isSearchActive ? searchedConversations : baseConversations),\n [isSearchActive, searchedConversations, baseConversations],\n );\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 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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"}
|