@ethora/chat-component-rn 26.5.12 → 26.5.14

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 (61) hide show
  1. package/README.md +1 -0
  2. package/lib/commonjs/components/MainComponents/ChatHeader.js +4 -1
  3. package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
  4. package/lib/commonjs/components/MainComponents/ChatRoom.js +59 -39
  5. package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
  6. package/lib/commonjs/components/MainComponents/ReduxWrapper.js +24 -15
  7. package/lib/commonjs/components/MainComponents/ReduxWrapper.js.map +1 -1
  8. package/lib/commonjs/context/xmppProvider.js +63 -0
  9. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  10. package/lib/commonjs/networking/xmppClient.js +26 -1
  11. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  12. package/lib/commonjs/roomStore/Middleware/unreadMidlleware.js +10 -10
  13. package/lib/commonjs/roomStore/Middleware/unreadMidlleware.js.map +1 -1
  14. package/lib/commonjs/roomStore/index.js +5 -4
  15. package/lib/commonjs/roomStore/index.js.map +1 -1
  16. package/lib/commonjs/roomStore/roomsSlice.js +59 -4
  17. package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
  18. package/lib/commonjs/utils/clientRegistry.js +19 -16
  19. package/lib/commonjs/utils/clientRegistry.js.map +1 -1
  20. package/lib/module/components/MainComponents/ChatHeader.js +4 -1
  21. package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
  22. package/lib/module/components/MainComponents/ChatRoom.js +61 -41
  23. package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
  24. package/lib/module/components/MainComponents/ReduxWrapper.js +24 -15
  25. package/lib/module/components/MainComponents/ReduxWrapper.js.map +1 -1
  26. package/lib/module/context/xmppProvider.js +65 -2
  27. package/lib/module/context/xmppProvider.js.map +1 -1
  28. package/lib/module/networking/xmppClient.js +26 -1
  29. package/lib/module/networking/xmppClient.js.map +1 -1
  30. package/lib/module/roomStore/Middleware/unreadMidlleware.js +10 -10
  31. package/lib/module/roomStore/Middleware/unreadMidlleware.js.map +1 -1
  32. package/lib/module/roomStore/index.js +5 -4
  33. package/lib/module/roomStore/index.js.map +1 -1
  34. package/lib/module/roomStore/roomsSlice.js +57 -3
  35. package/lib/module/roomStore/roomsSlice.js.map +1 -1
  36. package/lib/module/utils/clientRegistry.js +19 -16
  37. package/lib/module/utils/clientRegistry.js.map +1 -1
  38. package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
  39. package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
  40. package/lib/typescript/components/MainComponents/ReduxWrapper.d.ts.map +1 -1
  41. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  42. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  43. package/lib/typescript/roomStore/Middleware/unreadMidlleware.d.ts.map +1 -1
  44. package/lib/typescript/roomStore/index.d.ts +1 -1
  45. package/lib/typescript/roomStore/index.d.ts.map +1 -1
  46. package/lib/typescript/roomStore/roomsSlice.d.ts +18 -1
  47. package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
  48. package/lib/typescript/types/types.d.ts +3 -0
  49. package/lib/typescript/types/types.d.ts.map +1 -1
  50. package/lib/typescript/utils/clientRegistry.d.ts.map +1 -1
  51. package/package.json +1 -1
  52. package/src/components/MainComponents/ChatHeader.tsx +3 -2
  53. package/src/components/MainComponents/ChatRoom.tsx +60 -43
  54. package/src/components/MainComponents/ReduxWrapper.tsx +23 -9
  55. package/src/context/xmppProvider.tsx +55 -1
  56. package/src/networking/xmppClient.ts +24 -1
  57. package/src/roomStore/Middleware/unreadMidlleware.tsx +11 -8
  58. package/src/roomStore/index.ts +51 -39
  59. package/src/roomStore/roomsSlice.ts +59 -2
  60. package/src/types/types.ts +20 -0
  61. package/src/utils/clientRegistry.ts +32 -16
@@ -15,34 +15,45 @@ const rootReducer = combineReducers({
15
15
  roomHeapSlice: roomHeapSlice.reducer,
16
16
  });
17
17
 
18
- export const store = configureStore({
19
- reducer: rootReducer,
20
- middleware: (getDefaultMiddleware) =>
21
- getDefaultMiddleware({
22
- serializableCheck: {
23
- // Slice names: chatSlice→'chat', roomsStore→'roomMessages'.
24
- ignoredActions: [
25
- 'chat/addMessage',
26
- 'chat/setStoreClient',
27
- 'chat/setConfig',
28
- 'roomMessages/addRoom',
29
- ],
30
- ignoredActionPaths: ['payload.client', 'payload.config'],
31
- ignoredPaths: [
32
- 'chat.messages.timestamp',
33
- 'chatSettingStore.client',
34
- 'chatSettingStore.config',
35
- ],
36
- },
37
- })
38
- .concat(unreadMiddleware)
39
- .concat(newMessageMidlleware)
40
- .concat(reactionsMiddleware)
41
- .concat(logoutMiddleware)
42
- .concat(persistenceMiddleware),
43
- });
44
-
45
18
  export type RootState = ReturnType<typeof rootReducer>;
19
+
20
+ const createChatStore = () =>
21
+ configureStore({
22
+ reducer: rootReducer,
23
+ middleware: (getDefaultMiddleware) =>
24
+ getDefaultMiddleware({
25
+ serializableCheck: {
26
+ // Slice names: chatSlice→'chat', roomsStore→'roomMessages'.
27
+ ignoredActions: [
28
+ 'chat/addMessage',
29
+ 'chat/setStoreClient',
30
+ 'chat/setConfig',
31
+ 'roomMessages/addRoom',
32
+ ],
33
+ ignoredActionPaths: ['payload.client', 'payload.config'],
34
+ ignoredPaths: [
35
+ 'chat.messages.timestamp',
36
+ 'chatSettingStore.client',
37
+ 'chatSettingStore.config',
38
+ ],
39
+ },
40
+ })
41
+ .concat(unreadMiddleware)
42
+ .concat(newMessageMidlleware)
43
+ .concat(reactionsMiddleware)
44
+ .concat(logoutMiddleware)
45
+ .concat(persistenceMiddleware),
46
+ });
47
+
48
+ const globalScope = globalThis as typeof globalThis & {
49
+ __CHAT_STORE__?: ReturnType<typeof createChatStore>;
50
+ __CHAT_PERSISTOR_READY__?: Promise<void>;
51
+ };
52
+
53
+ export const store =
54
+ globalScope.__CHAT_STORE__ ||
55
+ (globalScope.__CHAT_STORE__ = createChatStore());
56
+
46
57
  export type AppDispatch = typeof store.dispatch;
47
58
 
48
59
  export const getActiveRoom = (state: RootState): IRoom | null => {
@@ -53,16 +64,17 @@ export const getActiveRoom = (state: RootState): IRoom | null => {
53
64
  };
54
65
 
55
66
  // Async rehydrate — read persisted slices and replay them as standard
56
- // actions. Idempotent: only rehydrates when a slice has data.
57
- export const persistorReady = (async () => {
58
- const { chat, rooms } = await readPersistedState();
59
- if (chat?.user && chat.user.walletAddress) {
60
- store.dispatch(setUser(chat.user));
61
- }
62
- if (rooms?.rooms) {
63
- for (const [jid, room] of Object.entries(rooms.rooms)) {
64
- if (!jid || !room) {continue;}
65
- store.dispatch(addRoom({ roomData: room as IRoom }));
67
+ export const persistorReady =
68
+ globalScope.__CHAT_PERSISTOR_READY__ ||
69
+ (globalScope.__CHAT_PERSISTOR_READY__ = (async () => {
70
+ const { chat, rooms } = await readPersistedState();
71
+ if (chat?.user && chat.user.walletAddress) {
72
+ store.dispatch(setUser(chat.user));
73
+ }
74
+ if (rooms?.rooms) {
75
+ for (const [jid, room] of Object.entries(rooms.rooms)) {
76
+ if (!jid || !room) {continue;}
77
+ store.dispatch(addRoom({ roomData: room as IRoom }));
78
+ }
66
79
  }
67
- }
68
- })();
80
+ })());
@@ -98,6 +98,12 @@ export interface RoomMessagesState {
98
98
  loadingText?: string;
99
99
  usersSet?: Record<string, any>;
100
100
  pendingNotificationJid?: string | null;
101
+ // Server-side read markers fetched from the XMPP private store
102
+ // (`{ roomJID: lastViewedMs }`). Kept here so a room that loads AFTER
103
+ // the markers were fetched still inherits its baseline via `addRoom`
104
+ // — see `applyPrivateStoreMarkers`. Re-fetched every init/reconnect,
105
+ // so it is intentionally NOT persisted.
106
+ privateStoreMarkers: Record<string, number>;
101
107
  }
102
108
 
103
109
  const initialState: RoomMessagesState = {
@@ -112,6 +118,7 @@ const initialState: RoomMessagesState = {
112
118
  text: '',
113
119
  },
114
120
  pendingNotificationJid: null,
121
+ privateStoreMarkers: {},
115
122
  };
116
123
 
117
124
  const isValidRoomJid = (jid: unknown): jid is string => {
@@ -160,11 +167,19 @@ const reducers = {
160
167
  // the previous session, so cold-start showed no unread badge for
161
168
  // messages received while the app was closed (bug #19/#20). A real
162
169
  // (non-zero) explicit value still wins; otherwise keep the existing
163
- // value; otherwise anchor to the newest message in the payload.
170
+ // value; otherwise fall back to the server-side read marker fetched
171
+ // from the private store (so a room the user has NEVER opened this
172
+ // session still gets a real baseline — without it the unread
173
+ // middleware's `lastViewedTimestamp > 0` gate skipped the room
174
+ // forever and the badge never lit up); otherwise anchor to the
175
+ // newest message in the payload.
176
+ const serverMarker = state.privateStoreMarkers?.[roomData.jid] || 0;
164
177
  if (roomData.lastViewedTimestamp != null && roomData.lastViewedTimestamp !== 0) {
165
178
  lastViewed = roomData.lastViewedTimestamp;
166
- } else if (existing?.lastViewedTimestamp != null) {
179
+ } else if (existing?.lastViewedTimestamp != null && existing.lastViewedTimestamp > 0) {
167
180
  lastViewed = existing.lastViewedTimestamp;
181
+ } else if (serverMarker > 0) {
182
+ lastViewed = serverMarker;
168
183
  } else {
169
184
  const msgs = roomData.messages || [];
170
185
  let newest = 0;
@@ -346,6 +361,7 @@ const reducers = {
346
361
  deleteAllRooms(state: WritableDraft<RoomMessagesState>) {
347
362
  state.rooms = {};
348
363
  state.visibleRoomJID = null;
364
+ state.privateStoreMarkers = {};
349
365
  },
350
366
  setComposing(
351
367
  state: WritableDraft<RoomMessagesState>,
@@ -392,6 +408,45 @@ const reducers = {
392
408
  }
393
409
  }
394
410
  },
411
+ /**
412
+ * Apply server-side read markers fetched from the XMPP private store
413
+ * (`{ roomJID: lastViewedMs }`) to redux. This is the hydration that
414
+ * was missing: `getChatsPrivateStoreRequest` fetched the markers on
415
+ * every init/reconnect and every caller then DISCARDED the result, so
416
+ * rooms kept `lastViewedTimestamp: 0` and the unread middleware's
417
+ * `> 0` gate skipped them forever — `useUnread()` stayed at 0 for any
418
+ * room the user hadn't locally opened+left this session. Centralised
419
+ * here (and consulted by `addRoom`) so BOTH already-loaded rooms and
420
+ * rooms that load later pick up the baseline.
421
+ *
422
+ * Monotonic: a marker only ever moves a baseline FORWARD, so a stale
423
+ * server value can't resurrect already-read messages and a more-recent
424
+ * local read (tab blur stamping Date.now()) always wins.
425
+ */
426
+ applyPrivateStoreMarkers: (
427
+ state: WritableDraft<RoomMessagesState>,
428
+ action: PayloadAction<Record<string, number>>
429
+ ) => {
430
+ const markers = action.payload;
431
+ if (!markers || typeof markers !== 'object') {return;}
432
+ if (!state.privateStoreMarkers) {state.privateStoreMarkers = {};}
433
+ for (const jid of Object.keys(markers)) {
434
+ const ts = Number(markers[jid]);
435
+ if (!jid || !Number.isFinite(ts) || ts <= 0) {continue;}
436
+ // Remember the marker so rooms that load LATER (via addRoom)
437
+ // inherit it even though they don't exist in the store yet.
438
+ if (ts > (state.privateStoreMarkers[jid] || 0)) {
439
+ state.privateStoreMarkers[jid] = ts;
440
+ }
441
+ // Upgrade an already-loaded room's baseline + recompute its badge.
442
+ // Only ever forward, so a later local read isn't clobbered.
443
+ const room = state.rooms[jid];
444
+ if (room && ts > (room.lastViewedTimestamp || 0)) {
445
+ room.lastViewedTimestamp = ts;
446
+ room.unreadMessages = countNewerMessages(room.messages, ts);
447
+ }
448
+ }
449
+ },
395
450
  setRoomRole: (
396
451
  state: WritableDraft<RoomMessagesState>,
397
452
  action: PayloadAction<{ chatJID: string; role: string }>
@@ -483,6 +538,7 @@ const reducers = {
483
538
  state.activeRoomJID = null;
484
539
  state.visibleRoomJID = null;
485
540
  state.isLoading = false;
541
+ state.privateStoreMarkers = {};
486
542
  },
487
543
  setActiveMessage: (
488
544
  state: WritableDraft<RoomMessagesState>,
@@ -674,6 +730,7 @@ export const {
674
730
  setComposing,
675
731
  setIsLoading,
676
732
  setLastViewedTimestamp,
733
+ applyPrivateStoreMarkers,
677
734
  setRoomNoMessages,
678
735
  setCurrentRoom,
679
736
  setVisibleRoom,
@@ -348,6 +348,7 @@ export interface IConfig {
348
348
  disableMembers?: boolean;
349
349
  hideMembers?: boolean;
350
350
  disableChatHeaderMenu?: boolean;
351
+ disableRoomMenu?: boolean;
351
352
  /**
352
353
  * Disables uploading / removing the chat icon from the chat-info
353
354
  * modal. The avatar stays rendered (initials / current icon) but
@@ -372,6 +373,25 @@ export interface IConfig {
372
373
  chatRoomStyles?: ViewStyle;
373
374
 
374
375
  keyboardVerticalOffset?: number;
376
+ // Opt OUT of the built-in keyboard handling. By default <Chat> mounts
377
+ // its own react-native-keyboard-controller KeyboardProvider +
378
+ // KeyboardAvoidingView (behavior="padding"). If the HOST app already
379
+ // wraps <Chat> in its own KeyboardProvider / KeyboardAvoidingView, the
380
+ // built-in one becomes a SECOND avoider over the same tree — two of them
381
+ // animating padding on keyboard open is the Android flicker in bug #6.
382
+ // Set this so the component renders a plain View instead and drops its
383
+ // KeyboardProvider, leaving the host to own keyboard handling outright.
384
+ disableKeyboardAvoidingView?: boolean;
385
+ // Built-in keyboard strategy: instead of the default KeyboardAvoidingView
386
+ // (which adds bottom padding to the WHOLE chat tree — the message list
387
+ // reflows on keyboard open, the Android "messages jump/flash" in bug #6),
388
+ // wrap ONLY the input dock in a react-native-keyboard-controller
389
+ // KeyboardStickyView so just the input tracks the keyboard and the list
390
+ // is never resized. Best for edge-to-edge Android hosts (where the OS
391
+ // doesn't also resize the window). Ignored when
392
+ // `disableKeyboardAvoidingView` is set. Needs on-device tuning of any
393
+ // `keyboardVerticalOffset`.
394
+ keyboardStickyInput?: boolean;
375
395
 
376
396
  // ----- interactions / messages -----
377
397
  disableInteractions?: boolean;
@@ -1,9 +1,23 @@
1
1
  import XmppClient from '../networking/xmppClient';
2
2
  import { xmppSettingsInterface } from '../types/types';
3
3
 
4
- let currentClient: XmppClient | null = null;
5
- let currentClientKey = '';
6
- const initLocks = new Map<string, Promise<XmppClient>>();
4
+ interface XmppRegistryState {
5
+ currentClient: XmppClient | null;
6
+ currentClientKey: string;
7
+ initLocks: Map<string, Promise<XmppClient>>;
8
+ }
9
+
10
+ const globalScope = globalThis as typeof globalThis & {
11
+ __XMPP_REGISTRY__?: XmppRegistryState;
12
+ };
13
+
14
+ const registry: XmppRegistryState =
15
+ globalScope.__XMPP_REGISTRY__ ||
16
+ (globalScope.__XMPP_REGISTRY__ = {
17
+ currentClient: null,
18
+ currentClientKey: '',
19
+ initLocks: new Map<string, Promise<XmppClient>>(),
20
+ });
7
21
 
8
22
  const DEFAULT_DEV_SERVER = 'xmpp.chat.ethora.com';
9
23
 
@@ -26,46 +40,48 @@ export function setGlobalXmppClient(
26
40
  client: XmppClient | null,
27
41
  key?: string
28
42
  ): void {
29
- currentClient = client;
30
- currentClientKey = client ? key || currentClientKey || '' : '';
43
+ registry.currentClient = client;
44
+ registry.currentClientKey = client
45
+ ? key || registry.currentClientKey || ''
46
+ : '';
31
47
  }
32
48
 
33
49
  export function getGlobalXmppClient(): XmppClient | null {
34
- return currentClient;
50
+ return registry.currentClient;
35
51
  }
36
52
 
37
53
  export function getGlobalXmppClientKey(): string {
38
- return currentClientKey;
54
+ return registry.currentClientKey;
39
55
  }
40
56
 
41
57
  export function getReusableXmppClientByKey(key: string): XmppClient | null {
42
- if (!key || key !== currentClientKey) {return null;}
43
- if (!isXmppClientReusable(currentClient)) {return null;}
44
- return currentClient;
58
+ if (!key || key !== registry.currentClientKey) {return null;}
59
+ if (!isXmppClientReusable(registry.currentClient)) {return null;}
60
+ return registry.currentClient;
45
61
  }
46
62
 
47
63
  export async function withXmppClientInitLock(
48
64
  key: string,
49
65
  init: () => Promise<XmppClient>
50
66
  ): Promise<XmppClient> {
51
- const existing = initLocks.get(key);
67
+ const existing = registry.initLocks.get(key);
52
68
  if (existing) {return existing;}
53
69
 
54
70
  const createdPromise = init().finally(() => {
55
- if (initLocks.get(key) === createdPromise) {
56
- initLocks.delete(key);
71
+ if (registry.initLocks.get(key) === createdPromise) {
72
+ registry.initLocks.delete(key);
57
73
  }
58
74
  });
59
75
 
60
- initLocks.set(key, createdPromise);
76
+ registry.initLocks.set(key, createdPromise);
61
77
  return createdPromise;
62
78
  }
63
79
 
64
80
  export function requireXmppClient(): XmppClient {
65
- if (!currentClient) {
81
+ if (!registry.currentClient) {
66
82
  throw new Error('XMPP client is not initialized');
67
83
  }
68
- return currentClient;
84
+ return registry.currentClient;
69
85
  }
70
86
 
71
87
  export default {