@ethora/chat-component-rn 26.7.9 → 26.7.10
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/CHANGELOG.md +3 -0
- package/README.md +10 -0
- package/lib/commonjs/components/MainComponents/ChatRoom.js +63 -15
- package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +58 -14
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/getServerReadTimestamp.js +66 -3
- package/lib/commonjs/helpers/getServerReadTimestamp.js.map +1 -1
- package/lib/commonjs/helpers/insertMessageWithDelimiter.js +51 -9
- package/lib/commonjs/helpers/insertMessageWithDelimiter.js.map +1 -1
- package/lib/commonjs/helpers/keyboardLayout.js +12 -2
- package/lib/commonjs/helpers/keyboardLayout.js.map +1 -1
- package/lib/commonjs/helpers/msgSortableMs.js +37 -0
- package/lib/commonjs/helpers/msgSortableMs.js.map +1 -0
- package/lib/commonjs/hooks/useChatRoomFocus.js +31 -7
- package/lib/commonjs/hooks/useChatRoomFocus.js.map +1 -1
- package/lib/commonjs/hooks/useLogout.js +8 -0
- package/lib/commonjs/hooks/useLogout.js.map +1 -1
- package/lib/commonjs/roomStore/roomsSlice.js +66 -32
- package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
- package/lib/module/components/MainComponents/ChatRoom.js +65 -17
- package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/module/context/xmppProvider.js +59 -15
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/getServerReadTimestamp.js +63 -2
- package/lib/module/helpers/getServerReadTimestamp.js.map +1 -1
- package/lib/module/helpers/insertMessageWithDelimiter.js +52 -9
- package/lib/module/helpers/insertMessageWithDelimiter.js.map +1 -1
- package/lib/module/helpers/keyboardLayout.js +12 -2
- package/lib/module/helpers/keyboardLayout.js.map +1 -1
- package/lib/module/helpers/msgSortableMs.js +32 -0
- package/lib/module/helpers/msgSortableMs.js.map +1 -0
- package/lib/module/hooks/useChatRoomFocus.js +33 -9
- package/lib/module/hooks/useChatRoomFocus.js.map +1 -1
- package/lib/module/hooks/useLogout.js +8 -0
- package/lib/module/hooks/useLogout.js.map +1 -1
- package/lib/module/roomStore/roomsSlice.js +53 -27
- package/lib/module/roomStore/roomsSlice.js.map +1 -1
- package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/helpers/getServerReadTimestamp.d.ts +40 -1
- package/lib/typescript/helpers/getServerReadTimestamp.d.ts.map +1 -1
- package/lib/typescript/helpers/insertMessageWithDelimiter.d.ts +1 -1
- package/lib/typescript/helpers/insertMessageWithDelimiter.d.ts.map +1 -1
- package/lib/typescript/helpers/keyboardLayout.d.ts +5 -1
- package/lib/typescript/helpers/keyboardLayout.d.ts.map +1 -1
- package/lib/typescript/helpers/msgSortableMs.d.ts +2 -0
- package/lib/typescript/helpers/msgSortableMs.d.ts.map +1 -0
- package/lib/typescript/hooks/useChatRoomFocus.d.ts.map +1 -1
- package/lib/typescript/hooks/useLogout.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts +29 -2
- package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +1 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/MainComponents/ChatRoom.tsx +68 -17
- package/src/context/xmppProvider.tsx +87 -18
- package/src/helpers/getServerReadTimestamp.ts +79 -2
- package/src/helpers/insertMessageWithDelimiter.ts +55 -19
- package/src/helpers/keyboardLayout.ts +23 -2
- package/src/helpers/msgSortableMs.ts +25 -0
- package/src/hooks/useChatRoomFocus.ts +39 -8
- package/src/hooks/useLogout.tsx +14 -0
- package/src/roomStore/roomsSlice.ts +74 -22
- package/src/types/types.ts +14 -0
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react';
|
|
2
2
|
import { useDispatch } from 'react-redux';
|
|
3
3
|
import {
|
|
4
|
+
clearReadBoundary,
|
|
4
5
|
clearVisibleRoom,
|
|
5
6
|
setCurrentRoom,
|
|
6
7
|
setLastViewedTimestamp,
|
|
7
8
|
setVisibleRoom,
|
|
8
9
|
} from '../roomStore/roomsSlice';
|
|
9
10
|
import { store } from '../roomStore';
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
getFlushBoundaryTs,
|
|
13
|
+
getReadMarkerTimestamp,
|
|
14
|
+
} from '../helpers/getServerReadTimestamp';
|
|
11
15
|
|
|
12
16
|
interface UseChatRoomFocusOptions {
|
|
13
17
|
/** The room JID that the consumer's tab/screen is currently showing. */
|
|
@@ -62,25 +66,52 @@ export const useChatRoomFocus = ({
|
|
|
62
66
|
isFocused: false,
|
|
63
67
|
});
|
|
64
68
|
|
|
65
|
-
|
|
69
|
+
// `clearBoundary`: pass true only when the room the user is leaving is
|
|
70
|
+
// genuinely being released (switching to a different room, or this
|
|
71
|
+
// hook unmounting) - not on a mere same-room blur. `<ChatRoom>`'s
|
|
72
|
+
// MessageList typically stays mounted across a same-room focus loss
|
|
73
|
+
// (the whole point of this hook is that the host keeps `<Chat>`
|
|
74
|
+
// mounted in a hidden tab), so its own scroll-tracking ref is still
|
|
75
|
+
// intact and the redux boundary must stay in sync with it - clearing
|
|
76
|
+
// it here would desync the two.
|
|
77
|
+
const leaveRoom = (jid: string, clearBoundary: boolean) => {
|
|
66
78
|
const state = store.getState();
|
|
67
79
|
const rooms = state.rooms?.rooms;
|
|
68
|
-
const
|
|
69
|
-
//
|
|
70
|
-
//
|
|
80
|
+
const boundaryTs = state.rooms?.readBoundaries?.[jid] ?? null;
|
|
81
|
+
// getReadMarkerTimestamp: honour the boundary (the newest message the
|
|
82
|
+
// user actually reached) when the user left this room scrolled up,
|
|
83
|
+
// otherwise fall back to the newest server-acked message. Only stamp
|
|
84
|
+
// when we actually have something to anchor to - skip rather than
|
|
85
|
+
// fall back to the device clock (bug #38). Ignoring the boundary here
|
|
86
|
+
// would stamp "everything read" regardless of scroll position (bug
|
|
87
|
+
// #42, a regression of #33 reintroduced by #38).
|
|
88
|
+
const timestamp = getReadMarkerTimestamp(rooms?.[jid], state.roomHeapSlice, boundaryTs);
|
|
71
89
|
if (timestamp > 0) {
|
|
72
90
|
dispatch(setLastViewedTimestamp({ chatJID: jid, timestamp }));
|
|
73
91
|
}
|
|
74
92
|
dispatch(clearVisibleRoom());
|
|
75
93
|
(state.chatSettingStore as any)?.client
|
|
76
|
-
?.flushLastViewedToPrivateStoreStanza(rooms, {
|
|
94
|
+
?.flushLastViewedToPrivateStoreStanza(rooms, {
|
|
95
|
+
visibleRoomJID: jid,
|
|
96
|
+
// Carry the same boundary to the SERVER marker - without it the
|
|
97
|
+
// flush defaults to "everything" for the visible room. Clamped
|
|
98
|
+
// through the same helper as the local stamp above.
|
|
99
|
+
visibleRoomTs: getFlushBoundaryTs(
|
|
100
|
+
rooms?.[jid],
|
|
101
|
+
state.roomHeapSlice,
|
|
102
|
+
boundaryTs
|
|
103
|
+
),
|
|
104
|
+
})
|
|
77
105
|
.catch(() => {});
|
|
106
|
+
if (clearBoundary) {
|
|
107
|
+
dispatch(clearReadBoundary({ jid }));
|
|
108
|
+
}
|
|
78
109
|
};
|
|
79
110
|
|
|
80
111
|
useEffect(() => {
|
|
81
112
|
const prev = prevRef.current;
|
|
82
113
|
if (prev.isFocused && prev.roomJID && (prev.roomJID !== roomJID || !isFocused)) {
|
|
83
|
-
leaveRoom(prev.roomJID);
|
|
114
|
+
leaveRoom(prev.roomJID, prev.roomJID !== roomJID);
|
|
84
115
|
}
|
|
85
116
|
|
|
86
117
|
if (roomJID && isFocused) {
|
|
@@ -95,7 +126,7 @@ export const useChatRoomFocus = ({
|
|
|
95
126
|
return () => {
|
|
96
127
|
const prev = prevRef.current;
|
|
97
128
|
if (prev.isFocused && prev.roomJID) {
|
|
98
|
-
leaveRoom(prev.roomJID);
|
|
129
|
+
leaveRoom(prev.roomJID, true);
|
|
99
130
|
}
|
|
100
131
|
};
|
|
101
132
|
}, []);
|
package/src/hooks/useLogout.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import { pushSubscriptionService } from '../services/pushSubscriptionService';
|
|
|
10
10
|
import { clearRoomsRestCache } from '../networking/api-requests/rooms.api';
|
|
11
11
|
import { clearPersistedState } from '../roomStore/persistence';
|
|
12
12
|
import { secureUserStorage } from '../helpers/secureUserStorage';
|
|
13
|
+
import { getFlushBoundaryTs } from '../helpers/getServerReadTimestamp';
|
|
13
14
|
|
|
14
15
|
// AsyncStorage keys the library writes but that aren't cleared by any
|
|
15
16
|
// slice reducer. Listed here so a single logout call wipes the full
|
|
@@ -59,10 +60,23 @@ const logoutService = {
|
|
|
59
60
|
const client = (state.chatSettingStore as any)?.client;
|
|
60
61
|
const rooms = state.rooms?.rooms;
|
|
61
62
|
const visibleRoomJID = state.rooms?.visibleRoomJID || null;
|
|
63
|
+
// The visible room is exempt from `onlyIfNoUnread`'s skip (it's
|
|
64
|
+
// meant to write "read up to now" for whatever's open), so if the
|
|
65
|
+
// user is logging out while scrolled up with unread messages
|
|
66
|
+
// above, the boundary must still be carried through here - same
|
|
67
|
+
// reasoning as every other "leaving a room" path (bug #42).
|
|
68
|
+
const boundaryTs = visibleRoomJID
|
|
69
|
+
? getFlushBoundaryTs(
|
|
70
|
+
rooms?.[visibleRoomJID],
|
|
71
|
+
state.roomHeapSlice,
|
|
72
|
+
state.rooms?.readBoundaries?.[visibleRoomJID] ?? null
|
|
73
|
+
)
|
|
74
|
+
: undefined;
|
|
62
75
|
if (client?.flushLastViewedToPrivateStoreStanza) {
|
|
63
76
|
await Promise.race([
|
|
64
77
|
client.flushLastViewedToPrivateStoreStanza(rooms, {
|
|
65
78
|
visibleRoomJID,
|
|
79
|
+
visibleRoomTs: boundaryTs,
|
|
66
80
|
onlyIfNoUnread: true,
|
|
67
81
|
}),
|
|
68
82
|
new Promise((res) => setTimeout(res, 2000)),
|
|
@@ -2,6 +2,7 @@ import { createAsyncThunk, createSlice, PayloadAction, type Slice } from '@redux
|
|
|
2
2
|
import type { WritableDraft } from 'immer';
|
|
3
3
|
import { EditAction, HistoryPreloadState, IMessage, IRoom } from '../types/types';
|
|
4
4
|
import { insertMessageWithDelimiter } from '../helpers/insertMessageWithDelimiter';
|
|
5
|
+
import { msgSortableMs } from '../helpers/msgSortableMs';
|
|
5
6
|
import type XmppClient from '../networking/xmppClient';
|
|
6
7
|
|
|
7
8
|
// Per-room runtime message cap. Mirrors the persistence layer's
|
|
@@ -236,6 +237,33 @@ export interface RoomMessagesState {
|
|
|
236
237
|
// — see `applyPrivateStoreMarkers`. Re-fetched every init/reconnect,
|
|
237
238
|
// so it is intentionally NOT persisted.
|
|
238
239
|
privateStoreMarkers: Record<string, number>;
|
|
240
|
+
// The single source of truth for "read up to here, but the user
|
|
241
|
+
// hasn't reached the bottom yet" (`{ roomJID: boundaryMs }`).
|
|
242
|
+
// Set by MessageList (via ChatRoom's `onReadBoundaryChange`) the
|
|
243
|
+
// moment the user first scrolls away from the bottom, to the
|
|
244
|
+
// msgSortableMs of the newest message they'd actually seen. Every
|
|
245
|
+
// path that stamps a read marker for a room the user is leaving
|
|
246
|
+
// (ChatRoom unmount, xmppProvider's AppState background handler and
|
|
247
|
+
// `isVisible=false` handler, the live `advance()` effect, and
|
|
248
|
+
// `useChatRoomFocus`'s `leaveRoom`) must consult this instead of
|
|
249
|
+
// unconditionally stamping "the newest acked message" - otherwise
|
|
250
|
+
// leaving a room while scrolled up marks messages the user never
|
|
251
|
+
// reached as read (customer #42, a regression of #33 reintroduced by
|
|
252
|
+
// #38's server-timestamp marker). See `getReadMarkerTimestamp` in
|
|
253
|
+
// helpers/getServerReadTimestamp.ts.
|
|
254
|
+
//
|
|
255
|
+
// Cleared when the boundary no longer applies: the user scrolls back
|
|
256
|
+
// to the bottom, the visible room changes, or the room is truly left
|
|
257
|
+
// (ChatRoom unmounts, or a tab-navigator focus hook releases the
|
|
258
|
+
// room). NOT cleared merely because the app backgrounds or a host's
|
|
259
|
+
// `isVisible` flips false - MessageList typically stays mounted
|
|
260
|
+
// through those, so its own scroll-tracking ref (the only thing that
|
|
261
|
+
// could re-derive this value) is still intact and the boundary must
|
|
262
|
+
// stay in sync with it. Deliberately NOT persisted (this key isn't
|
|
263
|
+
// read by `persistence.ts`, which only ever picks `rooms` back out of
|
|
264
|
+
// this slice) - it's meaningless across a process restart, where
|
|
265
|
+
// MessageList always mounts fresh at the bottom.
|
|
266
|
+
readBoundaries: Record<string, number>;
|
|
239
267
|
}
|
|
240
268
|
|
|
241
269
|
const initialState: RoomMessagesState = {
|
|
@@ -252,6 +280,7 @@ const initialState: RoomMessagesState = {
|
|
|
252
280
|
},
|
|
253
281
|
pendingNotificationJid: null,
|
|
254
282
|
privateStoreMarkers: {},
|
|
283
|
+
readBoundaries: {},
|
|
255
284
|
};
|
|
256
285
|
|
|
257
286
|
const isValidRoomJid = (jid: unknown): jid is string => {
|
|
@@ -518,7 +547,7 @@ const reducers = {
|
|
|
518
547
|
state.visibleRoomJID === roomJID
|
|
519
548
|
? null
|
|
520
549
|
: lastViewedValue
|
|
521
|
-
?
|
|
550
|
+
? lastViewedValue
|
|
522
551
|
: null;
|
|
523
552
|
|
|
524
553
|
insertMessageWithDelimiter(roomMessages, message, lastViewedTimestamp);
|
|
@@ -536,6 +565,7 @@ const reducers = {
|
|
|
536
565
|
state.rooms = {};
|
|
537
566
|
state.visibleRoomJID = null;
|
|
538
567
|
state.privateStoreMarkers = {};
|
|
568
|
+
state.readBoundaries = {};
|
|
539
569
|
state.isUnreadSyncing = false;
|
|
540
570
|
},
|
|
541
571
|
setComposing(
|
|
@@ -659,6 +689,39 @@ const reducers = {
|
|
|
659
689
|
}
|
|
660
690
|
}
|
|
661
691
|
},
|
|
692
|
+
/**
|
|
693
|
+
* Set (or clear) the "read up to here, but not further" boundary for
|
|
694
|
+
* a room - see `readBoundaries` on `RoomMessagesState` for the full
|
|
695
|
+
* contract. `ts` is the msgSortableMs of the newest message the user
|
|
696
|
+
* actually reached before scrolling away from the bottom; `null`/`0`
|
|
697
|
+
* clears it (the user is at the bottom, or nothing was reached yet).
|
|
698
|
+
*/
|
|
699
|
+
setReadBoundary: (
|
|
700
|
+
state: WritableDraft<RoomMessagesState>,
|
|
701
|
+
action: PayloadAction<{ jid: string; ts: number | null }>
|
|
702
|
+
) => {
|
|
703
|
+
const { jid, ts } = action.payload;
|
|
704
|
+
if (!jid) {return;}
|
|
705
|
+
if (!state.readBoundaries) {state.readBoundaries = {};}
|
|
706
|
+
if (typeof ts === 'number' && Number.isFinite(ts) && ts > 0) {
|
|
707
|
+
state.readBoundaries[jid] = ts;
|
|
708
|
+
} else {
|
|
709
|
+
delete state.readBoundaries[jid];
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
/**
|
|
713
|
+
* Release a room's read boundary once it has been consumed by a
|
|
714
|
+
* genuine "leave" (ChatRoom unmount, or a tab-navigator focus hook
|
|
715
|
+
* switching to a different room) - see `readBoundaries` for why the
|
|
716
|
+
* background/`isVisible=false` paths deliberately do NOT call this.
|
|
717
|
+
*/
|
|
718
|
+
clearReadBoundary: (
|
|
719
|
+
state: WritableDraft<RoomMessagesState>,
|
|
720
|
+
action: PayloadAction<{ jid: string }>
|
|
721
|
+
) => {
|
|
722
|
+
const { jid } = action.payload;
|
|
723
|
+
if (jid && state.readBoundaries) {delete state.readBoundaries[jid];}
|
|
724
|
+
},
|
|
662
725
|
setRoomRole: (
|
|
663
726
|
state: WritableDraft<RoomMessagesState>,
|
|
664
727
|
action: PayloadAction<{ chatJID: string; role: string }>
|
|
@@ -752,6 +815,7 @@ const reducers = {
|
|
|
752
815
|
state.isLoading = false;
|
|
753
816
|
state.isUnreadSyncing = false;
|
|
754
817
|
state.privateStoreMarkers = {};
|
|
818
|
+
state.readBoundaries = {};
|
|
755
819
|
},
|
|
756
820
|
setActiveMessage: (
|
|
757
821
|
state: WritableDraft<RoomMessagesState>,
|
|
@@ -873,27 +937,13 @@ export const roomsStore: Slice<RoomMessagesState, typeof reducers, 'roomMessages
|
|
|
873
937
|
reducers,
|
|
874
938
|
});
|
|
875
939
|
|
|
876
|
-
//
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
//
|
|
882
|
-
|
|
883
|
-
// user's own messages (parity with unreadMiddleware's isOwnMessage
|
|
884
|
-
// filter — without this, the reducer and the middleware disagree about
|
|
885
|
-
// the count and we get a flicker as the badge gets written twice with
|
|
886
|
-
// different values on every message).
|
|
887
|
-
export const msgSortableMs = (msg: any): number => {
|
|
888
|
-
const id = String(msg?.id || '');
|
|
889
|
-
const m = /^(\d{13})/.exec(id);
|
|
890
|
-
if (m) {return Number(m[1]);}
|
|
891
|
-
if (msg?.date) {
|
|
892
|
-
const t = new Date(msg.date as any).getTime();
|
|
893
|
-
if (Number.isFinite(t)) {return t;}
|
|
894
|
-
}
|
|
895
|
-
return 0;
|
|
896
|
-
};
|
|
940
|
+
// Re-exported for backward compatibility - every existing call site
|
|
941
|
+
// imports `msgSortableMs` from here. The implementation now lives in
|
|
942
|
+
// helpers/msgSortableMs.ts (imported above), so insertMessageWithDelimiter.ts
|
|
943
|
+
// (which this file itself imports) can use the SAME ordering source
|
|
944
|
+
// without a roomStore <-> helpers import cycle. See that file for the
|
|
945
|
+
// full rationale (server id vs. client `date`).
|
|
946
|
+
export { msgSortableMs };
|
|
897
947
|
|
|
898
948
|
const norm = (s: any): string => {
|
|
899
949
|
if (s == null) {return '';}
|
|
@@ -980,6 +1030,8 @@ export const {
|
|
|
980
1030
|
setUnreadSyncing,
|
|
981
1031
|
setLastViewedTimestamp,
|
|
982
1032
|
applyPrivateStoreMarkers,
|
|
1033
|
+
setReadBoundary,
|
|
1034
|
+
clearReadBoundary,
|
|
983
1035
|
setRoomNoMessages,
|
|
984
1036
|
setCurrentRoom,
|
|
985
1037
|
setVisibleRoom,
|
package/src/types/types.ts
CHANGED
|
@@ -585,6 +585,20 @@ export interface IConfig {
|
|
|
585
585
|
// `disableKeyboardAvoidingView` is set. Needs on-device tuning of any
|
|
586
586
|
// `keyboardVerticalOffset`.
|
|
587
587
|
keyboardStickyInput?: boolean;
|
|
588
|
+
// Bottom padding applied to the input dock (the composer's outer
|
|
589
|
+
// wrapper), on both iOS and Android. When a number is provided (0 is
|
|
590
|
+
// allowed) it is used verbatim on both platforms, overriding the
|
|
591
|
+
// built-in default below.
|
|
592
|
+
//
|
|
593
|
+
// Left unset, the default is:
|
|
594
|
+
// - 0 when `disableKeyboardAvoidingView` is set, since the host owns
|
|
595
|
+
// layout entirely and typically already places the chat above its
|
|
596
|
+
// own chrome (e.g. a tab bar) - the built-in padding would just add
|
|
597
|
+
// an extra gap between the composer and that chrome.
|
|
598
|
+
// - otherwise unchanged: the iOS safe-area bottom inset, or a fixed
|
|
599
|
+
// 12dp gap on Android (`ANDROID_INPUT_DOCK_GAP`).
|
|
600
|
+
// Customer-reported #44.
|
|
601
|
+
inputDockPaddingBottom?: number;
|
|
588
602
|
// The message list's FlatList uses keyboardDismissMode="interactive" by
|
|
589
603
|
// default, so a drag can be used to pull the keyboard closed. Because
|
|
590
604
|
// the list is `inverted`, an ordinary scroll gesture through history can
|