@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.
Files changed (38) hide show
  1. package/dist/conversations/ConversationsUserRoutes.js +4 -3
  2. package/dist/conversations/ConversationsUserRoutes.js.map +1 -1
  3. package/dist/conversations/components/EmptyState.js +27 -0
  4. package/dist/conversations/components/EmptyState.js.map +1 -0
  5. package/dist/conversations/components/LazyPageBoundary.js +26 -42
  6. package/dist/conversations/components/LazyPageBoundary.js.map +1 -1
  7. package/dist/conversations/components/MessageBubble.js +3 -1
  8. package/dist/conversations/components/MessageBubble.js.map +1 -1
  9. package/dist/conversations/components/PageLayout.js +3 -39
  10. package/dist/conversations/components/PageLayout.js.map +1 -1
  11. package/dist/conversations/components/PageSection.js +25 -0
  12. package/dist/conversations/components/PageSection.js.map +1 -0
  13. package/dist/conversations/components/PermissionGuard.js +1 -1
  14. package/dist/conversations/components/PermissionGuard.js.map +1 -1
  15. package/dist/conversations/components/lazyRetry.js +23 -0
  16. package/dist/conversations/components/lazyRetry.js.map +1 -0
  17. package/dist/conversations/constants/permissions.js +0 -1
  18. package/dist/conversations/constants/permissions.js.map +1 -1
  19. package/dist/conversations/hooks/useMessageQueries.js.map +1 -1
  20. package/dist/conversations/hooks/useMessageSubscriptions.js.map +1 -1
  21. package/dist/conversations/pages/AdminConversationsLandingPage.js +6 -6
  22. package/dist/conversations/pages/AdminConversationsLandingPage.js.map +1 -1
  23. package/dist/conversations/pages/ConversationAdminPages.js +56 -49
  24. package/dist/conversations/pages/ConversationAdminPages.js.map +1 -1
  25. package/dist/conversations/pages/ConversationDetailPage.js +3 -2
  26. package/dist/conversations/pages/ConversationDetailPage.js.map +1 -1
  27. package/dist/conversations/pages/ConversationListPage.js +133 -136
  28. package/dist/conversations/pages/ConversationListPage.js.map +1 -1
  29. package/dist/conversations/pages/ConversationsLandingPage.js +50 -57
  30. package/dist/conversations/pages/ConversationsLandingPage.js.map +1 -1
  31. package/dist/conversations/pages/ConversationsSettingsPage.js +8 -5
  32. package/dist/conversations/pages/ConversationsSettingsPage.js.map +1 -1
  33. package/dist/conversations/pages/NewConversationPage.js +87 -88
  34. package/dist/conversations/pages/NewConversationPage.js.map +1 -1
  35. package/dist/conversations/providers/ConversationsProvider.js +1 -1
  36. package/dist/conversations/providers/ConversationsProvider.js.map +1 -1
  37. package/dist/conversations/utils/mappers.js.map +1 -1
  38. 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, PageLayout as ee } from "../components/PageLayout.js";
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 re, Filter as h, LayoutGrid as ie, LayoutList as ae, MessageCircle as g, Pin as _, Plus as oe, Rows3 as se, Search as ce, SlidersHorizontal as le, X as v } from "lucide-react";
14
- import { useNavigate as y, useSearchParams as b } from "react-router-dom";
15
- import { useEventBus as x } from "@burdenoff/fe-libs/shared/events";
16
- import { jsx as S, jsxs as C } from "react/jsx-runtime";
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 w = {
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 T(e, t) {
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 E({ forcedFilterParam: c } = {}) {
34
- let l = y(), { basePath: E, onConversationSelect: k } = e(), A = x(), j = n(), { searchQuery: M, setSearchQuery: N, viewMode: P, setViewMode: F, conversationFilter: I, updateConversationFilter: L, activeConversationId: R, setActiveConversationId: z } = a(), [B, V] = m(!1), [H] = b(), U = d(T(M, 250)), W = U.trim().length >= 2;
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 ?? H.get("filter");
37
+ let e = c ?? W.get("filter");
37
38
  if (!e) {
38
- L(w), V(!1);
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
- L(w), V(!1);
49
+ z(E), U(!1);
49
50
  return;
50
51
  }
51
- L(t === "ARCHIVED" ? {
52
- ...w,
52
+ z(t === "ARCHIVED" ? {
53
+ ...E,
53
54
  state: "ARCHIVED"
54
55
  } : {
55
- ...w,
56
+ ...E,
56
57
  type: t
57
- }), V(!0);
58
+ }), U(!0);
58
59
  }, [
59
60
  c,
60
- H,
61
- L
61
+ W,
62
+ z
62
63
  ]);
63
- let { conversations: G, loading: ue, error: de } = o({
64
+ let { conversations: ce, loading: le, error: ue } = o({
64
65
  first: 50,
65
- types: I.type === "all" ? void 0 : [I.type],
66
- hasUnread: I.hasUnread,
67
- isPinned: I.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: K, loading: fe, error: pe } = te(U), q = p(() => W ? K : G, [
71
- W,
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(E || "/conversations", e.id));
82
+ l(t(O || "/conversations", e.id));
86
83
  }, [
87
- E,
88
- A,
84
+ O,
85
+ M,
89
86
  l,
90
- k,
91
- z
92
- ]), me = ue || fe, $ = de ?? pe, he = [`${J.length} ${j("conversations.countLabel", "conversations")}`, Z > 0 ? `${Z} ${j("conversations.unreadCountLabel", "unread")}` : null].filter(Boolean).join(" · ");
93
- return /* @__PURE__ */ C(ee, {
94
- title: j("conversations.title", "Conversations"),
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__ */ S(g, { size: 24 }),
93
+ icon: /* @__PURE__ */ w(v, { size: 24 }),
97
94
  maxWidth: "2xl",
98
- headerActions: /* @__PURE__ */ C("button", {
95
+ headerActions: /* @__PURE__ */ T("button", {
99
96
  "data-tour": "conversation-new-button",
100
97
  type: "button",
101
- onClick: () => l(t(E || "/conversations", "new")),
102
- "aria-label": j("conversations.createNewConversation", "Create a new conversation"),
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__ */ S(oe, { size: 16 }), j("conversations.newConversation", "New Conversation")]
101
+ children: [/* @__PURE__ */ w(b, { size: 16 }), N("conversations.newConversation", "New Conversation")]
105
102
  }),
106
- children: [/* @__PURE__ */ C("div", {
103
+ children: [/* @__PURE__ */ T("div", {
107
104
  "data-tour": "conversation-list-header",
108
105
  className: "space-y-3",
109
- children: [/* @__PURE__ */ C("div", {
106
+ children: [/* @__PURE__ */ T("div", {
110
107
  className: "flex flex-wrap items-center gap-2",
111
108
  children: [
112
- /* @__PURE__ */ C("div", {
109
+ /* @__PURE__ */ T("div", {
113
110
  className: "relative min-w-[14rem] flex-1",
114
111
  children: [
115
- /* @__PURE__ */ S(ce, {
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__ */ S("input", {
116
+ /* @__PURE__ */ w("input", {
120
117
  "data-tour": "conversation-search",
121
- "aria-label": j("conversations.search", "Search conversations"),
118
+ "aria-label": N("conversations.search", "Search conversations"),
122
119
  type: "text",
123
- placeholder: j("conversations.search", "Search conversations..."),
124
- value: M,
125
- onChange: (e) => N(e.target.value),
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
- M && /* @__PURE__ */ S("button", {
125
+ P && /* @__PURE__ */ w("button", {
129
126
  type: "button",
130
- onClick: () => N(""),
131
- "aria-label": j("conversations.clearSearch", "Clear conversation search"),
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(v, { size: 14 })
130
+ children: /* @__PURE__ */ w(S, { size: 14 })
134
131
  })
135
132
  ]
136
133
  }),
137
- /* @__PURE__ */ S("button", {
134
+ /* @__PURE__ */ w("button", {
138
135
  "data-tour": "conversation-filters",
139
136
  type: "button",
140
- onClick: () => V((e) => !e),
141
- "aria-label": B ? j("conversations.hideFilters", "Hide conversation filters") : j("conversations.showFilters", "Show conversation filters"),
142
- "aria-pressed": B,
143
- className: r("p-2 rounded-lg border transition-colors", B ? "bg-primary/10 border-primary/30 text-primary" : "border-border text-muted-foreground hover:text-foreground hover:bg-muted/50"),
144
- children: /* @__PURE__ */ S(le, { size: 16 })
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__ */ C("div", {
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__ */ S(D, {
151
- active: P === "list",
152
- icon: /* @__PURE__ */ S(ae, { size: 14 }),
153
- label: j("conversations.listView", "List view"),
154
- onClick: () => F("list")
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__ */ S(D, {
157
- active: P === "grid",
158
- icon: /* @__PURE__ */ S(ie, { size: 14 }),
159
- label: j("conversations.gridView", "Grid view"),
160
- onClick: () => F("grid")
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__ */ S(D, {
163
- active: P === "compact",
164
- icon: /* @__PURE__ */ S(se, { size: 14 }),
165
- label: j("conversations.compactView", "Compact view"),
166
- onClick: () => F("compact")
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
- }), B && /* @__PURE__ */ C("div", {
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__ */ S(O, {
175
- label: j("conversations.all", "All"),
176
- active: I.type === "all",
177
- onClick: () => L({ type: "all" })
171
+ /* @__PURE__ */ w(A, {
172
+ label: N("conversations.all", "All"),
173
+ active: R.type === "all",
174
+ onClick: () => z({ type: "all" })
178
175
  }),
179
- /* @__PURE__ */ S(O, {
180
- label: j("conversations.direct", "Direct"),
181
- active: I.type === "DIRECT",
182
- onClick: () => L({ type: "DIRECT" })
176
+ /* @__PURE__ */ w(A, {
177
+ label: N("conversations.direct", "Direct"),
178
+ active: R.type === "DIRECT",
179
+ onClick: () => z({ type: "DIRECT" })
183
180
  }),
184
- /* @__PURE__ */ S(O, {
185
- label: j("conversations.groups", "Groups"),
186
- active: I.type === "GROUP",
187
- onClick: () => L({ type: "GROUP" })
181
+ /* @__PURE__ */ w(A, {
182
+ label: N("conversations.groups", "Groups"),
183
+ active: R.type === "GROUP",
184
+ onClick: () => z({ type: "GROUP" })
188
185
  }),
189
- /* @__PURE__ */ S(O, {
190
- label: j("conversations.channels", "Channels"),
191
- active: I.type === "CHANNEL",
192
- onClick: () => L({ type: "CHANNEL" })
186
+ /* @__PURE__ */ w(A, {
187
+ label: N("conversations.channels", "Channels"),
188
+ active: R.type === "CHANNEL",
189
+ onClick: () => z({ type: "CHANNEL" })
193
190
  }),
194
- /* @__PURE__ */ S("div", { className: "w-px h-6 bg-border self-center" }),
195
- /* @__PURE__ */ S(O, {
196
- label: j("conversations.unread", "Unread"),
197
- icon: /* @__PURE__ */ S(h, { size: 12 }),
198
- active: I.hasUnread === !0,
199
- onClick: () => L({ hasUnread: I.hasUnread ? void 0 : !0 })
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__ */ S(O, {
202
- label: j("conversations.pinned", "Pinned"),
203
- icon: /* @__PURE__ */ S(_, { size: 12 }),
204
- active: I.isPinned === !0,
205
- onClick: () => L({ isPinned: I.isPinned ? void 0 : !0 })
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__ */ S(O, {
208
- label: j("conversations.archived", "Archived"),
209
- icon: /* @__PURE__ */ S(re, { size: 12 }),
210
- active: I.state === "ARCHIVED",
211
- onClick: () => L({ state: I.state === "ARCHIVED" ? "all" : "ARCHIVED" })
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__ */ S("div", {
212
+ }), me ? /* @__PURE__ */ w("div", {
216
213
  className: "px-4 py-8 text-sm text-muted-foreground",
217
- children: j("conversations.loading", "Loading conversations...")
218
- }) : $ ? /* @__PURE__ */ S(i, {
219
- icon: /* @__PURE__ */ S(g, { size: 32 }),
220
- title: j("conversations.couldNotLoad", "Could not load conversations"),
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__ */ S(i, {
223
- icon: /* @__PURE__ */ S(g, { size: 32 }),
224
- title: j("conversations.noConversations", "No conversations found"),
225
- description: W ? `${j("conversations.noMatchingConversations", "No conversations matching")} "${M}"` : j("conversations.startConversationPrompt", "Start a new conversation to get going")
226
- }) : /* @__PURE__ */ C("div", {
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__ */ C("div", { children: [/* @__PURE__ */ C("div", {
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__ */ S(_, {
228
+ children: [/* @__PURE__ */ w(y, {
232
229
  size: 12,
233
230
  className: "text-muted-foreground"
234
- }), /* @__PURE__ */ S("span", {
231
+ }), /* @__PURE__ */ w("span", {
235
232
  className: "text-xs font-medium text-muted-foreground uppercase tracking-wider",
236
- children: j("conversations.pinned", "Pinned")
233
+ children: N("conversations.pinned", "Pinned")
237
234
  })]
238
- }), Y.map((e) => /* @__PURE__ */ S(u, {
235
+ }), Y.map((e) => /* @__PURE__ */ w(u, {
239
236
  conversation: e,
240
- isActive: R === e.id,
237
+ isActive: B === e.id,
241
238
  onClick: Q
242
- }, e.id))] }), X.length > 0 && /* @__PURE__ */ C("div", { children: [Y.length > 0 && /* @__PURE__ */ S("div", {
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__ */ S("span", {
241
+ children: /* @__PURE__ */ w("span", {
245
242
  className: "text-xs font-medium text-muted-foreground uppercase tracking-wider",
246
- children: j("conversations.allConversations", "All Conversations")
243
+ children: N("conversations.allConversations", "All Conversations")
247
244
  })
248
- }), X.map((e) => /* @__PURE__ */ S(u, {
245
+ }), X.map((e) => /* @__PURE__ */ w(u, {
249
246
  conversation: e,
250
- isActive: R === e.id,
247
+ isActive: B === e.id,
251
248
  onClick: Q
252
249
  }, e.id))] })]
253
250
  })]
254
251
  });
255
252
  }
256
- function D({ active: e, icon: t, label: n, onClick: i }) {
257
- return /* @__PURE__ */ S("button", {
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 O({ label: e, icon: t, active: n, onClick: i }) {
267
- return /* @__PURE__ */ S(c, {
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__ */ C("button", {
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 { E as ConversationListPage };
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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"}