@ai-matrx/messaging 0.1.1 → 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,40 @@
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
+
3
38
  ## 0.1.1 — 2026-08-31
4
39
 
5
40
  Caught in review immediately after 0.1.0 published.
package/dist/react.cjs CHANGED
@@ -2125,7 +2125,12 @@ function useTypists(conversationId, participants) {
2125
2125
  const typing = (0, import_react4.useTyping)(
2126
2126
  {
2127
2127
  topic: conversationId === null ? "" : conversationTopic(conversationId),
2128
- identity: { userId: host?.identity.userId ?? "" }
2128
+ identity: { userId: host?.identity.userId ?? "" },
2129
+ // READ-ONLY. This label and `useComposer` sit on the SAME topic, which is
2130
+ // one channel carrying ONE presence entry per key — so exactly one of
2131
+ // them publishes, and it is the composer that actually knows whether this
2132
+ // user is typing. Publishing here would overwrite that on every heartbeat.
2133
+ publish: false
2129
2134
  },
2130
2135
  { enabled: conversationId !== null && host !== null }
2131
2136
  );
@@ -2152,7 +2157,10 @@ function useOnlineUserIds(conversationId) {
2152
2157
  const presence = (0, import_react4.usePresence)(
2153
2158
  {
2154
2159
  topic: conversationId === null ? "" : conversationTopic(conversationId),
2155
- presence: { state: { userId: host?.identity.userId ?? "" } }
2160
+ // READ-ONLY, for the same reason: `useComposer`'s typing presence already
2161
+ // puts this user in the roster under this key with their `userId`, and a
2162
+ // second publisher on one entry just fights it.
2163
+ presence: { publish: false, state: { userId: host?.identity.userId ?? "" } }
2156
2164
  },
2157
2165
  { enabled: conversationId !== null && host !== null }
2158
2166
  );
@@ -2233,7 +2241,7 @@ function useMessagingAi(conversationId) {
2233
2241
  }
2234
2242
  function useMessageAction(message) {
2235
2243
  const host = useRequiredMessagingHost();
2236
- const action = message.action;
2244
+ const action = message.action ?? null;
2237
2245
  const [receipt, setReceipt] = (0, import_react3.useState)(
2238
2246
  () => action === null ? null : host.actions.receiptFor(action.kind, message.id, host.identity.userId)
2239
2247
  );
@@ -2765,7 +2773,7 @@ function ConversationView(props) {
2765
2773
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TypingDots, {}),
2766
2774
  typists.label
2767
2775
  ] }) : null }),
2768
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Composer, { conversationId })
2776
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ComposerView, { composer, disabled: false })
2769
2777
  ] });
2770
2778
  }
2771
2779
  function MessageGroupView(props) {
@@ -2898,6 +2906,10 @@ function MessageActionChips(props) {
2898
2906
  }
2899
2907
  function Composer(props) {
2900
2908
  const composer = useComposer(props.conversationId);
2909
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ComposerView, { composer, disabled: props.conversationId === null });
2910
+ }
2911
+ function ComposerView(props) {
2912
+ const composer = props.composer;
2901
2913
  const textareaRef = (0, import_react5.useRef)(null);
2902
2914
  (0, import_react5.useEffect)(() => {
2903
2915
  const node = textareaRef.current;
@@ -2931,7 +2943,7 @@ function Composer(props) {
2931
2943
  value: composer.value,
2932
2944
  placeholder: "Message",
2933
2945
  "aria-label": "Message",
2934
- disabled: props.conversationId === null,
2946
+ disabled: props.disabled,
2935
2947
  onChange: (event) => {
2936
2948
  composer.setValue(event.target.value);
2937
2949
  composer.onKeystroke();