@ai-matrx/messaging 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.cjs CHANGED
@@ -869,7 +869,7 @@ function projectMessage(row, fallbackOrganizationId) {
869
869
  id: requiredStr(row, "id", operation),
870
870
  conversationId: requiredStr(row, "conversation_id", operation),
871
871
  senderId: requiredStr(row, "sender_id", operation),
872
- organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
872
+ organizationId: str(row, "organization_id") ?? fallbackOrganizationId ?? "",
873
873
  content: typeof row["content"] === "string" ? row["content"] : "",
874
874
  kind: MESSAGE_KINDS.has(kindRaw) ? kindRaw : "text",
875
875
  // A row read from the DB is at least `sent`. `sending`/`failed` are outbox
@@ -917,7 +917,7 @@ function projectConversationSummary(row, viewerId, fallbackOrganizationId) {
917
917
  groupName,
918
918
  groupImageUrl,
919
919
  createdBy: createdBy === null ? null : createdBy,
920
- organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
920
+ organizationId: str(row, "organization_id") ?? fallbackOrganizationId ?? "",
921
921
  createdAt,
922
922
  updatedAt,
923
923
  metadata: jsonObject(row["metadata"])
@@ -983,6 +983,7 @@ function createMessagingStore() {
983
983
  let threads = /* @__PURE__ */ new Map();
984
984
  let activeConversationId = null;
985
985
  let conversationReadFailures = /* @__PURE__ */ new Map();
986
+ let conversationsLoadError = null;
986
987
  const listeners = /* @__PURE__ */ new Set();
987
988
  let cached = null;
988
989
  function snapshot() {
@@ -996,7 +997,8 @@ function createMessagingStore() {
996
997
  totalUnreadConversations: conversations.filter((item) => item.unreadCount > 0).length,
997
998
  archiveFilter,
998
999
  archivedCount,
999
- conversationReadFailures
1000
+ conversationReadFailures,
1001
+ conversationsLoadError
1000
1002
  };
1001
1003
  return cached;
1002
1004
  }
@@ -1041,6 +1043,12 @@ function createMessagingStore() {
1041
1043
  conversations = normalizeConversations(items);
1042
1044
  hasMoreConversations = hasMore;
1043
1045
  hasLoadedConversations = true;
1046
+ conversationsLoadError = null;
1047
+ emit();
1048
+ },
1049
+ setConversationsLoadError(message) {
1050
+ if (message === conversationsLoadError) return;
1051
+ conversationsLoadError = message;
1044
1052
  emit();
1045
1053
  },
1046
1054
  setArchiveFilter(next) {
@@ -1250,7 +1258,11 @@ function createMessagingEngine(options) {
1250
1258
  optimisticMessage({
1251
1259
  conversationId: entry.draft.conversationId,
1252
1260
  senderId: identity.userId,
1253
- organizationId: identity.organizationId,
1261
+ // Client-only until the confirmed row replaces it: the active
1262
+ // organization, else the conversation's own.
1263
+ organizationId: identity.organizationId ?? store.snapshot().conversations.find(
1264
+ (item) => item.conversation.id === entry.draft.conversationId
1265
+ )?.conversation.organizationId ?? "",
1254
1266
  content: entry.draft.content,
1255
1267
  clientMessageId: entry.clientMessageId,
1256
1268
  ...entry.draft.kind !== void 0 ? { kind: entry.draft.kind } : {},
@@ -1269,10 +1281,20 @@ function createMessagingEngine(options) {
1269
1281
  }
1270
1282
  async function reloadInbox() {
1271
1283
  if (disposed) return;
1272
- const page = await repository.listConversations({
1273
- limit: conversationPageSize,
1274
- archived: store.snapshot().archiveFilter
1275
- });
1284
+ let page;
1285
+ try {
1286
+ page = await repository.listConversations({
1287
+ limit: conversationPageSize,
1288
+ archived: store.snapshot().archiveFilter
1289
+ });
1290
+ } catch (error) {
1291
+ if (!disposed) {
1292
+ store.setConversationsLoadError(
1293
+ error instanceof Error ? error.message : typeof error === "string" ? error : "The conversation list could not be read."
1294
+ );
1295
+ }
1296
+ throw error;
1297
+ }
1276
1298
  if (disposed) return;
1277
1299
  conversationCursor = page.nextCursor;
1278
1300
  store.setConversations(page.items, page.hasMore);
@@ -1357,6 +1379,13 @@ function createMessagingEngine(options) {
1357
1379
  }
1358
1380
  });
1359
1381
  },
1382
+ async refreshInbox() {
1383
+ try {
1384
+ await reloadInbox();
1385
+ } catch (error) {
1386
+ reportError(error, "inboxRefresh");
1387
+ }
1388
+ },
1360
1389
  async setArchiveFilter(next) {
1361
1390
  if (next === store.snapshot().archiveFilter) return;
1362
1391
  store.setArchiveFilter(next);
@@ -1739,12 +1768,15 @@ var RPCS = {
1739
1768
  };
1740
1769
  var MESSAGE_COLUMNS = "id,conversation_id,sender_id,organization_id,content,message_type,status,reply_to_id,client_message_id,action_data,media_url,media_thumbnail_url,media_metadata,created_at,edited_at,deleted_at,deleted_for_everyone,metadata";
1741
1770
  var CONVERSATION_COLUMNS = "id,type,group_name,group_image_url,created_by,organization_id,created_at,updated_at,metadata";
1771
+ function optionalOrg(organizationId) {
1772
+ return typeof organizationId === "string" && organizationId.length > 0 ? organizationId : null;
1773
+ }
1742
1774
  function requireOrg(organizationId, operation) {
1743
- if (typeof organizationId !== "string" || organizationId.length === 0) {
1775
+ if (organizationId === null) {
1744
1776
  throw new MessagingError(
1745
- "misconfigured",
1746
- `${operation}: no organization_id`,
1747
- "Every conversation and message write carries an explicit organization_id (R5). Pass a real org on <MessagingProvider>; the package refuses the write rather than letting an unscoped row reach the database."
1777
+ "organization-required",
1778
+ `${operation}: no organization selected`,
1779
+ "A new conversation is filed under one organization. Ask the person to choose one, then pass it as organizationId on <MessagingProvider>. Reading and replying to existing conversations never needs it."
1748
1780
  );
1749
1781
  }
1750
1782
  return organizationId;
@@ -1756,7 +1788,7 @@ function sleep(ms) {
1756
1788
  return new Promise((resolve) => setTimeout(resolve, jittered));
1757
1789
  }
1758
1790
  function createMessagingRepository(options) {
1759
- const org = requireOrg(options.organizationId, "createMessagingRepository");
1791
+ const org = optionalOrg(options.organizationId);
1760
1792
  const handedIn = options.identity;
1761
1793
  if (handedIn !== void 0 && handedIn !== null && handedIn.userId !== void 0) {
1762
1794
  options.onDiagnostic?.(
@@ -1826,6 +1858,11 @@ function createMessagingRepository(options) {
1826
1858
  });
1827
1859
  return found;
1828
1860
  }
1861
+ async function organizationOfExisting(conversationId) {
1862
+ if (org !== null) return org;
1863
+ const conversation = await repository.getConversation(conversationId);
1864
+ return conversation.organizationId;
1865
+ }
1829
1866
  const repository = {
1830
1867
  organizationId: org,
1831
1868
  actingUserId,
@@ -1958,6 +1995,7 @@ function createMessagingRepository(options) {
1958
1995
  },
1959
1996
  async getOrCreateDirectConversation(otherUserId) {
1960
1997
  const operation = "getOrCreateDirectConversation";
1998
+ const destination = requireOrg(org, operation);
1961
1999
  const me = await actingUser(operation);
1962
2000
  const id = await withSessionRetry(
1963
2001
  operation,
@@ -1969,7 +2007,7 @@ function createMessagingRepository(options) {
1969
2007
  // not a preference.
1970
2008
  p_user1_id: me,
1971
2009
  p_user2_id: otherUserId,
1972
- p_organization_id: org
2010
+ p_organization_id: destination
1973
2011
  },
1974
2012
  operation
1975
2013
  )
@@ -1981,13 +2019,14 @@ function createMessagingRepository(options) {
1981
2019
  },
1982
2020
  async createGroupConversation({ name, memberIds }) {
1983
2021
  const operation = "createGroupConversation";
2022
+ const destination = requireOrg(org, operation);
1984
2023
  const me = await actingUser(operation);
1985
2024
  const { data, error } = await withSessionRetry(
1986
2025
  operation,
1987
2026
  async () => db().from(TABLES.conversations).insert({
1988
2027
  type: "group",
1989
2028
  group_name: name,
1990
- organization_id: org,
2029
+ organization_id: destination,
1991
2030
  created_by: me
1992
2031
  }).select("id").single()
1993
2032
  );
@@ -2002,7 +2041,7 @@ function createMessagingRepository(options) {
2002
2041
  conversation_id: conversationId,
2003
2042
  user_id: userId,
2004
2043
  role: userId === me ? "owner" : "member",
2005
- organization_id: org,
2044
+ organization_id: destination,
2006
2045
  created_by: me
2007
2046
  }))
2008
2047
  );
@@ -2012,10 +2051,11 @@ function createMessagingRepository(options) {
2012
2051
  async insertMessage(draft, clientMessageId) {
2013
2052
  const operation = "insertMessage";
2014
2053
  const me = await actingUser(operation);
2054
+ const destination = await organizationOfExisting(draft.conversationId);
2015
2055
  const { data, error } = await db().from(TABLES.messages).insert({
2016
2056
  conversation_id: draft.conversationId,
2017
2057
  sender_id: me,
2018
- organization_id: org,
2058
+ organization_id: destination,
2019
2059
  created_by: me,
2020
2060
  content: draft.content,
2021
2061
  message_type: draft.kind ?? "text",
@@ -2076,12 +2116,13 @@ function createMessagingRepository(options) {
2076
2116
  async addMembers(conversationId, memberIds) {
2077
2117
  const operation = "addMembers";
2078
2118
  const me = await actingUser(operation);
2119
+ const destination = await organizationOfExisting(conversationId);
2079
2120
  const { error } = await db().from(TABLES.participants).insert(
2080
2121
  memberIds.map((userId) => ({
2081
2122
  conversation_id: conversationId,
2082
2123
  user_id: userId,
2083
2124
  role: "member",
2084
- organization_id: org,
2125
+ organization_id: destination,
2085
2126
  created_by: me
2086
2127
  }))
2087
2128
  );
@@ -2192,7 +2233,7 @@ function MessagingProvider(props) {
2192
2233
  }, [strayUserId]);
2193
2234
  const session = useSessionUserId(client, props.onDiagnostic);
2194
2235
  const userId = session.snapshot.userId;
2195
- const ready = client != null && userId !== null && typeof organizationId === "string" && organizationId.length > 0;
2236
+ const ready = client != null && userId !== null;
2196
2237
  const hostProvidesRealtime = (0, import_react2.useIsRealtimeProviderMounted)();
2197
2238
  const runtime = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2198
2239
  MessagingRuntime,
@@ -2228,12 +2269,13 @@ function MessagingRuntime(props) {
2228
2269
  actionRegistry.register(handler);
2229
2270
  });
2230
2271
  }, [actionRegistry, handlers]);
2231
- const ready = client != null && manager !== null && sessionIdentity !== null && userId !== null && typeof organizationId === "string" && organizationId.length > 0;
2272
+ const ready = client != null && manager !== null && sessionIdentity !== null && userId !== null;
2273
+ const activeOrganizationId = typeof organizationId === "string" && organizationId.length > 0 ? organizationId : null;
2232
2274
  const engine = (0, import_react.useMemo)(() => {
2233
2275
  if (!ready || manager === null || sessionIdentity === null || userId === null) return null;
2234
2276
  const identity = {
2235
2277
  userId,
2236
- organizationId
2278
+ organizationId: activeOrganizationId
2237
2279
  };
2238
2280
  const report = (event) => {
2239
2281
  (diagnosticRef.current ?? defaultDiagnostics)(event);
@@ -2244,7 +2286,7 @@ function MessagingRuntime(props) {
2244
2286
  // supabase-shape.ts); everything below this line works against the rich
2245
2287
  // contract, which `supabase-shape.test.ts` proves a real client honors.
2246
2288
  client,
2247
- organizationId,
2289
+ organizationId: activeOrganizationId,
2248
2290
  // THE SAME READER the render loop above uses. Two readers would be two
2249
2291
  // answers during the tick an account switch takes.
2250
2292
  sessionIdentity,
@@ -2277,14 +2319,14 @@ function MessagingRuntime(props) {
2277
2319
  }
2278
2320
  });
2279
2321
  return built;
2280
- }, [ready, manager, sessionIdentity, userId, organizationId]);
2322
+ }, [ready, manager, sessionIdentity, userId, activeOrganizationId]);
2281
2323
  (0, import_react.useEffect)(() => {
2282
2324
  if (engine === null) return void 0;
2283
2325
  void engine.start().catch((error) => {
2284
2326
  (diagnosticRef.current ?? defaultDiagnostics)({
2285
2327
  level: "error",
2286
2328
  message: `Messaging failed to start: ${String(error?.message ?? error)}`,
2287
- remedy: "Check the signed-in session and organization, then reload the surface."
2329
+ remedy: "Check the signed-in session, then reload the surface."
2288
2330
  });
2289
2331
  });
2290
2332
  return () => {
@@ -2298,9 +2340,10 @@ function MessagingRuntime(props) {
2298
2340
  const sourceFeature = props.sourceFeature;
2299
2341
  const ai = (0, import_react.useMemo)(() => {
2300
2342
  if (transport === void 0 || agents === void 0 || !ready) return null;
2343
+ if (activeOrganizationId === null) return null;
2301
2344
  return createMessagingAi({
2302
2345
  transport,
2303
- organizationId,
2346
+ organizationId: activeOrganizationId,
2304
2347
  agents,
2305
2348
  ...maxTranscriptMessages !== void 0 ? { maxTranscriptMessages } : {},
2306
2349
  ...sourceApp !== void 0 ? { sourceApp } : {},
@@ -2310,7 +2353,7 @@ function MessagingRuntime(props) {
2310
2353
  transport,
2311
2354
  agents,
2312
2355
  ready,
2313
- organizationId,
2356
+ activeOrganizationId,
2314
2357
  maxTranscriptMessages,
2315
2358
  sourceApp,
2316
2359
  sourceFeature
@@ -2361,7 +2404,7 @@ function useRequiredMessagingHost() {
2361
2404
  const host = useMessagingHost();
2362
2405
  if (host === null) {
2363
2406
  throw new Error(
2364
- "[@ai-matrx/messaging] No messaging host. Wrap this tree in <MessagingProvider client={supabase} userId={userId} organizationId={orgId}> and render it only once auth has resolved."
2407
+ "[@ai-matrx/messaging] No messaging host. Wrap this tree in <MessagingProvider client={supabase} organizationId={orgId}> and render it only once auth has resolved."
2365
2408
  );
2366
2409
  }
2367
2410
  return host;
@@ -2486,7 +2529,11 @@ function useConversations() {
2486
2529
  hasMore: snapshot?.hasMoreConversations ?? false,
2487
2530
  // "Not loaded yet" is a FACT from the store, never a guess. Showing
2488
2531
  // "No conversations yet" before the first read is a screen telling a lie.
2489
- isInitialLoading: snapshot === null || !snapshot.hasLoadedConversations,
2532
+ isInitialLoading: snapshot === null || !snapshot.hasLoadedConversations && snapshot.conversationsLoadError === null,
2533
+ loadError: snapshot?.conversationsLoadError ?? null,
2534
+ retry: () => {
2535
+ void host?.engine.refreshInbox();
2536
+ },
2490
2537
  totalUnreadConversations: snapshot?.totalUnreadConversations ?? 0,
2491
2538
  loadMore: () => {
2492
2539
  void host?.engine.loadMoreConversations();
@@ -2751,7 +2798,8 @@ function useMessageAction(message) {
2751
2798
  const context = {
2752
2799
  messageId: message.id,
2753
2800
  actorId: host.identity.userId,
2754
- organizationId: host.identity.organizationId,
2801
+ // The active organization, else the organization the message lives in.
2802
+ organizationId: host.identity.organizationId ?? message.organizationId,
2755
2803
  choice
2756
2804
  };
2757
2805
  host.actions.execute(action, context).then(setReceipt).catch((cause) => {
@@ -3067,6 +3115,7 @@ function EmptyState(props) {
3067
3115
  }
3068
3116
 
3069
3117
  // src/react/components.tsx
3118
+ var import_design_system = require("@ai-matrx/design-system");
3070
3119
  var import_jsx_runtime4 = require("react/jsx-runtime");
3071
3120
  var AI_LABELS = {
3072
3121
  catchUp: "Catch me up",
@@ -3084,7 +3133,9 @@ function ConversationList(props) {
3084
3133
  activeConversationId,
3085
3134
  archiveFilter,
3086
3135
  setArchiveFilter,
3087
- archivedCount
3136
+ archivedCount,
3137
+ loadError,
3138
+ retry
3088
3139
  } = useConversations();
3089
3140
  const RowChrome = useMessagingHost()?.wrapConversationRow ?? null;
3090
3141
  const [query, setQuery] = (0, import_react5.useState)("");
@@ -3149,7 +3200,20 @@ function ConversationList(props) {
3149
3200
  }
3150
3201
  ) }) : null,
3151
3202
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "mx-msg__scroll", children: [
3152
- isInitialLoading ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ConversationSkeleton, {}) : visible.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3203
+ isInitialLoading ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ConversationSkeleton, {}) : loadError !== null && visible.length === 0 ? (
3204
+ // A failed read resolves to the failure — never a skeleton forever,
3205
+ // never "No conversations yet" (RC-B12 round 11). ErrorBox is the
3206
+ // design-system error slot: the host fills it with its menu.
3207
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3208
+ ReadFailureBox,
3209
+ {
3210
+ title: "Couldn't load your conversations",
3211
+ message: loadError,
3212
+ operation: "List your conversations",
3213
+ onRetry: retry
3214
+ }
3215
+ )
3216
+ ) : visible.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3153
3217
  EmptyState,
3154
3218
  {
3155
3219
  title: query.length > 0 ? "No matches" : showingArchive ? "No archived conversations" : "No conversations yet",
@@ -3326,10 +3390,11 @@ function ConversationView(props) {
3326
3390
  body: "It doesn't exist, or you aren't a member of it."
3327
3391
  }
3328
3392
  ) : conversation.error !== null && messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3329
- EmptyState,
3393
+ ReadFailureBox,
3330
3394
  {
3331
3395
  title: "Couldn't load this conversation",
3332
- body: "Check your connection, then open it again."
3396
+ message: conversation.error.message || "Check your connection, then open it again.",
3397
+ operation: "Open this conversation"
3333
3398
  }
3334
3399
  ) : messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(EmptyState, { title: "No messages yet", body: "Say something to get started." }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "mx-msg__messages", children: [
3335
3400
  conversation.hasMoreOlder ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -3660,4 +3725,27 @@ function MessagingInbox(props) {
3660
3725
  }
3661
3726
  );
3662
3727
  }
3728
+ function ReadFailureBox({
3729
+ title,
3730
+ message,
3731
+ operation,
3732
+ onRetry
3733
+ }) {
3734
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "mx-msg__empty", role: "alert", children: [
3735
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3736
+ import_design_system.ErrorBox,
3737
+ {
3738
+ as: "div",
3739
+ title,
3740
+ message,
3741
+ operation,
3742
+ origin: "messaging",
3743
+ style: { color: "var(--mx-msg-danger)", fontWeight: 600 },
3744
+ children: title
3745
+ }
3746
+ ),
3747
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { children: message }),
3748
+ onRetry ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: "mx-msg__button mx-msg__button--ghost", onClick: onRetry, children: "Try again" }) : null
3749
+ ] });
3750
+ }
3663
3751
  //# sourceMappingURL=react.cjs.map