@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/dist/react.d.cts CHANGED
@@ -672,6 +672,15 @@ interface MessagingStore {
672
672
  ingestMany(messages: readonly Message[]): void;
673
673
  removeMessage(conversationId: ConversationId, messageId: MessageId): void;
674
674
  markConversationRead(id: ConversationId): void;
675
+ /**
676
+ * Move a conversation's preview + sort position to reflect a message, with NO
677
+ * refetch. Sending a message must not cost a conversation-list RPC — at a
678
+ * busy org that is one full page read per keystroke-burst, and the row's new
679
+ * state is entirely derivable from the message we already hold.
680
+ */
681
+ applyLastMessage(message: Message, args?: {
682
+ incrementUnread?: boolean;
683
+ }): void;
675
684
  setUnreadCount(id: ConversationId, count: number): void;
676
685
  }
677
686
  declare function createMessagingStore(): MessagingStore;
@@ -934,6 +943,13 @@ declare function MessageBubble(props: {
934
943
  declare function MessageActionChips(props: {
935
944
  message: Message;
936
945
  }): ReactNode;
946
+ /**
947
+ * The standalone composer: it owns its own state. Inside `ConversationView` the
948
+ * VIEW owns it instead (`ComposerView` below) — two `useComposer` instances on
949
+ * one conversation are two independent drafts, two independent reply targets,
950
+ * and — since typing rides presence — two publishers fighting over ONE presence
951
+ * entry, which `@ai-matrx/realtime` warns about by name.
952
+ */
937
953
  declare function Composer(props: {
938
954
  conversationId: ConversationId | null;
939
955
  }): ReactNode;
package/dist/react.d.ts CHANGED
@@ -672,6 +672,15 @@ interface MessagingStore {
672
672
  ingestMany(messages: readonly Message[]): void;
673
673
  removeMessage(conversationId: ConversationId, messageId: MessageId): void;
674
674
  markConversationRead(id: ConversationId): void;
675
+ /**
676
+ * Move a conversation's preview + sort position to reflect a message, with NO
677
+ * refetch. Sending a message must not cost a conversation-list RPC — at a
678
+ * busy org that is one full page read per keystroke-burst, and the row's new
679
+ * state is entirely derivable from the message we already hold.
680
+ */
681
+ applyLastMessage(message: Message, args?: {
682
+ incrementUnread?: boolean;
683
+ }): void;
675
684
  setUnreadCount(id: ConversationId, count: number): void;
676
685
  }
677
686
  declare function createMessagingStore(): MessagingStore;
@@ -934,6 +943,13 @@ declare function MessageBubble(props: {
934
943
  declare function MessageActionChips(props: {
935
944
  message: Message;
936
945
  }): ReactNode;
946
+ /**
947
+ * The standalone composer: it owns its own state. Inside `ConversationView` the
948
+ * VIEW owns it instead (`ComposerView` below) — two `useComposer` instances on
949
+ * one conversation are two independent drafts, two independent reply targets,
950
+ * and — since typing rides presence — two publishers fighting over ONE presence
951
+ * entry, which `@ai-matrx/realtime` warns about by name.
952
+ */
937
953
  declare function Composer(props: {
938
954
  conversationId: ConversationId | null;
939
955
  }): ReactNode;
package/dist/react.js CHANGED
@@ -966,6 +966,29 @@ function createMessagingStore() {
966
966
  });
967
967
  emit();
968
968
  },
969
+ applyLastMessage(message, args = {}) {
970
+ const existing = conversations.find(
971
+ (item) => item.conversation.id === message.conversationId
972
+ );
973
+ if (existing === void 0) return;
974
+ if (message.deliveryState === "sending" || message.deletedAt !== null) return;
975
+ if (existing.lastMessageAt !== null && message.createdAt < existing.lastMessageAt) return;
976
+ const isActive = activeConversationId === message.conversationId;
977
+ const shouldCount = args.incrementUnread === true && !isActive;
978
+ conversations = normalizeConversations(
979
+ conversations.map(
980
+ (item) => item.conversation.id === message.conversationId ? {
981
+ ...item,
982
+ lastMessageContent: message.content,
983
+ lastMessageSenderId: message.senderId,
984
+ lastMessageAt: message.createdAt,
985
+ sortAt: message.createdAt,
986
+ unreadCount: shouldCount ? item.unreadCount + 1 : item.unreadCount
987
+ } : item
988
+ )
989
+ );
990
+ emit();
991
+ },
969
992
  markConversationRead(id) {
970
993
  conversations = conversations.map(
971
994
  (item) => item.conversation.id === id ? { ...item, unreadCount: 0 } : item
@@ -1036,7 +1059,7 @@ function createMessagingEngine(options) {
1036
1059
  onSent: (entry, message) => {
1037
1060
  store.ingest(message);
1038
1061
  broadcastMessage(message);
1039
- void refreshConversationRow(message.conversationId);
1062
+ store.applyLastMessage(message);
1040
1063
  void entry;
1041
1064
  },
1042
1065
  onChange: (entries) => {
@@ -1062,15 +1085,6 @@ function createMessagingEngine(options) {
1062
1085
  function broadcastMessage(message) {
1063
1086
  openChannels.get(message.conversationId)?.send(MESSAGING_EVENTS.message, message);
1064
1087
  }
1065
- async function refreshConversationRow(id) {
1066
- try {
1067
- const page = await repository.listConversations({ limit: conversationPageSize });
1068
- const row = page.items.find((item) => item.conversation.id === id);
1069
- if (row !== void 0) store.upsertConversation(row);
1070
- } catch (error) {
1071
- reportError(error, "refreshConversationRow");
1072
- }
1073
- }
1074
1088
  async function reloadInbox() {
1075
1089
  const page = await repository.listConversations({ limit: conversationPageSize });
1076
1090
  conversationCursor = page.nextCursor;
@@ -1121,10 +1135,9 @@ function createMessagingEngine(options) {
1121
1135
  return;
1122
1136
  }
1123
1137
  store.ingest(message);
1124
- if (message.senderId !== identity.userId) {
1125
- options.onIncoming?.(message);
1126
- }
1127
- void refreshConversationRow(message.conversationId);
1138
+ const isMine = message.senderId === identity.userId;
1139
+ store.applyLastMessage(message, { incrementUnread: !isMine });
1140
+ if (!isMine) options.onIncoming?.(message);
1128
1141
  }
1129
1142
  },
1130
1143
  {
@@ -2016,7 +2029,12 @@ function useTypists(conversationId, participants) {
2016
2029
  const typing = useTyping(
2017
2030
  {
2018
2031
  topic: conversationId === null ? "" : conversationTopic(conversationId),
2019
- identity: { userId: host?.identity.userId ?? "" }
2032
+ identity: { userId: host?.identity.userId ?? "" },
2033
+ // READ-ONLY. This label and `useComposer` sit on the SAME topic, which is
2034
+ // one channel carrying ONE presence entry per key — so exactly one of
2035
+ // them publishes, and it is the composer that actually knows whether this
2036
+ // user is typing. Publishing here would overwrite that on every heartbeat.
2037
+ publish: false
2020
2038
  },
2021
2039
  { enabled: conversationId !== null && host !== null }
2022
2040
  );
@@ -2043,7 +2061,10 @@ function useOnlineUserIds(conversationId) {
2043
2061
  const presence = usePresence(
2044
2062
  {
2045
2063
  topic: conversationId === null ? "" : conversationTopic(conversationId),
2046
- presence: { state: { userId: host?.identity.userId ?? "" } }
2064
+ // READ-ONLY, for the same reason: `useComposer`'s typing presence already
2065
+ // puts this user in the roster under this key with their `userId`, and a
2066
+ // second publisher on one entry just fights it.
2067
+ presence: { publish: false, state: { userId: host?.identity.userId ?? "" } }
2047
2068
  },
2048
2069
  { enabled: conversationId !== null && host !== null }
2049
2070
  );
@@ -2124,7 +2145,7 @@ function useMessagingAi(conversationId) {
2124
2145
  }
2125
2146
  function useMessageAction(message) {
2126
2147
  const host = useRequiredMessagingHost();
2127
- const action = message.action;
2148
+ const action = message.action ?? null;
2128
2149
  const [receipt, setReceipt] = useState(
2129
2150
  () => action === null ? null : host.actions.receiptFor(action.kind, message.id, host.identity.userId)
2130
2151
  );
@@ -2656,7 +2677,7 @@ function ConversationView(props) {
2656
2677
  /* @__PURE__ */ jsx4(TypingDots, {}),
2657
2678
  typists.label
2658
2679
  ] }) : null }),
2659
- /* @__PURE__ */ jsx4(Composer, { conversationId })
2680
+ /* @__PURE__ */ jsx4(ComposerView, { composer, disabled: false })
2660
2681
  ] });
2661
2682
  }
2662
2683
  function MessageGroupView(props) {
@@ -2789,6 +2810,10 @@ function MessageActionChips(props) {
2789
2810
  }
2790
2811
  function Composer(props) {
2791
2812
  const composer = useComposer(props.conversationId);
2813
+ return /* @__PURE__ */ jsx4(ComposerView, { composer, disabled: props.conversationId === null });
2814
+ }
2815
+ function ComposerView(props) {
2816
+ const composer = props.composer;
2792
2817
  const textareaRef = useRef3(null);
2793
2818
  useEffect3(() => {
2794
2819
  const node = textareaRef.current;
@@ -2822,7 +2847,7 @@ function Composer(props) {
2822
2847
  value: composer.value,
2823
2848
  placeholder: "Message",
2824
2849
  "aria-label": "Message",
2825
- disabled: props.conversationId === null,
2850
+ disabled: props.disabled,
2826
2851
  onChange: (event) => {
2827
2852
  composer.setValue(event.target.value);
2828
2853
  composer.onKeystroke();