@ai-matrx/messaging 0.1.0 → 0.1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,62 @@
1
1
  # Changelog — `@ai-matrx/messaging`
2
2
 
3
+ ## 0.1.2 — 2026-08-31
4
+
5
+ **Typing, presence and message broadcast work.** The root defect was in `@ai-matrx/realtime`
6
+ (every channel got a unique instance topic, and for broadcast/presence the topic IS the room);
7
+ this package found it, and it is fixed in realtime 0.2.0. These are the consumer-side changes
8
+ that fix came with — both of them real defects this package was carrying.
9
+
10
+ ### Fixed
11
+
12
+ - **One presence publisher per conversation.** One channel carries ONE presence entry per key, so
13
+ `useTypists` and `useOnlineUserIds` now read the roster with `presence: { publish: false }`.
14
+ The composer — which actually knows whether this user is typing — is the single publisher. Two
15
+ publishers overwrote each other on every heartbeat.
16
+ - **One composer per conversation, so REPLY works.** `ConversationView` called `useComposer` and
17
+ also rendered `<Composer>`, which called it again: two independent drafts, two independent
18
+ reply targets — picking "reply" on a message set it on an instance the composer's reply bar
19
+ never read — and two typing publishers. The view now owns the state and renders `ComposerView`;
20
+ the exported `Composer` is unchanged for standalone use.
21
+ - **A broadcast message one field short no longer takes down the thread.** `useMessageAction`
22
+ reads `message.action ?? null` instead of assuming the field is present; an off-shape wire
23
+ payload used to throw `undefined.kind` and unmount the conversation.
24
+
25
+ ### Added
26
+
27
+ - `src/react/realtime-rooms.test.tsx` — renders the real `ConversationView` against a shared,
28
+ topic-deduping fake Supabase client and proves every holder of the conversation topic lands on
29
+ ONE live channel named as declared (0.1.1 produced five), a remote typist reaches the
30
+ indicator, a remote broadcast reaches the thread, and exactly one holder publishes presence.
31
+ All four fail against `@ai-matrx/realtime` 0.1.1.
32
+
33
+ ### Consumer action (C28)
34
+
35
+ None. No public API changed. `@ai-matrx/realtime` >= 0.2.0 is required and is picked up
36
+ automatically by the `"latest"` spec.
37
+
38
+ ## 0.1.1 — 2026-08-31
39
+
40
+ Caught in review immediately after 0.1.0 published.
41
+
42
+ ### Fixed
43
+
44
+ - **Sending a message no longer costs a conversation-list read.** `onSent` and the inbox
45
+ channel's message handler both called `listConversations` to refresh one row — a full RPC page
46
+ read per sent message, i.e. one per burst at a busy org, and a regression against the origin
47
+ (`matrx-frontend` updated the row optimistically via `updateConversationLastMessage`). The row's
48
+ new preview and sort position are entirely derivable from the message already in hand, so the
49
+ store gained `applyLastMessage`, which also refuses to move a row BACKWARDS on an out-of-order
50
+ arrival, ignores an optimistic (still-sending) message, and never badges the conversation being
51
+ read. Five regression tests.
52
+ - **The inbox no longer flashes "No conversations yet" before it has looked.** An empty list meant
53
+ two different things — "you have none" and "we have not read yet" — and the surface guessed.
54
+ `MessagingSnapshot.hasLoadedConversations` makes it a fact.
55
+
56
+ ### Consumer action
57
+
58
+ None. Both fixes are internal to the package; no API changed and no host wiring is affected.
59
+
3
60
  ## 0.1.0 — 2026-08-31
4
61
 
5
62
  First release. Enterprise, AI-native in-app messaging for Matrx clients, built to the ratified
@@ -40,7 +97,7 @@ work order at `common-docs/systems/communications/messaging/HANDOFF.md`, on top
40
97
  insets, 44px touch targets, a 16px composer minimum so iOS Safari does not zoom on focus) and
41
98
  `@ai-matrx/messaging/tokens.css` (a complete default palette, light and dark, host-overridable).
42
99
  Hardcoded colors in the structural sheet fail the release gate.
43
- - 113 tests, each named for the failure it prevents, plus a packed-tarball canary that exercises
100
+ - 118 tests, each named for the failure it prevents, plus a packed-tarball canary that exercises
44
101
  ESM + CJS, both entries, the `"use client"` banner per chunk, the `globalThis` slots across
45
102
  module graphs, the stylesheets' presence and export-map resolution, and the stability of the
46
103
  table/RPC names.
package/dist/index.cjs CHANGED
@@ -1111,6 +1111,29 @@ function createMessagingStore() {
1111
1111
  });
1112
1112
  emit();
1113
1113
  },
1114
+ applyLastMessage(message, args = {}) {
1115
+ const existing = conversations.find(
1116
+ (item) => item.conversation.id === message.conversationId
1117
+ );
1118
+ if (existing === void 0) return;
1119
+ if (message.deliveryState === "sending" || message.deletedAt !== null) return;
1120
+ if (existing.lastMessageAt !== null && message.createdAt < existing.lastMessageAt) return;
1121
+ const isActive = activeConversationId === message.conversationId;
1122
+ const shouldCount = args.incrementUnread === true && !isActive;
1123
+ conversations = normalizeConversations(
1124
+ conversations.map(
1125
+ (item) => item.conversation.id === message.conversationId ? {
1126
+ ...item,
1127
+ lastMessageContent: message.content,
1128
+ lastMessageSenderId: message.senderId,
1129
+ lastMessageAt: message.createdAt,
1130
+ sortAt: message.createdAt,
1131
+ unreadCount: shouldCount ? item.unreadCount + 1 : item.unreadCount
1132
+ } : item
1133
+ )
1134
+ );
1135
+ emit();
1136
+ },
1114
1137
  markConversationRead(id) {
1115
1138
  conversations = conversations.map(
1116
1139
  (item) => item.conversation.id === id ? { ...item, unreadCount: 0 } : item
@@ -1181,7 +1204,7 @@ function createMessagingEngine(options) {
1181
1204
  onSent: (entry, message) => {
1182
1205
  store.ingest(message);
1183
1206
  broadcastMessage(message);
1184
- void refreshConversationRow(message.conversationId);
1207
+ store.applyLastMessage(message);
1185
1208
  void entry;
1186
1209
  },
1187
1210
  onChange: (entries) => {
@@ -1207,15 +1230,6 @@ function createMessagingEngine(options) {
1207
1230
  function broadcastMessage(message) {
1208
1231
  openChannels.get(message.conversationId)?.send(MESSAGING_EVENTS.message, message);
1209
1232
  }
1210
- async function refreshConversationRow(id) {
1211
- try {
1212
- const page = await repository.listConversations({ limit: conversationPageSize });
1213
- const row = page.items.find((item) => item.conversation.id === id);
1214
- if (row !== void 0) store.upsertConversation(row);
1215
- } catch (error) {
1216
- reportError(error, "refreshConversationRow");
1217
- }
1218
- }
1219
1233
  async function reloadInbox() {
1220
1234
  const page = await repository.listConversations({ limit: conversationPageSize });
1221
1235
  conversationCursor = page.nextCursor;
@@ -1266,10 +1280,9 @@ function createMessagingEngine(options) {
1266
1280
  return;
1267
1281
  }
1268
1282
  store.ingest(message);
1269
- if (message.senderId !== identity.userId) {
1270
- options.onIncoming?.(message);
1271
- }
1272
- void refreshConversationRow(message.conversationId);
1283
+ const isMine = message.senderId === identity.userId;
1284
+ store.applyLastMessage(message, { incrementUnread: !isMine });
1285
+ if (!isMine) options.onIncoming?.(message);
1273
1286
  }
1274
1287
  },
1275
1288
  {