@antzsoft/chat-core 1.4.6 → 1.4.7

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.
@@ -4270,26 +4270,39 @@ socket.<span class="fn">on</span>(<span class="str">'message_deleted'</span>, (e
4270
4270
  <!-- ─── STEP 11: TYPING ───────────────────────────────────────────────── -->
4271
4271
  <section id="step-typing">
4272
4272
  <h2><span class="step">STEP 11</span> Typing Indicators</h2>
4273
+ <p><strong>Call <code>socketEmit.typing(id, true)</code> on every keystroke &mdash; do not throttle it yourself.</strong> Since core v1.4.7 the SDK leading-throttles it internally to one emit per 3&nbsp;s per conversation, while <code>isTyping: false</code> always goes out and resets the window. Adding a second throttle on top makes the two windows interact unpredictably. You still want the trailing timer below, because that is what sends the <em>stop</em> edge.</p>
4274
+ <p>Incoming events arrive as <code>'typing_indicator'</code> (not <code>'typing'</code>). The server broadcasts to everyone in the room including the sender, so filter your own echo by <code>userId</code>. Each store entry self-expires after 6&nbsp;s without a refreshing event, so a lost stop edge can no longer leave an indicator stuck.</p>
4273
4275
  <div data-p="rn web">
4274
4276
  <pre><code><span class="kw">const</span> timer = <span class="fn">useRef</span>&lt;<span class="tp">ReturnType</span>&lt;<span class="kw">typeof</span> setTimeout&gt;&gt;();
4275
4277
 
4276
4278
  <span class="kw">function</span> <span class="fn">onChangeText</span>(val: <span class="tp">string</span>) {
4277
4279
  <span class="fn">setValue</span>(val);
4280
+ <span class="cm">// Fine to call per keystroke — core throttles this internally.</span>
4278
4281
  socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">true</span>);
4279
4282
  <span class="fn">clearTimeout</span>(timer.current);
4280
- timer.current = <span class="fn">setTimeout</span>(() => socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">false</span>), <span class="num">3000</span>);
4283
+ timer.current = <span class="fn">setTimeout</span>(() => socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">false</span>), <span class="num">2000</span>);
4281
4284
  }
4282
4285
 
4283
- <span class="cm">// Listen for others typing</span>
4284
- socket.<span class="fn">on</span>(<span class="str">'typing'</span>, ({ conversationId, userId, displayName, isTyping }) => {
4286
+ <span class="cm">// Also send the stop edge when the composer unmounts or the</span>
4287
+ <span class="cm">// conversation changes otherwise a pending debounce is discarded</span>
4288
+ <span class="cm">// and the peer's indicator lingers until it expires.</span>
4289
+ <span class="fn">useEffect</span>(() => () => {
4290
+ <span class="fn">clearTimeout</span>(timer.current);
4291
+ socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">false</span>);
4292
+ }, [conversationId]);
4293
+
4294
+ <span class="cm">// Listen for others typing — event name is 'typing_indicator'</span>
4295
+ socket.<span class="fn">on</span>(<span class="str">'typing_indicator'</span>, ({ conversationId, userId, displayName, isTyping }) => {
4296
+ <span class="kw">if</span> (userId === currentUserId) <span class="kw">return</span>; <span class="cm">// ignore our own echo</span>
4285
4297
  isTyping ? <span class="fn">addTypingUser</span>(conversationId, { userId, displayName })
4286
4298
  : <span class="fn">removeTypingUser</span>(conversationId, userId);
4287
4299
  });</code></pre>
4288
4300
  </div>
4289
4301
  <div data-p="node">
4290
4302
  <pre><code>socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">true</span>);
4291
- <span class="kw">setTimeout</span>(() => socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">false</span>), <span class="num">3000</span>);</code></pre>
4303
+ <span class="kw">setTimeout</span>(() => socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">false</span>), <span class="num">2000</span>);</code></pre>
4292
4304
  </div>
4305
+ <div class="callout info"><strong>Server-side suppression.</strong> chat-server skips typing broadcasts entirely in rooms with more than <code>TYPING_MAX_ROOM_SOCKETS</code> connected sockets (default&nbsp;30, <code>0</code> disables). In a large, busy group nobody sees an indicator &mdash; by design, since the UI can only render &ldquo;Several people are typing&hellip;&rdquo; there anyway. The count is sockets, not members, so one user on phone&nbsp;+&nbsp;laptop counts twice.</div>
4293
4306
  </section>
4294
4307
 
4295
4308
  <!-- ─── STEP 12: READ RECEIPTS & LAST SEEN ────────────────────────────── -->
@@ -5476,9 +5489,10 @@ Notifications.<span class="fn">addNotificationResponseReceivedListener</span>((r
5476
5489
  };
5477
5490
 
5478
5491
  <span class="kw">const</span> <span class="fn">onChangeText</span> = (val: <span class="tp">string</span>) => {
5492
+ <span class="cm">// Per-keystroke is fine: core throttles typing:true internally.</span>
5479
5493
  <span class="fn">setText</span>(val); socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">true</span>);
5480
5494
  <span class="fn">clearTimeout</span>(timer.current);
5481
- timer.current = <span class="fn">setTimeout</span>(() => socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">false</span>), <span class="num">3000</span>);
5495
+ timer.current = <span class="fn">setTimeout</span>(() => socketEmit.<span class="fn">typing</span>(conversationId, <span class="kw">false</span>), <span class="num">2000</span>);
5482
5496
  };
5483
5497
 
5484
5498
  <span class="kw">const</span> <span class="fn">loadMore</span> = <span class="kw">async</span> () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antzsoft/chat-core",
3
- "version": "1.4.6",
3
+ "version": "1.4.7",
4
4
  "description": "Platform-agnostic core for Antz Chat — API, socket, stores, types. Works in browser, React Native (Expo or bare), and Node.js.",
5
5
  "author": "Antz",
6
6
  "license": "MIT",
@@ -1,7 +0,0 @@
1
- import {
2
- useChatStore
3
- } from "./chunk-UIYJAOGL.js";
4
- export {
5
- useChatStore
6
- };
7
- //# sourceMappingURL=chat.store-UVTDBPEC.js.map
@@ -1,62 +0,0 @@
1
- // src/stores/chat.store.ts
2
- import { create } from "zustand";
3
- var useChatStore = create((set) => ({
4
- activeConversationId: null,
5
- pendingTarget: null,
6
- typingUsers: {},
7
- onlineUsers: [],
8
- lastRead: {},
9
- lastSeen: {},
10
- replyingTo: null,
11
- editingMessage: null,
12
- forwardingMessage: null,
13
- isSidebarOpen: true,
14
- isGroupInfoOpen: false,
15
- isStarredPanelOpen: false,
16
- messageInfoId: null,
17
- setActiveConversation: (id) => set({ activeConversationId: id, replyingTo: null, editingMessage: null, forwardingMessage: null }),
18
- setPendingTarget: (target) => set({ pendingTarget: target }),
19
- addTypingUser: (conversationId, user) => set((state) => {
20
- const existing = state.typingUsers[conversationId] ?? [];
21
- const deduped = existing.filter((u) => u.userId !== user.userId);
22
- return { typingUsers: { ...state.typingUsers, [conversationId]: [...deduped, user] } };
23
- }),
24
- removeTypingUser: (conversationId, userId) => set((state) => ({
25
- typingUsers: {
26
- ...state.typingUsers,
27
- [conversationId]: (state.typingUsers[conversationId] ?? []).filter(
28
- (u) => u.userId !== userId
29
- )
30
- }
31
- })),
32
- setUserOnline: (userId) => set((state) => ({
33
- onlineUsers: state.onlineUsers.includes(userId) ? state.onlineUsers : [...state.onlineUsers, userId]
34
- })),
35
- setUserOffline: (userId) => set((state) => ({ onlineUsers: state.onlineUsers.filter((id) => id !== userId) })),
36
- setOnlineUsers: (userIds) => set({ onlineUsers: userIds }),
37
- setLastRead: (conversationId, messageId, readAt) => set((state) => ({
38
- lastRead: { ...state.lastRead, [conversationId]: { messageId, readAt } }
39
- })),
40
- setLastSeen: (userId, lastSeenAt) => set((state) => {
41
- if (lastSeenAt === null) {
42
- const { [userId]: _, ...rest } = state.lastSeen;
43
- return { lastSeen: rest };
44
- }
45
- return { lastSeen: { ...state.lastSeen, [userId]: lastSeenAt } };
46
- }),
47
- setReplyingTo: (message) => set({ replyingTo: message, editingMessage: null }),
48
- setEditingMessage: (message) => set({ editingMessage: message, replyingTo: null }),
49
- setForwardingMessage: (message) => set({ forwardingMessage: message }),
50
- toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
51
- setSidebarOpen: (open) => set({ isSidebarOpen: open }),
52
- toggleGroupInfo: () => set((state) => ({ isGroupInfoOpen: !state.isGroupInfoOpen })),
53
- setGroupInfoOpen: (open) => set({ isGroupInfoOpen: open }),
54
- toggleStarredPanel: () => set((state) => ({ isStarredPanelOpen: !state.isStarredPanelOpen })),
55
- setStarredPanelOpen: (open) => set({ isStarredPanelOpen: open }),
56
- setMessageInfoId: (id) => set({ messageInfoId: id })
57
- }));
58
-
59
- export {
60
- useChatStore
61
- };
62
- //# sourceMappingURL=chunk-UIYJAOGL.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/stores/chat.store.ts"],"sourcesContent":["import { create } from 'zustand';\nimport type { Message } from '../types/index.js';\n\ninterface TypingUser {\n userId: string;\n displayName: string;\n avatarUrl?: string;\n}\n\nexport interface LastReadEntry {\n messageId: string;\n readAt: string;\n}\n\ninterface ChatState {\n activeConversationId: string | null;\n pendingTarget: { conversationId: string; messageId: string } | null;\n typingUsers: Record<string, TypingUser[]>;\n onlineUsers: string[];\n /** keyed by conversationId — current user's last read pointer per conversation */\n lastRead: Record<string, LastReadEntry>;\n /** keyed by userId — last seen timestamp for each user */\n lastSeen: Record<string, string>;\n replyingTo: Message | null;\n editingMessage: Message | null;\n /** Message staged in the forward picker, null = picker closed. Unlike replyingTo,\n * this does not fold into the next composer send — forwarding targets a different\n * conversation and needs its own conversation-picker UI. */\n forwardingMessage: Message | null;\n isSidebarOpen: boolean;\n isGroupInfoOpen: boolean;\n isStarredPanelOpen: boolean;\n /** messageId currently shown in the Message Info panel, null = closed */\n messageInfoId: string | null;\n\n setActiveConversation: (id: string | null) => void;\n setPendingTarget: (target: { conversationId: string; messageId: string } | null) => void;\n addTypingUser: (conversationId: string, user: TypingUser) => void;\n removeTypingUser: (conversationId: string, userId: string) => void;\n setUserOnline: (userId: string) => void;\n setUserOffline: (userId: string) => void;\n setOnlineUsers: (userIds: string[]) => void;\n setLastRead: (conversationId: string, messageId: string, readAt: string) => void;\n setLastSeen: (userId: string, lastSeenAt: string | null) => void;\n setReplyingTo: (message: Message | null) => void;\n setEditingMessage: (message: Message | null) => void;\n setForwardingMessage: (message: Message | null) => void;\n toggleSidebar: () => void;\n setSidebarOpen: (open: boolean) => void;\n toggleGroupInfo: () => void;\n setGroupInfoOpen: (open: boolean) => void;\n toggleStarredPanel: () => void;\n setStarredPanelOpen: (open: boolean) => void;\n setMessageInfoId: (id: string | null) => void;\n}\n\nexport const useChatStore = create<ChatState>((set) => ({\n activeConversationId: null,\n pendingTarget: null,\n typingUsers: {},\n onlineUsers: [],\n lastRead: {},\n lastSeen: {},\n replyingTo: null,\n editingMessage: null,\n forwardingMessage: null,\n isSidebarOpen: true,\n isGroupInfoOpen: false,\n isStarredPanelOpen: false,\n messageInfoId: null,\n\n setActiveConversation: (id) =>\n set({ activeConversationId: id, replyingTo: null, editingMessage: null, forwardingMessage: null }),\n\n setPendingTarget: (target) => set({ pendingTarget: target }),\n\n addTypingUser: (conversationId, user) =>\n set((state) => {\n const existing = state.typingUsers[conversationId] ?? [];\n const deduped = existing.filter((u) => u.userId !== user.userId);\n return { typingUsers: { ...state.typingUsers, [conversationId]: [...deduped, user] } };\n }),\n\n removeTypingUser: (conversationId, userId) =>\n set((state) => ({\n typingUsers: {\n ...state.typingUsers,\n [conversationId]: (state.typingUsers[conversationId] ?? []).filter(\n (u) => u.userId !== userId,\n ),\n },\n })),\n\n setUserOnline: (userId) =>\n set((state) => ({\n onlineUsers: state.onlineUsers.includes(userId)\n ? state.onlineUsers\n : [...state.onlineUsers, userId],\n })),\n\n setUserOffline: (userId) =>\n set((state) => ({ onlineUsers: state.onlineUsers.filter((id) => id !== userId) })),\n\n setOnlineUsers: (userIds) => set({ onlineUsers: userIds }),\n\n setLastRead: (conversationId, messageId, readAt) =>\n set((state) => ({\n lastRead: { ...state.lastRead, [conversationId]: { messageId, readAt } },\n })),\n\n setLastSeen: (userId, lastSeenAt) =>\n set((state) => {\n if (lastSeenAt === null) {\n const { [userId]: _, ...rest } = state.lastSeen;\n return { lastSeen: rest };\n }\n return { lastSeen: { ...state.lastSeen, [userId]: lastSeenAt } };\n }),\n\n setReplyingTo: (message) => set({ replyingTo: message, editingMessage: null }),\n\n setEditingMessage: (message) => set({ editingMessage: message, replyingTo: null }),\n\n setForwardingMessage: (message) => set({ forwardingMessage: message }),\n\n toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),\n setSidebarOpen: (open) => set({ isSidebarOpen: open }),\n\n toggleGroupInfo: () => set((state) => ({ isGroupInfoOpen: !state.isGroupInfoOpen })),\n setGroupInfoOpen: (open) => set({ isGroupInfoOpen: open }),\n\n toggleStarredPanel: () => set((state) => ({ isStarredPanelOpen: !state.isStarredPanelOpen })),\n setStarredPanelOpen: (open) => set({ isStarredPanelOpen: open }),\n\n setMessageInfoId: (id: string | null) => set({ messageInfoId: id }),\n}));\n"],"mappings":"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","names":[]}