@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.
- package/lib/commonjs/components/MainComponents/ChatRoom.js +60 -3
- package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatWrapper.js +18 -0
- package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MessageList.js +9 -3
- package/lib/commonjs/components/MainComponents/MessageList.js.map +1 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +130 -125
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +12 -0
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/historyPreloadScheduler.js +65 -2
- package/lib/commonjs/helpers/historyPreloadScheduler.js.map +1 -1
- package/lib/commonjs/helpers/shallowEqual.js +36 -0
- package/lib/commonjs/helpers/shallowEqual.js.map +1 -0
- package/lib/commonjs/hooks/useChatWrapperInit.js +15 -0
- package/lib/commonjs/hooks/useChatWrapperInit.js.map +1 -1
- package/lib/commonjs/hooks/useComposing.js +27 -0
- package/lib/commonjs/hooks/useComposing.js.map +1 -1
- package/lib/commonjs/hooks/useSendMessage.js +27 -1
- package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
- package/lib/commonjs/hooks/useUnreadMessagesCounter.js +6 -1
- package/lib/commonjs/hooks/useUnreadMessagesCounter.js.map +1 -1
- package/lib/commonjs/networking/xmpp/flushLastViewedToPrivateStore.js +6 -2
- package/lib/commonjs/networking/xmpp/flushLastViewedToPrivateStore.js.map +1 -1
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/roomStore/roomsSlice.js +2 -2
- package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
- package/lib/module/components/MainComponents/ChatRoom.js +61 -4
- package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/module/components/MainComponents/ChatWrapper.js +19 -1
- package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/module/components/MainComponents/MessageList.js +9 -3
- package/lib/module/components/MainComponents/MessageList.js.map +1 -1
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +130 -125
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/module/context/xmppProvider.js +12 -0
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/historyPreloadScheduler.js +66 -3
- package/lib/module/helpers/historyPreloadScheduler.js.map +1 -1
- package/lib/module/helpers/shallowEqual.js +31 -0
- package/lib/module/helpers/shallowEqual.js.map +1 -0
- package/lib/module/hooks/useChatWrapperInit.js +15 -0
- package/lib/module/hooks/useChatWrapperInit.js.map +1 -1
- package/lib/module/hooks/useComposing.js +28 -1
- package/lib/module/hooks/useComposing.js.map +1 -1
- package/lib/module/hooks/useSendMessage.js +28 -2
- package/lib/module/hooks/useSendMessage.js.map +1 -1
- package/lib/module/hooks/useUnreadMessagesCounter.js +6 -1
- package/lib/module/hooks/useUnreadMessagesCounter.js.map +1 -1
- package/lib/module/networking/xmpp/flushLastViewedToPrivateStore.js +6 -2
- package/lib/module/networking/xmpp/flushLastViewedToPrivateStore.js.map +1 -1
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/roomStore/roomsSlice.js +1 -1
- package/lib/module/roomStore/roomsSlice.js.map +1 -1
- package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MessageList.d.ts +10 -1
- package/lib/typescript/components/MainComponents/MessageList.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/helpers/historyPreloadScheduler.d.ts.map +1 -1
- package/lib/typescript/helpers/shallowEqual.d.ts +11 -0
- package/lib/typescript/helpers/shallowEqual.d.ts.map +1 -0
- package/lib/typescript/hooks/useChatWrapperInit.d.ts.map +1 -1
- package/lib/typescript/hooks/useComposing.d.ts.map +1 -1
- package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/hooks/useUnreadMessagesCounter.d.ts +1 -0
- package/lib/typescript/hooks/useUnreadMessagesCounter.d.ts.map +1 -1
- package/lib/typescript/networking/xmpp/flushLastViewedToPrivateStore.d.ts +1 -0
- package/lib/typescript/networking/xmpp/flushLastViewedToPrivateStore.d.ts.map +1 -1
- package/lib/typescript/networking/xmppClient.d.ts +1 -0
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts +1 -0
- 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 +63 -1
- package/src/components/MainComponents/ChatWrapper.tsx +19 -1
- package/src/components/MainComponents/MessageList.tsx +19 -2
- package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +8 -6
- package/src/context/xmppProvider.tsx +14 -0
- package/src/helpers/historyPreloadScheduler.ts +80 -3
- package/src/helpers/shallowEqual.ts +33 -0
- package/src/hooks/useChatWrapperInit.ts +15 -0
- package/src/hooks/useComposing.tsx +37 -1
- package/src/hooks/useSendMessage.tsx +32 -2
- package/src/hooks/useUnreadMessagesCounter.ts +11 -0
- package/src/networking/xmpp/flushLastViewedToPrivateStore.ts +17 -3
- package/src/networking/xmppClient.ts +5 -1
- package/src/roomStore/roomsSlice.ts +1 -1
- package/src/types/types.ts +8 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useCallback, useEffect,
|
|
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 =
|
|
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: {
|
|
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
|
package/src/types/types.ts
CHANGED
|
@@ -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;
|