@ethora/chat-component-rn 26.7.2 → 26.7.3

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.
Files changed (92) hide show
  1. package/lib/commonjs/components/MainComponents/ChatRoom.js +60 -3
  2. package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
  3. package/lib/commonjs/components/MainComponents/ChatWrapper.js +18 -0
  4. package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
  5. package/lib/commonjs/components/MainComponents/MessageList.js +9 -3
  6. package/lib/commonjs/components/MainComponents/MessageList.js.map +1 -1
  7. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +130 -125
  8. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  9. package/lib/commonjs/context/xmppProvider.js +12 -0
  10. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  11. package/lib/commonjs/helpers/historyPreloadScheduler.js +65 -2
  12. package/lib/commonjs/helpers/historyPreloadScheduler.js.map +1 -1
  13. package/lib/commonjs/helpers/shallowEqual.js +36 -0
  14. package/lib/commonjs/helpers/shallowEqual.js.map +1 -0
  15. package/lib/commonjs/hooks/useChatWrapperInit.js +15 -0
  16. package/lib/commonjs/hooks/useChatWrapperInit.js.map +1 -1
  17. package/lib/commonjs/hooks/useComposing.js +27 -0
  18. package/lib/commonjs/hooks/useComposing.js.map +1 -1
  19. package/lib/commonjs/hooks/useSendMessage.js +27 -1
  20. package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
  21. package/lib/commonjs/hooks/useUnreadMessagesCounter.js +6 -1
  22. package/lib/commonjs/hooks/useUnreadMessagesCounter.js.map +1 -1
  23. package/lib/commonjs/networking/xmpp/flushLastViewedToPrivateStore.js +6 -2
  24. package/lib/commonjs/networking/xmpp/flushLastViewedToPrivateStore.js.map +1 -1
  25. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  26. package/lib/commonjs/roomStore/roomsSlice.js +2 -2
  27. package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
  28. package/lib/module/components/MainComponents/ChatRoom.js +61 -4
  29. package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
  30. package/lib/module/components/MainComponents/ChatWrapper.js +19 -1
  31. package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
  32. package/lib/module/components/MainComponents/MessageList.js +9 -3
  33. package/lib/module/components/MainComponents/MessageList.js.map +1 -1
  34. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +130 -125
  35. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  36. package/lib/module/context/xmppProvider.js +12 -0
  37. package/lib/module/context/xmppProvider.js.map +1 -1
  38. package/lib/module/helpers/historyPreloadScheduler.js +66 -3
  39. package/lib/module/helpers/historyPreloadScheduler.js.map +1 -1
  40. package/lib/module/helpers/shallowEqual.js +31 -0
  41. package/lib/module/helpers/shallowEqual.js.map +1 -0
  42. package/lib/module/hooks/useChatWrapperInit.js +15 -0
  43. package/lib/module/hooks/useChatWrapperInit.js.map +1 -1
  44. package/lib/module/hooks/useComposing.js +28 -1
  45. package/lib/module/hooks/useComposing.js.map +1 -1
  46. package/lib/module/hooks/useSendMessage.js +28 -2
  47. package/lib/module/hooks/useSendMessage.js.map +1 -1
  48. package/lib/module/hooks/useUnreadMessagesCounter.js +6 -1
  49. package/lib/module/hooks/useUnreadMessagesCounter.js.map +1 -1
  50. package/lib/module/networking/xmpp/flushLastViewedToPrivateStore.js +6 -2
  51. package/lib/module/networking/xmpp/flushLastViewedToPrivateStore.js.map +1 -1
  52. package/lib/module/networking/xmppClient.js.map +1 -1
  53. package/lib/module/roomStore/roomsSlice.js +1 -1
  54. package/lib/module/roomStore/roomsSlice.js.map +1 -1
  55. package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
  56. package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
  57. package/lib/typescript/components/MainComponents/MessageList.d.ts +10 -1
  58. package/lib/typescript/components/MainComponents/MessageList.d.ts.map +1 -1
  59. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
  60. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  61. package/lib/typescript/helpers/historyPreloadScheduler.d.ts.map +1 -1
  62. package/lib/typescript/helpers/shallowEqual.d.ts +11 -0
  63. package/lib/typescript/helpers/shallowEqual.d.ts.map +1 -0
  64. package/lib/typescript/hooks/useChatWrapperInit.d.ts.map +1 -1
  65. package/lib/typescript/hooks/useComposing.d.ts.map +1 -1
  66. package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
  67. package/lib/typescript/hooks/useUnreadMessagesCounter.d.ts +1 -0
  68. package/lib/typescript/hooks/useUnreadMessagesCounter.d.ts.map +1 -1
  69. package/lib/typescript/networking/xmpp/flushLastViewedToPrivateStore.d.ts +1 -0
  70. package/lib/typescript/networking/xmpp/flushLastViewedToPrivateStore.d.ts.map +1 -1
  71. package/lib/typescript/networking/xmppClient.d.ts +1 -0
  72. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  73. package/lib/typescript/roomStore/roomsSlice.d.ts +1 -0
  74. package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
  75. package/lib/typescript/types/types.d.ts +1 -0
  76. package/lib/typescript/types/types.d.ts.map +1 -1
  77. package/package.json +1 -1
  78. package/src/components/MainComponents/ChatRoom.tsx +63 -1
  79. package/src/components/MainComponents/ChatWrapper.tsx +19 -1
  80. package/src/components/MainComponents/MessageList.tsx +19 -2
  81. package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +8 -6
  82. package/src/context/xmppProvider.tsx +14 -0
  83. package/src/helpers/historyPreloadScheduler.ts +80 -3
  84. package/src/helpers/shallowEqual.ts +33 -0
  85. package/src/hooks/useChatWrapperInit.ts +15 -0
  86. package/src/hooks/useComposing.tsx +37 -1
  87. package/src/hooks/useSendMessage.tsx +32 -2
  88. package/src/hooks/useUnreadMessagesCounter.ts +11 -0
  89. package/src/networking/xmpp/flushLastViewedToPrivateStore.ts +17 -3
  90. package/src/networking/xmppClient.ts +5 -1
  91. package/src/roomStore/roomsSlice.ts +1 -1
  92. package/src/types/types.ts +8 -0
@@ -1,4 +1,5 @@
1
- import { useCallback, useEffect, useState } from 'react';
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+ import { AppState, AppStateStatus } from 'react-native';
2
3
  import { useXmppClient } from '../context/xmppProvider';
3
4
  import { useSelector } from 'react-redux';
4
5
  import { RootState } from '../roomStore';
@@ -9,8 +10,16 @@ const useComposing = (config?: IConfig) => {
9
10
  const { client } = useXmppClient();
10
11
  const { activeRoomJID } = useSelector((state: RootState) => state.rooms);
11
12
  const { user } = useChatSettingState();
13
+ // RN keeps a focused TextInput focused when the app is backgrounded or
14
+ // the device is locked — nothing fires `onBlur`, so without this the
15
+ // other participant sees "typing…" for as long as the user is away.
16
+ // Tracks whether the input is *currently* focused, independent of the
17
+ // foreground/background transition, so we know whether to resume
18
+ // composing on return.
19
+ const isFocusedRef = useRef(false);
12
20
 
13
21
  const sendStartComposing = useCallback(() => {
22
+ isFocusedRef.current = true;
14
23
  if (config?.disableTypingIndicator) {
15
24
  return;
16
25
  }
@@ -24,6 +33,7 @@ const useComposing = (config?: IConfig) => {
24
33
  }, [activeRoomJID, client, config?.disableTypingIndicator, user.firstName, user.lastName]);
25
34
 
26
35
  const sendEndComposing = useCallback(() => {
36
+ isFocusedRef.current = false;
27
37
  if (config?.disableTypingIndicator) {
28
38
  return;
29
39
  }
@@ -44,6 +54,32 @@ const useComposing = (config?: IConfig) => {
44
54
  return () => clearTimeout(timerId);
45
55
  }, [sendEndComposing]);
46
56
 
57
+ // Foreground/background transition only — a pause in typing must NOT
58
+ // clear the indicator (that would flicker for a slowly-composing user),
59
+ // so this deliberately does not hook keystroke/idle timing at all.
60
+ useEffect(() => {
61
+ const handleAppStateChange = (nextState: AppStateStatus) => {
62
+ if (nextState !== 'active') {
63
+ if (isFocusedRef.current && !config?.disableTypingIndicator && client) {
64
+ client.sendTypingRequestStanza(
65
+ activeRoomJID || '',
66
+ `${user.firstName} ${user.lastName}`,
67
+ false
68
+ );
69
+ }
70
+ } else if (isFocusedRef.current && !config?.disableTypingIndicator && client) {
71
+ client.sendTypingRequestStanza(
72
+ activeRoomJID || '',
73
+ `${user.firstName} ${user.lastName}`,
74
+ true
75
+ );
76
+ }
77
+ };
78
+
79
+ const subscription = AppState.addEventListener('change', handleAppStateChange);
80
+ return () => subscription.remove();
81
+ }, [activeRoomJID, client, config?.disableTypingIndicator, user.firstName, user.lastName]);
82
+
47
83
  return { sendStartComposing, sendEndComposing };
48
84
  };
49
85
 
@@ -10,7 +10,7 @@ import {
10
10
  markMessageFailed,
11
11
  } from '../roomStore/roomHeapSlice';
12
12
  import { uploadFileV2 } from '../networking/api-requests/auth.api';
13
- import { enqueueOutboundSend } from '../networking/outboundQueue';
13
+ import { enqueueOutboundSend, removeOutboundSend } from '../networking/outboundQueue';
14
14
  import { RootState, store } from '../roomStore';
15
15
  import { getGlobalXmppClient } from '../utils/clientRegistry';
16
16
  import { useEventHandlers } from './useEventHandlers';
@@ -214,6 +214,13 @@ export const useSendMessage = (_configOverride?: IConfig) => {
214
214
  console.warn(
215
215
  `Send watchdog fired — message ${optimisticId} stuck pending > ${PENDING_WATCHDOG_MS}ms; marking failed`
216
216
  );
217
+ // Drop the buffered replay now that the bubble is showing
218
+ // "failed / tap to retry" — a later onOnline flush must not
219
+ // silently resend it behind the user's back. If the original
220
+ // send actually lands after this point, the server echo still
221
+ // flips the bubble back to delivered (newMessageMidlleware);
222
+ // only an explicit tap-to-retry should trigger another send.
223
+ removeOutboundSend(optimisticId);
217
224
  dispatch(
218
225
  markMessageFailed({
219
226
  kind: 'text',
@@ -308,6 +315,9 @@ export const useSendMessage = (_configOverride?: IConfig) => {
308
315
  });
309
316
  } catch (error) {
310
317
  clearTimeout(watchdogTimer);
318
+ // Same reasoning as the watchdog: once the bubble shows failed,
319
+ // only an explicit tap-to-retry should resend it.
320
+ removeOutboundSend(optimisticId);
311
321
  dispatch(
312
322
  markMessageFailed({
313
323
  kind: 'text',
@@ -454,6 +464,10 @@ export const useSendMessage = (_configOverride?: IConfig) => {
454
464
  console.warn(
455
465
  `Media-send watchdog fired — ${id} stuck pending > ${PENDING_WATCHDOG_MS}ms; marking failed`
456
466
  );
467
+ // See the text-send watchdog above: drop the buffered replay so
468
+ // a later onOnline flush can't silently resend behind the
469
+ // "failed / tap to retry" bubble.
470
+ removeOutboundSend(id);
457
471
  dispatch(
458
472
  markMessageFailed({
459
473
  kind: 'media',
@@ -661,6 +675,22 @@ export const useSendMessage = (_configOverride?: IConfig) => {
661
675
  async (failedId: string) => {
662
676
  const payload = failedMessages[failedId];
663
677
  if (!payload) {return;}
678
+
679
+ // The server may have accepted the original send even though its
680
+ // echo arrived too late to beat the watchdog — the bubble shows
681
+ // failed, but a confirmed (non-pending) copy is already sitting in
682
+ // the room. Resending here would deliver a genuine duplicate; just
683
+ // clear the failure and let the existing copy stand.
684
+ const roomMsgs = rooms?.[payload.roomJID]?.messages || [];
685
+ const alreadyDelivered = roomMsgs.some(
686
+ (m: any) =>
687
+ !m.pending && (m.id === failedId || m.xmppId === failedId)
688
+ );
689
+ if (alreadyDelivered) {
690
+ dispatch(clearMessageFailure(failedId));
691
+ return;
692
+ }
693
+
664
694
  dispatch(clearMessageFailure(failedId));
665
695
  handleMessageRetry({
666
696
  messageId: failedId,
@@ -690,7 +720,7 @@ export const useSendMessage = (_configOverride?: IConfig) => {
690
720
  );
691
721
  }
692
722
  },
693
- [failedMessages, dispatch, handleMessageRetry, sendMessage, sendMedia]
723
+ [failedMessages, rooms, dispatch, handleMessageRetry, sendMessage, sendMedia]
694
724
  );
695
725
 
696
726
  // ChatRoom/ThreadWrapper consume this as the "edit branch" of send.
@@ -11,6 +11,12 @@ interface UnreadMessagesStats {
11
11
  totalCount: number;
12
12
  unreadByRoom: UnreadMessagesMap;
13
13
  isLoading: boolean;
14
+ // True while at least one room's unread count could still be a floor
15
+ // rather than the true total — the history-preload scheduler paged back
16
+ // as far as its catch-up budget allows without finding the read
17
+ // boundary. Lets a host show "99+"-style affordances honestly instead of
18
+ // implying an exact count it can't back up.
19
+ isCapped: boolean;
14
20
  }
15
21
 
16
22
  export const useUnreadMessagesCounter = (): UnreadMessagesStats => {
@@ -35,6 +41,7 @@ export const useUnreadMessagesCounter = (): UnreadMessagesStats => {
35
41
  const rooms = roomsState.rooms;
36
42
  const unreadByRoom: UnreadMessagesMap = {};
37
43
  let totalCount = 0;
44
+ let isCapped = false;
38
45
 
39
46
  Object.entries(rooms).forEach(([roomJid, room]: [string, IRoom]) => {
40
47
  const unreadCount = room.unreadMessages || 0;
@@ -42,6 +49,9 @@ export const useUnreadMessagesCounter = (): UnreadMessagesStats => {
42
49
  unreadByRoom[roomJid] = unreadCount;
43
50
  totalCount += unreadCount;
44
51
  }
52
+ if ((room as any)?.unreadCapped) {
53
+ isCapped = true;
54
+ }
45
55
  });
46
56
 
47
57
  return {
@@ -49,6 +59,7 @@ export const useUnreadMessagesCounter = (): UnreadMessagesStats => {
49
59
  totalCount,
50
60
  unreadByRoom,
51
61
  isLoading: !!roomsState.isUnreadSyncing || !!roomsState.isLoading,
62
+ isCapped,
52
63
  };
53
64
  };
54
65
 
@@ -12,6 +12,14 @@ interface FlushOpts {
12
12
  // up to now" and stamp it with Date.now() — even if its in-memory
13
13
  // lastViewedTimestamp has not been updated yet.
14
14
  visibleRoomJID?: string | null;
15
+ // Explicit read boundary for the visible room, used INSTEAD of
16
+ // Date.now(). ChatRoom passes the timestamp of the newest message the
17
+ // user actually reached when they leave a room while scrolled up —
18
+ // without it, the Date.now() default writes "read everything" to the
19
+ // server and the messages they never scrolled down to come back as
20
+ // read on the next login, even though the local count was correct.
21
+ // Customer-reported #33 (server half).
22
+ visibleRoomTs?: number | null;
15
23
  // If true, only write entries for rooms where unreadMessages === 0
16
24
  // (or the visible room). Used at logout time: rooms with outstanding
17
25
  // unread keep their old marker so the next login still surfaces them.
@@ -34,7 +42,7 @@ export async function flushLastViewedToPrivateStore(
34
42
  opts: FlushOpts = {}
35
43
  ): Promise<boolean> {
36
44
  if (!client?.client) {return false;}
37
- const { visibleRoomJID, onlyIfNoUnread } = opts;
45
+ const { visibleRoomJID, visibleRoomTs, onlyIfNoUnread } = opts;
38
46
  const roomList = Object.values(rooms || {});
39
47
  if (roomList.length === 0) {return false;}
40
48
 
@@ -55,10 +63,16 @@ export async function flushLastViewedToPrivateStore(
55
63
  const hasUnread = Number(room.unreadMessages || 0) > 0;
56
64
 
57
65
  // For the visible room, persist "now" — read state is driven by
58
- // room visibility, not by a sentinel timestamp.
66
+ // room visibility, not by a sentinel timestamp. The exception is an
67
+ // explicit `visibleRoomTs`: the caller knows the user only read up
68
+ // to a certain point (left the room while scrolled up), so stamping
69
+ // "now" here would mark genuinely-unread messages as read.
59
70
  let ts: number | undefined;
60
71
  if (isVisible) {
61
- ts = nowMs;
72
+ ts =
73
+ typeof visibleRoomTs === 'number' && visibleRoomTs > 0
74
+ ? visibleRoomTs
75
+ : nowMs;
62
76
  } else if (room.lastViewedTimestamp && room.lastViewedTimestamp > 0) {
63
77
  ts = room.lastViewedTimestamp;
64
78
  }
@@ -1039,7 +1039,11 @@ export class XmppClient {
1039
1039
  // src/networking/xmpp/flushLastViewedToPrivateStore.ts for details.
1040
1040
  async flushLastViewedToPrivateStoreStanza(
1041
1041
  rooms: Record<string, any> | null | undefined,
1042
- opts: { visibleRoomJID?: string | null; onlyIfNoUnread?: boolean } = {}
1042
+ opts: {
1043
+ visibleRoomJID?: string | null;
1044
+ visibleRoomTs?: number | null;
1045
+ onlyIfNoUnread?: boolean;
1046
+ } = {}
1043
1047
  ) {
1044
1048
  if (this.disableLastRead) {return false;}
1045
1049
  try {
@@ -13,7 +13,7 @@ import type XmppClient from '../networking/xmppClient';
13
13
  // older history added via unshift/splice is exempt, so the user can
14
14
  // page backward arbitrarily far without instantly losing what they
15
15
  // just fetched. After cap eviction we drop from the head (oldest).
16
- const RUNTIME_MESSAGE_LIMIT = 100;
16
+ export const RUNTIME_MESSAGE_LIMIT = 100;
17
17
 
18
18
  // Body strings the server uses for call signaling broadcasts (call-token,
19
19
  // call-state ringing/ended, etc). These should never reach the chat
@@ -571,6 +571,14 @@ export interface IConfig {
571
571
  // `disableKeyboardAvoidingView` is set. Needs on-device tuning of any
572
572
  // `keyboardVerticalOffset`.
573
573
  keyboardStickyInput?: boolean;
574
+ // The message list's FlatList uses keyboardDismissMode="interactive" by
575
+ // default, so a drag can be used to pull the keyboard closed. Because
576
+ // the list is `inverted`, an ordinary scroll gesture through history can
577
+ // also be read as a dismiss drag, closing the keyboard mid-scroll. Set
578
+ // this to keep the keyboard open regardless of how the list is scrolled
579
+ // — dismissal then only happens when the user deliberately closes it
580
+ // (tapping outside, back button, etc). Customer-reported #36.
581
+ keepKeyboardOpenOnScroll?: boolean;
574
582
 
575
583
  // ----- interactions / messages -----
576
584
  disableInteractions?: boolean;