@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.
- package/README.md +1 -0
- package/lib/commonjs/components/MainComponents/ChatHeader.js +4 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatRoom.js +59 -39
- package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ReduxWrapper.js +24 -15
- package/lib/commonjs/components/MainComponents/ReduxWrapper.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +63 -0
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/networking/xmppClient.js +26 -1
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/roomStore/Middleware/unreadMidlleware.js +10 -10
- package/lib/commonjs/roomStore/Middleware/unreadMidlleware.js.map +1 -1
- package/lib/commonjs/roomStore/index.js +5 -4
- package/lib/commonjs/roomStore/index.js.map +1 -1
- package/lib/commonjs/roomStore/roomsSlice.js +59 -4
- package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
- package/lib/commonjs/utils/clientRegistry.js +19 -16
- package/lib/commonjs/utils/clientRegistry.js.map +1 -1
- package/lib/module/components/MainComponents/ChatHeader.js +4 -1
- package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/module/components/MainComponents/ChatRoom.js +61 -41
- package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/module/components/MainComponents/ReduxWrapper.js +24 -15
- package/lib/module/components/MainComponents/ReduxWrapper.js.map +1 -1
- package/lib/module/context/xmppProvider.js +65 -2
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/networking/xmppClient.js +26 -1
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/roomStore/Middleware/unreadMidlleware.js +10 -10
- package/lib/module/roomStore/Middleware/unreadMidlleware.js.map +1 -1
- package/lib/module/roomStore/index.js +5 -4
- package/lib/module/roomStore/index.js.map +1 -1
- package/lib/module/roomStore/roomsSlice.js +57 -3
- package/lib/module/roomStore/roomsSlice.js.map +1 -1
- package/lib/module/utils/clientRegistry.js +19 -16
- package/lib/module/utils/clientRegistry.js.map +1 -1
- package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ReduxWrapper.d.ts.map +1 -1
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/roomStore/Middleware/unreadMidlleware.d.ts.map +1 -1
- package/lib/typescript/roomStore/index.d.ts +1 -1
- package/lib/typescript/roomStore/index.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts +18 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +3 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/lib/typescript/utils/clientRegistry.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/MainComponents/ChatHeader.tsx +3 -2
- package/src/components/MainComponents/ChatRoom.tsx +60 -43
- package/src/components/MainComponents/ReduxWrapper.tsx +23 -9
- package/src/context/xmppProvider.tsx +55 -1
- package/src/networking/xmppClient.ts +24 -1
- package/src/roomStore/Middleware/unreadMidlleware.tsx +11 -8
- package/src/roomStore/index.ts +51 -39
- package/src/roomStore/roomsSlice.ts +59 -2
- package/src/types/types.ts +20 -0
- package/src/utils/clientRegistry.ts +32 -16
package/src/roomStore/index.ts
CHANGED
|
@@ -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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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,
|
package/src/types/types.ts
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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 {
|