@burdenoff/microfe-conversations 2026.624.1 → 2026.625.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,17 +2,17 @@ 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 { useConversationsStore as r } from "../store/conversationsStore.js";
5
- import { useGetConversations as i, useSearchConversations as a } from "../hooks/useConversationQueries.js";
6
- import { useConversationStream as ee } from "../hooks/useMessageSubscriptions.js";
5
+ import { useGetConversations as i, useSearchConversations as ee } from "../hooks/useConversationQueries.js";
6
+ import { useConversationStream as a } from "../hooks/useMessageSubscriptions.js";
7
7
  import { PermissionGuard as o } from "../components/PermissionGuard.js";
8
8
  import { CONVERSATIONS_PERMISSIONS as s } from "../constants/permissions.js";
9
9
  import { ConversationRow as c } from "../components/ConversationRow.js";
10
- import { useCallback as te, useDeferredValue as ne, useEffect as l, useMemo as u, useState as d } from "react";
11
- import { AlertCircle as f, Archive as p, Hash as m, MessageCircle as h, Pin as g, Users as _ } from "lucide-react";
12
- import { useNavigate as v, useSearchParams as re } from "react-router-dom";
13
- import { useEventBus as ie } from "@burdenoff/fe-libs/shared/events";
14
- import { Fragment as y, jsx as b, jsxs as x } from "react/jsx-runtime";
15
- import { EmptyState as S } from "@burdenoff/fe-libs/ui";
10
+ import { useCallback as te, useDeferredValue as l, useEffect as u, useMemo as d, useState as f } from "react";
11
+ import { AlertCircle as p, Archive as m, Hash as h, MessageCircle as g, Pin as ne, Users as re } from "lucide-react";
12
+ import { useNavigate as ie, useSearchParams as ae } from "react-router-dom";
13
+ import { useEventBus as _ } from "@burdenoff/fe-libs/shared/events";
14
+ import { Fragment as v, jsx as y, jsxs as b } from "react/jsx-runtime";
15
+ import { EmptyState as x, IllustratedEmptyState as S } from "@burdenoff/fe-libs/ui";
16
16
  //#region src/conversations/pages/ConversationListPage.tsx
17
17
  var C = {
18
18
  state: "all",
@@ -23,16 +23,16 @@ var C = {
23
23
  isMuted: void 0
24
24
  };
25
25
  function w(e, t) {
26
- let [n, r] = d(e);
27
- return l(() => {
26
+ let [n, r] = f(e);
27
+ return u(() => {
28
28
  let n = window.setTimeout(() => r(e), t);
29
29
  return () => window.clearTimeout(n);
30
30
  }, [t, e]), n;
31
31
  }
32
- function T({ forcedFilterParam: d, embedded: T = !1 } = {}) {
33
- let E = v(), { basePath: D, onConversationSelect: O } = e(), k = ie(), A = n(), { searchQuery: ae, conversationFilter: j, updateConversationFilter: M, activeConversationId: N, setActiveConversationId: P } = r(), [F] = re(), I = ne(w(ae, 250)), L = I.trim(), R = L.length >= 2;
34
- l(() => {
35
- let e = d ?? F.get("filter");
32
+ function T({ forcedFilterParam: f, embedded: T = !1 } = {}) {
33
+ let E = ie(), { basePath: D, onConversationSelect: O } = e(), k = _(), A = n(), { searchQuery: oe, conversationFilter: j, updateConversationFilter: M, activeConversationId: N, setActiveConversationId: P } = r(), [F] = ae(), I = l(w(oe, 250)), L = I.trim(), R = L.length >= 2;
34
+ u(() => {
35
+ let e = f ?? F.get("filter");
36
36
  if (!e) {
37
37
  M(C);
38
38
  return;
@@ -55,11 +55,11 @@ function T({ forcedFilterParam: d, embedded: T = !1 } = {}) {
55
55
  type: t
56
56
  });
57
57
  }, [
58
- d,
58
+ f,
59
59
  F,
60
60
  M
61
61
  ]);
62
- let z = u(() => {
62
+ let z = d(() => {
63
63
  if (!(j.state === "all" || j.state === "ACTIVE")) return [j.state];
64
64
  }, [j.state]), { conversations: B, loading: V, error: H } = i({
65
65
  first: 50,
@@ -68,8 +68,8 @@ function T({ forcedFilterParam: d, embedded: T = !1 } = {}) {
68
68
  hasUnread: j.hasUnread,
69
69
  isPinned: j.isPinned
70
70
  });
71
- ee();
72
- let { conversations: U, loading: W, error: G } = a(I), K = R ? U : B, q = u(() => K.filter((e) => e.isPinned), [K]), J = u(() => K.filter((e) => !e.isPinned), [K]), Y = te((e) => {
71
+ a();
72
+ let { conversations: U, loading: W, error: G } = ee(I), K = R ? U : B, q = d(() => K.filter((e) => e.isPinned), [K]), J = d(() => K.filter((e) => !e.isPinned), [K]), Y = te((e) => {
73
73
  if (P(e.id), O?.(e), k.emit("conversations.conversation.opened", {
74
74
  conversationId: e.id,
75
75
  conversationType: e.type
@@ -87,145 +87,151 @@ function T({ forcedFilterParam: d, embedded: T = !1 } = {}) {
87
87
  E,
88
88
  O,
89
89
  P
90
- ]), X = V || W, Z = H ?? G, Q = d === "archived" || j.state === "ARCHIVED", $ = u(() => {
90
+ ]), X = V || W, Z = H ?? G, Q = f === "archived" || j.state === "ARCHIVED", $ = d(() => {
91
91
  if (R) return {
92
92
  title: A("conversations.empty.search.title", "No matches"),
93
93
  description: `${A("conversations.empty.search.description", "No conversations matching")} "${L}"`,
94
- icon: /* @__PURE__ */ b(h, {
94
+ icon: /* @__PURE__ */ y(g, {
95
95
  size: 28,
96
96
  className: "text-text-secondary"
97
- })
97
+ }),
98
+ illustration: "empty-search"
98
99
  };
99
100
  if (Q) return {
100
101
  title: A("conversations.empty.archived.title", "No archived conversations"),
101
102
  description: A("conversations.empty.archived.description", "Archived conversations are hidden from your main inbox. Move conversations here from the row actions menu."),
102
- icon: /* @__PURE__ */ b(p, {
103
+ icon: /* @__PURE__ */ y(m, {
103
104
  size: 28,
104
105
  className: "text-text-secondary"
105
- })
106
+ }),
107
+ illustration: "empty-files"
106
108
  };
107
- switch (d) {
109
+ switch (f) {
108
110
  case "direct": return {
109
111
  title: A("conversations.empty.direct.title", "No direct conversations yet"),
110
112
  description: A("conversations.empty.direct.description", "Start one with a workspace member from the New conversation button above."),
111
- icon: /* @__PURE__ */ b(h, {
113
+ icon: /* @__PURE__ */ y(g, {
112
114
  size: 28,
113
115
  className: "text-text-secondary"
114
- })
116
+ }),
117
+ illustration: "empty-vibes"
115
118
  };
116
119
  case "group": return {
117
120
  title: A("conversations.empty.groups.title", "Create a group"),
118
121
  description: A("conversations.empty.groups.description", "Groups let you chat with multiple people in one thread. Tap New to start one."),
119
- icon: /* @__PURE__ */ b(_, {
122
+ icon: /* @__PURE__ */ y(re, {
120
123
  size: 28,
121
124
  className: "text-text-secondary"
122
- })
125
+ }),
126
+ illustration: "empty-people"
123
127
  };
124
128
  case "channel": return {
125
129
  title: A("conversations.empty.channels.title", "Subscribe to a channel"),
126
130
  description: A("conversations.empty.channels.description", "Channels are topic-based discussions visible to everyone who joins them."),
127
- icon: /* @__PURE__ */ b(m, {
131
+ icon: /* @__PURE__ */ y(h, {
128
132
  size: 28,
129
133
  className: "text-text-secondary"
130
- })
134
+ }),
135
+ illustration: "empty-vibes"
131
136
  };
132
137
  default: return {
133
138
  title: A("conversations.empty.all.title", "No conversations yet"),
134
139
  description: A("conversations.empty.all.description", "Start your first conversation with someone in this workspace."),
135
- icon: /* @__PURE__ */ b(h, {
140
+ icon: /* @__PURE__ */ y(g, {
136
141
  size: 28,
137
142
  className: "text-text-secondary"
138
- })
143
+ }),
144
+ illustration: "empty-vibes"
139
145
  };
140
146
  }
141
147
  }, [
142
- d,
148
+ f,
143
149
  Q,
144
150
  R,
145
151
  A,
146
152
  L
147
153
  ]);
148
- return T ? /* @__PURE__ */ b(o, {
154
+ return T ? /* @__PURE__ */ y(o, {
149
155
  permission: s.CONVERSATION_READ,
150
156
  allowUnavailablePermissions: !0,
151
- children: /* @__PURE__ */ b("div", {
157
+ children: /* @__PURE__ */ y("div", {
152
158
  "data-tour": "conversation-list",
153
159
  className: "flex h-full min-h-0 w-full flex-col",
154
160
  "aria-live": "polite",
155
- children: X ? /* @__PURE__ */ b("div", {
161
+ children: X ? /* @__PURE__ */ y("div", {
156
162
  role: "status",
157
163
  className: "px-4 py-8 text-sm text-text-secondary",
158
164
  children: A("conversations.loading", "Loading conversations...")
159
- }) : Z ? /* @__PURE__ */ b(S, {
160
- icon: /* @__PURE__ */ b(f, {
165
+ }) : Z ? /* @__PURE__ */ y(x, {
166
+ icon: /* @__PURE__ */ y(p, {
161
167
  size: 28,
162
168
  className: "text-status-error-text",
163
169
  "aria-hidden": "true"
164
170
  }),
165
171
  title: A("conversations.couldNotLoad", "Could not load conversations"),
166
172
  description: Z.message
167
- }) : K.length === 0 ? /* @__PURE__ */ b(S, {
168
- icon: $.icon,
173
+ }) : K.length === 0 ? /* @__PURE__ */ y(S, {
174
+ illustration: $.illustration,
169
175
  title: $.title,
170
176
  description: $.description
171
- }) : /* @__PURE__ */ x("div", {
177
+ }) : /* @__PURE__ */ b("div", {
172
178
  "data-tour": "conversation-list-results",
173
179
  className: "flex flex-col gap-0.5 px-2 py-2",
174
- children: [q.length > 0 && /* @__PURE__ */ x(y, { children: [
175
- /* @__PURE__ */ x("div", {
180
+ children: [q.length > 0 && /* @__PURE__ */ b(v, { children: [
181
+ /* @__PURE__ */ b("div", {
176
182
  className: "flex items-center gap-1.5 px-2 pt-2 pb-1",
177
- children: [/* @__PURE__ */ b(g, {
183
+ children: [/* @__PURE__ */ y(ne, {
178
184
  size: 11,
179
185
  className: "text-text-secondary",
180
186
  "aria-hidden": "true"
181
- }), /* @__PURE__ */ b("span", {
187
+ }), /* @__PURE__ */ y("span", {
182
188
  className: "text-[10px] font-semibold uppercase tracking-wider text-text-secondary",
183
189
  children: A("conversations.pinned", "Pinned")
184
190
  })]
185
191
  }),
186
- q.map((e) => /* @__PURE__ */ b(c, {
192
+ q.map((e) => /* @__PURE__ */ y(c, {
187
193
  conversation: e,
188
194
  isActive: N === e.id,
189
195
  onClick: Y
190
196
  }, e.id)),
191
- J.length > 0 && /* @__PURE__ */ b("div", {
197
+ J.length > 0 && /* @__PURE__ */ y("div", {
192
198
  className: "flex items-center gap-1.5 px-2 pt-3 pb-1",
193
- children: /* @__PURE__ */ b("span", {
199
+ children: /* @__PURE__ */ y("span", {
194
200
  className: "text-[10px] font-semibold uppercase tracking-wider text-text-secondary",
195
201
  children: A("conversations.tabs.all", "All")
196
202
  })
197
203
  })
198
- ] }), J.map((e) => /* @__PURE__ */ b(c, {
204
+ ] }), J.map((e) => /* @__PURE__ */ y(c, {
199
205
  conversation: e,
200
206
  isActive: N === e.id,
201
207
  onClick: Y
202
208
  }, e.id))]
203
209
  })
204
210
  })
205
- }) : /* @__PURE__ */ b(o, {
211
+ }) : /* @__PURE__ */ y(o, {
206
212
  permission: s.CONVERSATION_READ,
207
213
  allowUnavailablePermissions: !0,
208
- children: /* @__PURE__ */ b("div", {
214
+ children: /* @__PURE__ */ y("div", {
209
215
  "data-tour": "conversation-list-standalone",
210
216
  className: "flex h-full min-h-0 w-full flex-col bg-bg-surface",
211
- children: X ? /* @__PURE__ */ b("div", {
217
+ children: X ? /* @__PURE__ */ y("div", {
212
218
  className: "px-4 py-8 text-sm text-text-secondary",
213
219
  children: A("conversations.loading", "Loading conversations...")
214
- }) : Z ? /* @__PURE__ */ b(S, {
215
- icon: /* @__PURE__ */ b(f, {
220
+ }) : Z ? /* @__PURE__ */ y(x, {
221
+ icon: /* @__PURE__ */ y(p, {
216
222
  size: 28,
217
223
  className: "text-status-error-text",
218
224
  "aria-hidden": "true"
219
225
  }),
220
226
  title: A("conversations.couldNotLoad", "Could not load conversations"),
221
227
  description: Z.message
222
- }) : K.length === 0 ? /* @__PURE__ */ b(S, {
223
- icon: $.icon,
228
+ }) : K.length === 0 ? /* @__PURE__ */ y(S, {
229
+ illustration: $.illustration,
224
230
  title: $.title,
225
231
  description: $.description
226
- }) : /* @__PURE__ */ b("div", {
232
+ }) : /* @__PURE__ */ y("div", {
227
233
  className: "flex flex-col gap-0.5 px-2 py-2",
228
- children: K.map((e) => /* @__PURE__ */ b(c, {
234
+ children: K.map((e) => /* @__PURE__ */ y(c, {
229
235
  conversation: e,
230
236
  isActive: N === e.id,
231
237
  onClick: Y
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationListPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationListPage.tsx"],"sourcesContent":["import {\n useDeferredValue,\n useEffect,\n useMemo,\n useState,\n useCallback,\n} from \"react\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport { useNavigate, useSearchParams } from \"react-router-dom\";\nimport { EmptyState } from \"@burdenoff/fe-libs/ui\";\nimport {\n Archive,\n AlertCircle,\n Hash,\n MessageCircle,\n Pin,\n Users,\n} from \"lucide-react\";\nimport { ConversationRow } from \"../components/ConversationRow\";\nimport {\n useSearchConversations,\n useGetConversations,\n} from \"../hooks/useConversationQueries\";\nimport { useConversationStream } from \"../hooks/useMessageSubscriptions\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport type {\n Conversation,\n ConversationFilter,\n ConversationState,\n} from \"../types\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { useTr } from \"../utils/i18n\";\nimport { joinPath } from \"../utils/navigation\";\n\n// Default filter shape applied whenever the `?filter=` URL param is absent\n// or unrecognised. Covers every dimension on `ConversationFilter` except\n// `searchQuery` — the text search is independent of URL-driven filters and\n// must not be wiped when the URL flips between filter values.\n//\n// The boolean toggles (hasUnread / isPinned / isMuted) are explicitly set\n// to `undefined` here (rather than omitted). Zustand's `set` merges\n// shallowly — the store's `updateConversationFilter` does\n// `{ ...currentFilter, ...partial }` — so an omitted key would preserve the\n// previous value, leaving a toggle like `hasUnread: true` sticky across a\n// URL that drops the filter param. Typing the constant as\n// `Partial<ConversationFilter>` widens the type so the explicit\n// `undefined` entries are accepted as `boolean | undefined` rather than\n// narrowed to the literal `undefined` under `as const`.\nconst FILTER_DEFAULTS: Partial<ConversationFilter> = {\n state: \"all\",\n type: \"all\",\n visibility: \"all\",\n hasUnread: undefined,\n isPinned: undefined,\n isMuted: undefined,\n};\n\nfunction useDebouncedValue<T>(value: T, delayMs: number): T {\n const [debouncedValue, setDebouncedValue] = useState(value);\n\n useEffect(() => {\n const timeoutId = window.setTimeout(\n () => setDebouncedValue(value),\n delayMs,\n );\n return () => window.clearTimeout(timeoutId);\n }, [delayMs, value]);\n\n return debouncedValue;\n}\n\nexport interface ConversationListPageProps {\n /**\n * Filter dimension forced by the parent inbox shell (driven by the\n * currently active tab). Mutually exclusive with the `?filter=` URL\n * param. When set, the URL param is ignored and the store filter is\n * driven by the prop on every change.\n */\n forcedFilterParam?: \"channel\" | \"direct\" | \"group\" | \"archived\";\n /**\n * When true the page renders bare-list chrome (no own header/search/\n * filter chips) so it can be embedded inside `InboxLayout`. Defaults to\n * false for backwards compatibility with the legacy `/conversations/list`\n * route.\n */\n embedded?: boolean;\n}\n\nexport function ConversationListPage({\n forcedFilterParam,\n embedded = false,\n}: ConversationListPageProps = {}) {\n const navigate = useNavigate();\n const { basePath, onConversationSelect } = useConversations();\n const bus = useEventBus();\n const tr = useTr();\n const {\n searchQuery,\n conversationFilter,\n updateConversationFilter,\n activeConversationId,\n setActiveConversationId,\n } = useConversationsStore();\n const [searchParams] = useSearchParams();\n const debouncedSearchQuery = useDebouncedValue(searchQuery, 250);\n const deferredSearchQuery = useDeferredValue(debouncedSearchQuery);\n const trimmedSearchQuery = deferredSearchQuery.trim();\n const isSearchActive = trimmedSearchQuery.length >= 2;\n\n // Sync URL filter param (?filter=channel|direct|group|archived) to the store\n // on mount / URL change. When the param is absent or unrecognised, reset\n // every filter dimension to its module-level default (`FILTER_DEFAULTS`) —\n // but deliberately do NOT touch `searchQuery`. The list's text search is\n // independent of URL-driven filters; zeroing it here would wipe in-progress\n // searches every time the URL flips between filter values or drops the\n // param entirely.\n useEffect(() => {\n const filterParam = forcedFilterParam ?? searchParams.get(\"filter\");\n\n if (!filterParam) {\n updateConversationFilter(FILTER_DEFAULTS);\n return;\n }\n\n const filterMap: Record<string, string> = {\n channel: \"CHANNEL\",\n direct: \"DIRECT\",\n group: \"GROUP\",\n archived: \"ARCHIVED\",\n };\n\n const mapped = filterMap[filterParam.toLowerCase()];\n if (!mapped) {\n updateConversationFilter(FILTER_DEFAULTS);\n return;\n }\n\n if (mapped === \"ARCHIVED\") {\n updateConversationFilter({ ...FILTER_DEFAULTS, state: \"ARCHIVED\" });\n } else {\n updateConversationFilter({\n ...FILTER_DEFAULTS,\n type: mapped as \"CHANNEL\" | \"DIRECT\" | \"GROUP\",\n });\n }\n }, [forcedFilterParam, searchParams, updateConversationFilter]);\n\n // Map filter.state (\"all\" | \"ACTIVE\" | \"ARCHIVED\" | \"DELETED\") to the\n // GraphQL `states: [ConversationState!]` filter. When the user is on the\n // Archived tab we send `states: [ARCHIVED]`; otherwise we omit the filter\n // so the backend default (ACTIVE only) kicks in.\n const stateFilter = useMemo<ConversationState[] | undefined>(() => {\n if (\n conversationFilter.state === \"all\" ||\n conversationFilter.state === \"ACTIVE\"\n ) {\n return undefined;\n }\n return [conversationFilter.state as ConversationState];\n }, [conversationFilter.state]);\n\n const {\n conversations: baseConversations,\n loading,\n error,\n } = useGetConversations({\n first: 50,\n types:\n conversationFilter.type === \"all\" ? undefined : [conversationFilter.type],\n states: stateFilter,\n hasUnread: conversationFilter.hasUnread,\n isPinned: conversationFilter.isPinned,\n });\n\n // Live conversation lifecycle stream — keeps the list current without polling.\n // Apollo cache updates are handled inside the hook (append on add, evict on\n // delete, normalisation handles updates).\n useConversationStream();\n\n const {\n conversations: searchedConversations,\n loading: searchLoading,\n error: searchError,\n } = useSearchConversations(deferredSearchQuery);\n\n const conversations = isSearchActive\n ? searchedConversations\n : baseConversations;\n\n // When the BE is queried with states:[ARCHIVED] the inbox tab is \"archived\",\n // so we skip the client-side state filter to avoid double-filtering.\n // For the inbox view the BE returns ACTIVE by default; no extra filter\n // needed here either.\n const filteredConversations = conversations;\n\n const pinnedConversations = useMemo(\n () => filteredConversations.filter((conversation) => conversation.isPinned),\n [filteredConversations],\n );\n\n const unpinnedConversations = useMemo(\n () =>\n filteredConversations.filter((conversation) => !conversation.isPinned),\n [filteredConversations],\n );\n\n const handleConversationClick = useCallback(\n (conversation: Conversation) => {\n setActiveConversationId(conversation.id);\n onConversationSelect?.(conversation);\n bus.emit(\"conversations.conversation.opened\", {\n conversationId: conversation.id,\n conversationType: conversation.type,\n });\n\n if (conversation.sourceContext?.contextHref) {\n try {\n const url = new URL(conversation.sourceContext.contextHref);\n if (url.origin === window.location.origin) {\n navigate(`${url.pathname}${url.search}${url.hash}`);\n return;\n }\n } catch {\n // Fall back to conversation detail below.\n }\n }\n\n navigate(joinPath(basePath || \"/conversations\", conversation.id));\n },\n [basePath, bus, navigate, onConversationSelect, setActiveConversationId],\n );\n\n const isLoading = loading || searchLoading;\n const activeError = error ?? searchError;\n const isArchivedTab =\n forcedFilterParam === \"archived\" || conversationFilter.state === \"ARCHIVED\";\n\n // Empty-state copy is tab-aware so users understand why a list is blank.\n const emptyState = useMemo(() => {\n if (isSearchActive) {\n return {\n title: tr(\"conversations.empty.search.title\", \"No matches\"),\n description: `${tr(\n \"conversations.empty.search.description\",\n \"No conversations matching\",\n )} \"${trimmedSearchQuery}\"`,\n icon: <MessageCircle size={28} className=\"text-text-secondary\" />,\n };\n }\n if (isArchivedTab) {\n return {\n title: tr(\n \"conversations.empty.archived.title\",\n \"No archived conversations\",\n ),\n description: tr(\n \"conversations.empty.archived.description\",\n \"Archived conversations are hidden from your main inbox. Move conversations here from the row actions menu.\",\n ),\n icon: <Archive size={28} className=\"text-text-secondary\" />,\n };\n }\n switch (forcedFilterParam) {\n case \"direct\":\n return {\n title: tr(\n \"conversations.empty.direct.title\",\n \"No direct conversations yet\",\n ),\n description: tr(\n \"conversations.empty.direct.description\",\n \"Start one with a workspace member from the New conversation button above.\",\n ),\n icon: <MessageCircle size={28} className=\"text-text-secondary\" />,\n };\n case \"group\":\n return {\n title: tr(\"conversations.empty.groups.title\", \"Create a group\"),\n description: tr(\n \"conversations.empty.groups.description\",\n \"Groups let you chat with multiple people in one thread. Tap New to start one.\",\n ),\n icon: <Users size={28} className=\"text-text-secondary\" />,\n };\n case \"channel\":\n return {\n title: tr(\n \"conversations.empty.channels.title\",\n \"Subscribe to a channel\",\n ),\n description: tr(\n \"conversations.empty.channels.description\",\n \"Channels are topic-based discussions visible to everyone who joins them.\",\n ),\n icon: <Hash size={28} className=\"text-text-secondary\" />,\n };\n default:\n return {\n title: tr(\"conversations.empty.all.title\", \"No conversations yet\"),\n description: tr(\n \"conversations.empty.all.description\",\n \"Start your first conversation with someone in this workspace.\",\n ),\n icon: <MessageCircle size={28} className=\"text-text-secondary\" />,\n };\n }\n }, [\n forcedFilterParam,\n isArchivedTab,\n isSearchActive,\n tr,\n trimmedSearchQuery,\n ]);\n\n // ─── Embedded mode: bare list, no own chrome (used inside InboxLayout) ───\n if (embedded) {\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <div\n data-tour=\"conversation-list\"\n className=\"flex h-full min-h-0 w-full flex-col\"\n aria-live=\"polite\"\n >\n {isLoading ? (\n <div\n role=\"status\"\n className=\"px-4 py-8 text-sm text-text-secondary\"\n >\n {tr(\"conversations.loading\", \"Loading conversations...\")}\n </div>\n ) : activeError ? (\n <EmptyState\n icon={\n <AlertCircle\n size={28}\n className=\"text-status-error-text\"\n aria-hidden=\"true\"\n />\n }\n title={tr(\n \"conversations.couldNotLoad\",\n \"Could not load conversations\",\n )}\n description={activeError.message}\n />\n ) : filteredConversations.length === 0 ? (\n <EmptyState\n icon={emptyState.icon}\n title={emptyState.title}\n description={emptyState.description}\n />\n ) : (\n <div\n data-tour=\"conversation-list-results\"\n className=\"flex flex-col gap-0.5 px-2 py-2\"\n >\n {pinnedConversations.length > 0 && (\n <>\n <div className=\"flex items-center gap-1.5 px-2 pt-2 pb-1\">\n <Pin\n size={11}\n className=\"text-text-secondary\"\n aria-hidden=\"true\"\n />\n <span className=\"text-[10px] font-semibold uppercase tracking-wider text-text-secondary\">\n {tr(\"conversations.pinned\", \"Pinned\")}\n </span>\n </div>\n {pinnedConversations.map((conversation) => (\n <ConversationRow\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n {unpinnedConversations.length > 0 && (\n <div className=\"flex items-center gap-1.5 px-2 pt-3 pb-1\">\n <span className=\"text-[10px] font-semibold uppercase tracking-wider text-text-secondary\">\n {tr(\"conversations.tabs.all\", \"All\")}\n </span>\n </div>\n )}\n </>\n )}\n {unpinnedConversations.map((conversation) => (\n <ConversationRow\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n </div>\n )}\n </div>\n </PermissionGuard>\n );\n }\n\n // ─── Standalone mode (legacy /conversations/list route) ───\n // Retains the original look but uses semantic tokens.\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <div\n data-tour=\"conversation-list-standalone\"\n className=\"flex h-full min-h-0 w-full flex-col bg-bg-surface\"\n >\n {isLoading ? (\n <div className=\"px-4 py-8 text-sm text-text-secondary\">\n {tr(\"conversations.loading\", \"Loading conversations...\")}\n </div>\n ) : activeError ? (\n <EmptyState\n icon={\n <AlertCircle\n size={28}\n className=\"text-status-error-text\"\n aria-hidden=\"true\"\n />\n }\n title={tr(\n \"conversations.couldNotLoad\",\n \"Could not load conversations\",\n )}\n description={activeError.message}\n />\n ) : filteredConversations.length === 0 ? (\n <EmptyState\n icon={emptyState.icon}\n title={emptyState.title}\n description={emptyState.description}\n />\n ) : (\n <div className=\"flex flex-col gap-0.5 px-2 py-2\">\n {filteredConversations.map((conversation) => (\n <ConversationRow\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n </div>\n )}\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ConversationListPage.js","names":[],"sources":["../../../src/conversations/pages/ConversationListPage.tsx"],"sourcesContent":["import {\n useDeferredValue,\n useEffect,\n useMemo,\n useState,\n useCallback,\n} from \"react\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport { useNavigate, useSearchParams } from \"react-router-dom\";\nimport { EmptyState, IllustratedEmptyState } from \"@burdenoff/fe-libs/ui\";\nimport {\n Archive,\n AlertCircle,\n Hash,\n MessageCircle,\n Pin,\n Users,\n} from \"lucide-react\";\nimport { ConversationRow } from \"../components/ConversationRow\";\nimport {\n useSearchConversations,\n useGetConversations,\n} from \"../hooks/useConversationQueries\";\nimport { useConversationStream } from \"../hooks/useMessageSubscriptions\";\nimport { useConversations } from \"../providers/ConversationsProvider\";\nimport { useConversationsStore } from \"../store/conversationsStore\";\nimport type {\n Conversation,\n ConversationFilter,\n ConversationState,\n} from \"../types\";\nimport { PermissionGuard } from \"../components/PermissionGuard\";\nimport { CONVERSATIONS_PERMISSIONS } from \"../constants/permissions\";\nimport { useTr } from \"../utils/i18n\";\nimport { joinPath } from \"../utils/navigation\";\n\n// Default filter shape applied whenever the `?filter=` URL param is absent\n// or unrecognised. Covers every dimension on `ConversationFilter` except\n// `searchQuery` — the text search is independent of URL-driven filters and\n// must not be wiped when the URL flips between filter values.\n//\n// The boolean toggles (hasUnread / isPinned / isMuted) are explicitly set\n// to `undefined` here (rather than omitted). Zustand's `set` merges\n// shallowly — the store's `updateConversationFilter` does\n// `{ ...currentFilter, ...partial }` — so an omitted key would preserve the\n// previous value, leaving a toggle like `hasUnread: true` sticky across a\n// URL that drops the filter param. Typing the constant as\n// `Partial<ConversationFilter>` widens the type so the explicit\n// `undefined` entries are accepted as `boolean | undefined` rather than\n// narrowed to the literal `undefined` under `as const`.\nconst FILTER_DEFAULTS: Partial<ConversationFilter> = {\n state: \"all\",\n type: \"all\",\n visibility: \"all\",\n hasUnread: undefined,\n isPinned: undefined,\n isMuted: undefined,\n};\n\nfunction useDebouncedValue<T>(value: T, delayMs: number): T {\n const [debouncedValue, setDebouncedValue] = useState(value);\n\n useEffect(() => {\n const timeoutId = window.setTimeout(\n () => setDebouncedValue(value),\n delayMs,\n );\n return () => window.clearTimeout(timeoutId);\n }, [delayMs, value]);\n\n return debouncedValue;\n}\n\nexport interface ConversationListPageProps {\n /**\n * Filter dimension forced by the parent inbox shell (driven by the\n * currently active tab). Mutually exclusive with the `?filter=` URL\n * param. When set, the URL param is ignored and the store filter is\n * driven by the prop on every change.\n */\n forcedFilterParam?: \"channel\" | \"direct\" | \"group\" | \"archived\";\n /**\n * When true the page renders bare-list chrome (no own header/search/\n * filter chips) so it can be embedded inside `InboxLayout`. Defaults to\n * false for backwards compatibility with the legacy `/conversations/list`\n * route.\n */\n embedded?: boolean;\n}\n\nexport function ConversationListPage({\n forcedFilterParam,\n embedded = false,\n}: ConversationListPageProps = {}) {\n const navigate = useNavigate();\n const { basePath, onConversationSelect } = useConversations();\n const bus = useEventBus();\n const tr = useTr();\n const {\n searchQuery,\n conversationFilter,\n updateConversationFilter,\n activeConversationId,\n setActiveConversationId,\n } = useConversationsStore();\n const [searchParams] = useSearchParams();\n const debouncedSearchQuery = useDebouncedValue(searchQuery, 250);\n const deferredSearchQuery = useDeferredValue(debouncedSearchQuery);\n const trimmedSearchQuery = deferredSearchQuery.trim();\n const isSearchActive = trimmedSearchQuery.length >= 2;\n\n // Sync URL filter param (?filter=channel|direct|group|archived) to the store\n // on mount / URL change. When the param is absent or unrecognised, reset\n // every filter dimension to its module-level default (`FILTER_DEFAULTS`) —\n // but deliberately do NOT touch `searchQuery`. The list's text search is\n // independent of URL-driven filters; zeroing it here would wipe in-progress\n // searches every time the URL flips between filter values or drops the\n // param entirely.\n useEffect(() => {\n const filterParam = forcedFilterParam ?? searchParams.get(\"filter\");\n\n if (!filterParam) {\n updateConversationFilter(FILTER_DEFAULTS);\n return;\n }\n\n const filterMap: Record<string, string> = {\n channel: \"CHANNEL\",\n direct: \"DIRECT\",\n group: \"GROUP\",\n archived: \"ARCHIVED\",\n };\n\n const mapped = filterMap[filterParam.toLowerCase()];\n if (!mapped) {\n updateConversationFilter(FILTER_DEFAULTS);\n return;\n }\n\n if (mapped === \"ARCHIVED\") {\n updateConversationFilter({ ...FILTER_DEFAULTS, state: \"ARCHIVED\" });\n } else {\n updateConversationFilter({\n ...FILTER_DEFAULTS,\n type: mapped as \"CHANNEL\" | \"DIRECT\" | \"GROUP\",\n });\n }\n }, [forcedFilterParam, searchParams, updateConversationFilter]);\n\n // Map filter.state (\"all\" | \"ACTIVE\" | \"ARCHIVED\" | \"DELETED\") to the\n // GraphQL `states: [ConversationState!]` filter. When the user is on the\n // Archived tab we send `states: [ARCHIVED]`; otherwise we omit the filter\n // so the backend default (ACTIVE only) kicks in.\n const stateFilter = useMemo<ConversationState[] | undefined>(() => {\n if (\n conversationFilter.state === \"all\" ||\n conversationFilter.state === \"ACTIVE\"\n ) {\n return undefined;\n }\n return [conversationFilter.state as ConversationState];\n }, [conversationFilter.state]);\n\n const {\n conversations: baseConversations,\n loading,\n error,\n } = useGetConversations({\n first: 50,\n types:\n conversationFilter.type === \"all\" ? undefined : [conversationFilter.type],\n states: stateFilter,\n hasUnread: conversationFilter.hasUnread,\n isPinned: conversationFilter.isPinned,\n });\n\n // Live conversation lifecycle stream — keeps the list current without polling.\n // Apollo cache updates are handled inside the hook (append on add, evict on\n // delete, normalisation handles updates).\n useConversationStream();\n\n const {\n conversations: searchedConversations,\n loading: searchLoading,\n error: searchError,\n } = useSearchConversations(deferredSearchQuery);\n\n const conversations = isSearchActive\n ? searchedConversations\n : baseConversations;\n\n // When the BE is queried with states:[ARCHIVED] the inbox tab is \"archived\",\n // so we skip the client-side state filter to avoid double-filtering.\n // For the inbox view the BE returns ACTIVE by default; no extra filter\n // needed here either.\n const filteredConversations = conversations;\n\n const pinnedConversations = useMemo(\n () => filteredConversations.filter((conversation) => conversation.isPinned),\n [filteredConversations],\n );\n\n const unpinnedConversations = useMemo(\n () =>\n filteredConversations.filter((conversation) => !conversation.isPinned),\n [filteredConversations],\n );\n\n const handleConversationClick = useCallback(\n (conversation: Conversation) => {\n setActiveConversationId(conversation.id);\n onConversationSelect?.(conversation);\n bus.emit(\"conversations.conversation.opened\", {\n conversationId: conversation.id,\n conversationType: conversation.type,\n });\n\n if (conversation.sourceContext?.contextHref) {\n try {\n const url = new URL(conversation.sourceContext.contextHref);\n if (url.origin === window.location.origin) {\n navigate(`${url.pathname}${url.search}${url.hash}`);\n return;\n }\n } catch {\n // Fall back to conversation detail below.\n }\n }\n\n navigate(joinPath(basePath || \"/conversations\", conversation.id));\n },\n [basePath, bus, navigate, onConversationSelect, setActiveConversationId],\n );\n\n const isLoading = loading || searchLoading;\n const activeError = error ?? searchError;\n const isArchivedTab =\n forcedFilterParam === \"archived\" || conversationFilter.state === \"ARCHIVED\";\n\n // Empty-state copy is tab-aware so users understand why a list is blank.\n const emptyState = useMemo(() => {\n if (isSearchActive) {\n return {\n title: tr(\"conversations.empty.search.title\", \"No matches\"),\n description: `${tr(\n \"conversations.empty.search.description\",\n \"No conversations matching\",\n )} \"${trimmedSearchQuery}\"`,\n icon: <MessageCircle size={28} className=\"text-text-secondary\" />,\n illustration: \"empty-search\",\n };\n }\n if (isArchivedTab) {\n return {\n title: tr(\n \"conversations.empty.archived.title\",\n \"No archived conversations\",\n ),\n description: tr(\n \"conversations.empty.archived.description\",\n \"Archived conversations are hidden from your main inbox. Move conversations here from the row actions menu.\",\n ),\n icon: <Archive size={28} className=\"text-text-secondary\" />,\n illustration: \"empty-files\",\n };\n }\n switch (forcedFilterParam) {\n case \"direct\":\n return {\n title: tr(\n \"conversations.empty.direct.title\",\n \"No direct conversations yet\",\n ),\n description: tr(\n \"conversations.empty.direct.description\",\n \"Start one with a workspace member from the New conversation button above.\",\n ),\n icon: <MessageCircle size={28} className=\"text-text-secondary\" />,\n illustration: \"empty-vibes\",\n };\n case \"group\":\n return {\n title: tr(\"conversations.empty.groups.title\", \"Create a group\"),\n description: tr(\n \"conversations.empty.groups.description\",\n \"Groups let you chat with multiple people in one thread. Tap New to start one.\",\n ),\n icon: <Users size={28} className=\"text-text-secondary\" />,\n illustration: \"empty-people\",\n };\n case \"channel\":\n return {\n title: tr(\n \"conversations.empty.channels.title\",\n \"Subscribe to a channel\",\n ),\n description: tr(\n \"conversations.empty.channels.description\",\n \"Channels are topic-based discussions visible to everyone who joins them.\",\n ),\n icon: <Hash size={28} className=\"text-text-secondary\" />,\n illustration: \"empty-vibes\",\n };\n default:\n return {\n title: tr(\"conversations.empty.all.title\", \"No conversations yet\"),\n description: tr(\n \"conversations.empty.all.description\",\n \"Start your first conversation with someone in this workspace.\",\n ),\n icon: <MessageCircle size={28} className=\"text-text-secondary\" />,\n illustration: \"empty-vibes\",\n };\n }\n }, [\n forcedFilterParam,\n isArchivedTab,\n isSearchActive,\n tr,\n trimmedSearchQuery,\n ]);\n\n // ─── Embedded mode: bare list, no own chrome (used inside InboxLayout) ───\n if (embedded) {\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <div\n data-tour=\"conversation-list\"\n className=\"flex h-full min-h-0 w-full flex-col\"\n aria-live=\"polite\"\n >\n {isLoading ? (\n <div\n role=\"status\"\n className=\"px-4 py-8 text-sm text-text-secondary\"\n >\n {tr(\"conversations.loading\", \"Loading conversations...\")}\n </div>\n ) : activeError ? (\n <EmptyState\n icon={\n <AlertCircle\n size={28}\n className=\"text-status-error-text\"\n aria-hidden=\"true\"\n />\n }\n title={tr(\n \"conversations.couldNotLoad\",\n \"Could not load conversations\",\n )}\n description={activeError.message}\n />\n ) : filteredConversations.length === 0 ? (\n <IllustratedEmptyState\n illustration={emptyState.illustration}\n title={emptyState.title}\n description={emptyState.description}\n />\n ) : (\n <div\n data-tour=\"conversation-list-results\"\n className=\"flex flex-col gap-0.5 px-2 py-2\"\n >\n {pinnedConversations.length > 0 && (\n <>\n <div className=\"flex items-center gap-1.5 px-2 pt-2 pb-1\">\n <Pin\n size={11}\n className=\"text-text-secondary\"\n aria-hidden=\"true\"\n />\n <span className=\"text-[10px] font-semibold uppercase tracking-wider text-text-secondary\">\n {tr(\"conversations.pinned\", \"Pinned\")}\n </span>\n </div>\n {pinnedConversations.map((conversation) => (\n <ConversationRow\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n {unpinnedConversations.length > 0 && (\n <div className=\"flex items-center gap-1.5 px-2 pt-3 pb-1\">\n <span className=\"text-[10px] font-semibold uppercase tracking-wider text-text-secondary\">\n {tr(\"conversations.tabs.all\", \"All\")}\n </span>\n </div>\n )}\n </>\n )}\n {unpinnedConversations.map((conversation) => (\n <ConversationRow\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n </div>\n )}\n </div>\n </PermissionGuard>\n );\n }\n\n // ─── Standalone mode (legacy /conversations/list route) ───\n // Retains the original look but uses semantic tokens.\n return (\n <PermissionGuard\n permission={CONVERSATIONS_PERMISSIONS.CONVERSATION_READ}\n allowUnavailablePermissions\n >\n <div\n data-tour=\"conversation-list-standalone\"\n className=\"flex h-full min-h-0 w-full flex-col bg-bg-surface\"\n >\n {isLoading ? (\n <div className=\"px-4 py-8 text-sm text-text-secondary\">\n {tr(\"conversations.loading\", \"Loading conversations...\")}\n </div>\n ) : activeError ? (\n <EmptyState\n icon={\n <AlertCircle\n size={28}\n className=\"text-status-error-text\"\n aria-hidden=\"true\"\n />\n }\n title={tr(\n \"conversations.couldNotLoad\",\n \"Could not load conversations\",\n )}\n description={activeError.message}\n />\n ) : filteredConversations.length === 0 ? (\n <IllustratedEmptyState\n illustration={emptyState.illustration}\n title={emptyState.title}\n description={emptyState.description}\n />\n ) : (\n <div className=\"flex flex-col gap-0.5 px-2 py-2\">\n {filteredConversations.map((conversation) => (\n <ConversationRow\n key={conversation.id}\n conversation={conversation}\n isActive={activeConversationId === conversation.id}\n onClick={handleConversationClick}\n />\n ))}\n </div>\n )}\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}