@ethora/chat-component-rn 26.7.8 → 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 -2
- package/README.md +10 -0
- package/lib/commonjs/assets/icons.js +2 -2
- package/lib/commonjs/assets/icons.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatRoom.js +76 -19
- package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MediaMessage.js +8 -5
- package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MessageContainer.js +5 -1
- package/lib/commonjs/components/MainComponents/MessageContainer.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MessageList.js +2 -1
- package/lib/commonjs/components/MainComponents/MessageList.js.map +1 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +1 -51
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +51 -12
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js +12 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js +13 -2
- package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js +13 -2
- package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
- package/lib/commonjs/components/styled/MessageImage.js +3 -1
- package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
- package/lib/commonjs/components/styled/UnsupportedType.js +14 -4
- package/lib/commonjs/components/styled/UnsupportedType.js.map +1 -1
- package/lib/commonjs/components/styled/VideoMessage.js +3 -1
- package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +95 -36
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/createMessageFromXml.js +19 -1
- package/lib/commonjs/helpers/createMessageFromXml.js.map +1 -1
- package/lib/commonjs/helpers/getDisplayFileName.js +79 -0
- package/lib/commonjs/helpers/getDisplayFileName.js.map +1 -0
- package/lib/commonjs/helpers/getServerReadTimestamp.js +188 -0
- package/lib/commonjs/helpers/getServerReadTimestamp.js.map +1 -0
- package/lib/commonjs/helpers/initXmppRooms.js +9 -7
- package/lib/commonjs/helpers/initXmppRooms.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/isOwnMessage.js +75 -0
- package/lib/commonjs/helpers/isOwnMessage.js.map +1 -0
- package/lib/commonjs/helpers/keyboardLayout.js +12 -2
- package/lib/commonjs/helpers/keyboardLayout.js.map +1 -1
- package/lib/commonjs/helpers/mimeToExtension.js +6 -14
- package/lib/commonjs/helpers/mimeToExtension.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 +45 -12
- package/lib/commonjs/hooks/useChatRoomFocus.js.map +1 -1
- package/lib/commonjs/hooks/useChatWrapperInit.js +12 -7
- package/lib/commonjs/hooks/useChatWrapperInit.js.map +1 -1
- package/lib/commonjs/hooks/useLogout.js +8 -0
- package/lib/commonjs/hooks/useLogout.js.map +1 -1
- package/lib/commonjs/i18n/strings.js +0 -18
- package/lib/commonjs/i18n/strings.js.map +1 -1
- package/lib/commonjs/networking/api-requests/rooms.api.js +0 -13
- package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/commonjs/networking/stanzaHandlers.js +20 -2
- package/lib/commonjs/networking/stanzaHandlers.js.map +1 -1
- package/lib/commonjs/networking/xmpp/flushLastViewedToPrivateStore.js +45 -11
- package/lib/commonjs/networking/xmpp/flushLastViewedToPrivateStore.js.map +1 -1
- package/lib/commonjs/networking/xmppClient.js +36 -2
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/roomStore/index.js +35 -2
- package/lib/commonjs/roomStore/index.js.map +1 -1
- package/lib/commonjs/roomStore/persistence.js +200 -11
- package/lib/commonjs/roomStore/persistence.js.map +1 -1
- package/lib/commonjs/roomStore/roomsSlice.js +148 -37
- package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
- package/lib/module/assets/icons.js +1 -1
- package/lib/module/assets/icons.js.map +1 -1
- package/lib/module/components/MainComponents/ChatRoom.js +77 -20
- package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/module/components/MainComponents/MediaMessage.js +9 -6
- package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/module/components/MainComponents/MessageContainer.js +5 -1
- package/lib/module/components/MainComponents/MessageContainer.js.map +1 -1
- package/lib/module/components/MainComponents/MessageList.js +2 -1
- package/lib/module/components/MainComponents/MessageList.js.map +1 -1
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +3 -53
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +52 -13
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastMessageFile.js +12 -1
- package/lib/module/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js +13 -2
- package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastMessageVideo.js +13 -2
- package/lib/module/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
- package/lib/module/components/styled/MessageImage.js +3 -1
- package/lib/module/components/styled/MessageImage.js.map +1 -1
- package/lib/module/components/styled/UnsupportedType.js +14 -4
- package/lib/module/components/styled/UnsupportedType.js.map +1 -1
- package/lib/module/components/styled/VideoMessage.js +3 -1
- package/lib/module/components/styled/VideoMessage.js.map +1 -1
- package/lib/module/context/xmppProvider.js +96 -37
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/createMessageFromXml.js +19 -1
- package/lib/module/helpers/createMessageFromXml.js.map +1 -1
- package/lib/module/helpers/getDisplayFileName.js +73 -0
- package/lib/module/helpers/getDisplayFileName.js.map +1 -0
- package/lib/module/helpers/getServerReadTimestamp.js +181 -0
- package/lib/module/helpers/getServerReadTimestamp.js.map +1 -0
- package/lib/module/helpers/initXmppRooms.js +9 -7
- package/lib/module/helpers/initXmppRooms.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/isOwnMessage.js +70 -0
- package/lib/module/helpers/isOwnMessage.js.map +1 -0
- package/lib/module/helpers/keyboardLayout.js +12 -2
- package/lib/module/helpers/keyboardLayout.js.map +1 -1
- package/lib/module/helpers/mimeToExtension.js +6 -13
- package/lib/module/helpers/mimeToExtension.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 +46 -13
- package/lib/module/hooks/useChatRoomFocus.js.map +1 -1
- package/lib/module/hooks/useChatWrapperInit.js +12 -7
- package/lib/module/hooks/useChatWrapperInit.js.map +1 -1
- package/lib/module/hooks/useLogout.js +8 -0
- package/lib/module/hooks/useLogout.js.map +1 -1
- package/lib/module/i18n/strings.js +0 -18
- package/lib/module/i18n/strings.js.map +1 -1
- package/lib/module/networking/api-requests/rooms.api.js +0 -12
- package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/module/networking/stanzaHandlers.js +20 -2
- package/lib/module/networking/stanzaHandlers.js.map +1 -1
- package/lib/module/networking/xmpp/flushLastViewedToPrivateStore.js +45 -11
- package/lib/module/networking/xmpp/flushLastViewedToPrivateStore.js.map +1 -1
- package/lib/module/networking/xmppClient.js +36 -2
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/roomStore/index.js +37 -4
- package/lib/module/roomStore/index.js.map +1 -1
- package/lib/module/roomStore/persistence.js +197 -11
- package/lib/module/roomStore/persistence.js.map +1 -1
- package/lib/module/roomStore/roomsSlice.js +136 -33
- package/lib/module/roomStore/roomsSlice.js.map +1 -1
- package/lib/typescript/assets/icons.d.ts +0 -8
- package/lib/typescript/assets/icons.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MessageContainer.d.ts.map +1 -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/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessageFile.d.ts +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessageFile.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessagePhoto.d.ts +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessagePhoto.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessageVideo.d.ts +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessageVideo.d.ts.map +1 -1
- package/lib/typescript/components/styled/MessageImage.d.ts +6 -0
- package/lib/typescript/components/styled/MessageImage.d.ts.map +1 -1
- package/lib/typescript/components/styled/UnsupportedType.d.ts.map +1 -1
- package/lib/typescript/components/styled/VideoMessage.d.ts +6 -0
- package/lib/typescript/components/styled/VideoMessage.d.ts.map +1 -1
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/helpers/createMessageFromXml.d.ts.map +1 -1
- package/lib/typescript/helpers/getAvatarColor.d.ts +1 -1
- package/lib/typescript/helpers/getDisplayFileName.d.ts +49 -0
- package/lib/typescript/helpers/getDisplayFileName.d.ts.map +1 -0
- package/lib/typescript/helpers/getServerReadTimestamp.d.ts +108 -0
- package/lib/typescript/helpers/getServerReadTimestamp.d.ts.map +1 -0
- package/lib/typescript/helpers/initXmppRooms.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/isOwnMessage.d.ts +36 -0
- package/lib/typescript/helpers/isOwnMessage.d.ts.map +1 -0
- package/lib/typescript/helpers/keyboardLayout.d.ts +5 -1
- package/lib/typescript/helpers/keyboardLayout.d.ts.map +1 -1
- package/lib/typescript/helpers/mimeToExtension.d.ts +0 -15
- package/lib/typescript/helpers/mimeToExtension.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 +4 -1
- package/lib/typescript/hooks/useChatRoomFocus.d.ts.map +1 -1
- package/lib/typescript/hooks/useChatWrapperInit.d.ts.map +1 -1
- package/lib/typescript/hooks/useLogout.d.ts.map +1 -1
- package/lib/typescript/i18n/strings.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/rooms.api.d.ts +0 -1
- package/lib/typescript/networking/api-requests/rooms.api.d.ts.map +1 -1
- package/lib/typescript/networking/stanzaHandlers.d.ts.map +1 -1
- package/lib/typescript/networking/xmpp/flushLastViewedToPrivateStore.d.ts +9 -2
- package/lib/typescript/networking/xmpp/flushLastViewedToPrivateStore.d.ts.map +1 -1
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/roomStore/index.d.ts.map +1 -1
- package/lib/typescript/roomStore/persistence.d.ts +18 -1
- package/lib/typescript/roomStore/persistence.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts +42 -3
- package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
- package/lib/typescript/types/models/room.model.d.ts +0 -1
- package/lib/typescript/types/models/room.model.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +1 -1
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/assets/icons.tsx +0 -24
- package/src/components/MainComponents/ChatRoom.tsx +81 -19
- package/src/components/MainComponents/MediaMessage.tsx +8 -5
- package/src/components/MainComponents/MessageContainer.tsx +5 -1
- package/src/components/MainComponents/MessageList.tsx +2 -1
- package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +2 -36
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +63 -26
- package/src/components/RoomComponents/styled/LastMessageFile.tsx +14 -2
- package/src/components/RoomComponents/styled/LastMessagePhoto.tsx +14 -2
- package/src/components/RoomComponents/styled/LastMessageVideo.tsx +14 -2
- package/src/components/styled/MessageImage.tsx +8 -1
- package/src/components/styled/UnsupportedType.tsx +14 -4
- package/src/components/styled/VideoMessage.tsx +8 -1
- package/src/context/xmppProvider.tsx +122 -30
- package/src/helpers/createMessageFromXml.ts +20 -1
- package/src/helpers/getDisplayFileName.ts +87 -0
- package/src/helpers/getServerReadTimestamp.ts +197 -0
- package/src/helpers/initXmppRooms.tsx +9 -9
- package/src/helpers/insertMessageWithDelimiter.ts +55 -19
- package/src/helpers/isOwnMessage.ts +90 -0
- package/src/helpers/keyboardLayout.ts +23 -2
- package/src/helpers/mimeToExtension.ts +6 -22
- package/src/helpers/msgSortableMs.ts +25 -0
- package/src/hooks/useChatRoomFocus.ts +49 -9
- package/src/hooks/useChatWrapperInit.ts +12 -7
- package/src/hooks/useLogout.tsx +14 -0
- package/src/i18n/strings.ts +0 -18
- package/src/networking/api-requests/rooms.api.ts +0 -9
- package/src/networking/stanzaHandlers.ts +20 -6
- package/src/networking/xmpp/flushLastViewedToPrivateStore.ts +65 -18
- package/src/networking/xmppClient.ts +41 -2
- package/src/roomStore/index.ts +40 -4
- package/src/roomStore/persistence.ts +197 -7
- package/src/roomStore/roomsSlice.ts +158 -28
- package/src/types/models/room.model.ts +0 -1
- package/src/types/types.ts +14 -1
|
@@ -18,6 +18,7 @@ import { ActivityIndicator, View } from 'react-native';
|
|
|
18
18
|
import { useChatSettingState } from '../../hooks/useChatSettingState';
|
|
19
19
|
import { FileIcon } from '../../assets/icons';
|
|
20
20
|
import { getExtensionForMime } from '../../helpers/mimeToExtension';
|
|
21
|
+
import { getDisplayFileName } from '../../helpers/getDisplayFileName';
|
|
21
22
|
|
|
22
23
|
interface FileDownloadProps {
|
|
23
24
|
fileName: string;
|
|
@@ -47,6 +48,15 @@ const FileDownload: React.FC<FileDownloadProps> = ({
|
|
|
47
48
|
const dispatch = useDispatch();
|
|
48
49
|
const { config } = useChatSettingState();
|
|
49
50
|
|
|
51
|
+
// Bug #40: prefer the sender's original name over the server's stored
|
|
52
|
+
// hash name for both the visible label and the extension badge below.
|
|
53
|
+
const displayName = getDisplayFileName({
|
|
54
|
+
originalName,
|
|
55
|
+
fileName,
|
|
56
|
+
location: fileURL,
|
|
57
|
+
mimetype,
|
|
58
|
+
});
|
|
59
|
+
|
|
50
60
|
const formatFileSize = (sizeInBytes: string): string => {
|
|
51
61
|
const size = parseInt(sizeInBytes, 10);
|
|
52
62
|
|
|
@@ -101,9 +111,9 @@ const FileDownload: React.FC<FileDownloadProps> = ({
|
|
|
101
111
|
};
|
|
102
112
|
|
|
103
113
|
const extensionLabel = (() => {
|
|
104
|
-
const dotIndex =
|
|
105
|
-
if (dotIndex !== -1 && dotIndex <
|
|
106
|
-
return
|
|
114
|
+
const dotIndex = displayName.lastIndexOf('.');
|
|
115
|
+
if (dotIndex !== -1 && dotIndex < displayName.length - 1) {
|
|
116
|
+
return displayName.slice(dotIndex + 1).toUpperCase();
|
|
107
117
|
}
|
|
108
118
|
return getExtensionForMime(mimetype).replace('.', '').toUpperCase();
|
|
109
119
|
})();
|
|
@@ -123,7 +133,7 @@ const FileDownload: React.FC<FileDownloadProps> = ({
|
|
|
123
133
|
isUser={isUser}
|
|
124
134
|
colorIsUser={config?.colors?.primary}
|
|
125
135
|
colorUsers={config?.colors?.secondary}
|
|
126
|
-
>{formatFileName(
|
|
136
|
+
>{formatFileName(displayName, 20)}</FileName>
|
|
127
137
|
{(pending || size) && (
|
|
128
138
|
<FileSizeContainer>
|
|
129
139
|
{pending ? (
|
|
@@ -19,6 +19,12 @@ import {
|
|
|
19
19
|
|
|
20
20
|
interface CustomMessageVideoProps {
|
|
21
21
|
fileName: string;
|
|
22
|
+
/**
|
|
23
|
+
* The sender's original file name (bug #40) - kept alongside `fileName`
|
|
24
|
+
* so the preview modal's save/share/download logic can prefer it over
|
|
25
|
+
* the server's stored/hashed name.
|
|
26
|
+
*/
|
|
27
|
+
originalName?: string;
|
|
22
28
|
fileURL: string;
|
|
23
29
|
mimetype: string;
|
|
24
30
|
/** The backend-generated poster frame (message.locationPreview). */
|
|
@@ -126,6 +132,7 @@ const PlaceholderVideo: React.FC<{
|
|
|
126
132
|
|
|
127
133
|
const CustomMessageVideo: React.FC<CustomMessageVideoProps> = ({
|
|
128
134
|
fileName,
|
|
135
|
+
originalName,
|
|
129
136
|
fileURL,
|
|
130
137
|
mimetype,
|
|
131
138
|
previewURL,
|
|
@@ -134,7 +141,7 @@ const CustomMessageVideo: React.FC<CustomMessageVideoProps> = ({
|
|
|
134
141
|
const [posterFailed, setPosterFailed] = useState(false);
|
|
135
142
|
|
|
136
143
|
const handleOpen = () => {
|
|
137
|
-
dispatch(setActiveFile({ fileName, fileURL, mimetype }));
|
|
144
|
+
dispatch(setActiveFile({ fileName, fileURL, mimetype, originalName }));
|
|
138
145
|
dispatch(setActiveModal(MODAL_TYPES.FILE_PREVIEW));
|
|
139
146
|
};
|
|
140
147
|
|
|
@@ -46,9 +46,12 @@ import {
|
|
|
46
46
|
clearVisibleRoom,
|
|
47
47
|
setLastViewedTimestamp,
|
|
48
48
|
deleteRoomMessage,
|
|
49
|
-
msgSortableMs,
|
|
50
49
|
setUnreadSyncing,
|
|
51
50
|
} from '../roomStore/roomsSlice';
|
|
51
|
+
import {
|
|
52
|
+
getFlushBoundaryTs,
|
|
53
|
+
getReadMarkerTimestamp,
|
|
54
|
+
} from '../helpers/getServerReadTimestamp';
|
|
52
55
|
import { runHistoryPreloadScheduler } from '../helpers/historyPreloadScheduler';
|
|
53
56
|
import { updateMessagesTillLast } from '../helpers/updateMessagesTillLast';
|
|
54
57
|
import { secureUserStorage } from '../helpers/secureUserStorage';
|
|
@@ -638,16 +641,43 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
638
641
|
// Mark the open room "not visible" while backgrounded so messages
|
|
639
642
|
// that arrive (or MAM-replay on reconnect) count as unread instead
|
|
640
643
|
// of being silently treated as read — the "mounted == visible ==
|
|
641
|
-
// read" gap. Stamp lastViewed
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
//
|
|
644
|
+
// read" gap. Stamp lastViewed=<the read boundary, if the user left
|
|
645
|
+
// scrolled up, else the newest server-acked message>, not
|
|
646
|
+
// Date.now(), as the read baseline: a device clock running ahead
|
|
647
|
+
// would otherwise write a future marker that the forward-only
|
|
648
|
+
// private-store merge can never correct again (bug #38). Then
|
|
649
|
+
// clear visibility and flush that marker to the server private
|
|
650
|
+
// store. Restored on the next 'active' transition above. Handled
|
|
651
|
+
// here in the provider so consumers using the component as a
|
|
652
|
+
// package get correct unread without reaching into the chat store
|
|
653
|
+
// themselves.
|
|
654
|
+
//
|
|
655
|
+
// The read boundary (rooms.readBoundaries) is deliberately left
|
|
656
|
+
// untouched here - MessageList normally stays mounted through a
|
|
657
|
+
// simple background/foreground cycle, so its own scroll-tracking
|
|
658
|
+
// ref (the only thing that could re-derive this value) is still
|
|
659
|
+
// intact, and clearing the redux copy would desync the two. It's
|
|
660
|
+
// only USED here for the one-shot stamp+flush, matching customer
|
|
661
|
+
// #42: leaving (or backgrounding) a room while scrolled up must not
|
|
662
|
+
// mark messages the user never reached as read.
|
|
646
663
|
visibleBeforeBackground = visibleRoomJID;
|
|
664
|
+
let boundaryTs: number | null = null;
|
|
647
665
|
if (visibleRoomJID) {
|
|
648
|
-
|
|
649
|
-
|
|
666
|
+
boundaryTs = state.rooms?.readBoundaries?.[visibleRoomJID] ?? null;
|
|
667
|
+
const readTs = getReadMarkerTimestamp(
|
|
668
|
+
rooms?.[visibleRoomJID],
|
|
669
|
+
state.roomHeapSlice,
|
|
670
|
+
boundaryTs
|
|
650
671
|
);
|
|
672
|
+
// Nothing to anchor to yet (no known messages, no prior marker)
|
|
673
|
+
// - skip the local stamp rather than fall back to the device
|
|
674
|
+
// clock. The flush below still runs so the visibility clear
|
|
675
|
+
// isn't blocked on this.
|
|
676
|
+
if (readTs > 0) {
|
|
677
|
+
store.dispatch(
|
|
678
|
+
setLastViewedTimestamp({ chatJID: visibleRoomJID, timestamp: readTs })
|
|
679
|
+
);
|
|
680
|
+
}
|
|
651
681
|
// Drop the "New messages" divider for the room we're leaving so it
|
|
652
682
|
// doesn't linger when the user comes back (mirrors ChatRoom's
|
|
653
683
|
// unmount cleanup, which never runs while the pane stays mounted).
|
|
@@ -657,10 +687,21 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
657
687
|
store.dispatch(clearVisibleRoom());
|
|
658
688
|
}
|
|
659
689
|
// Fire-and-forget — we're going to the background and don't
|
|
660
|
-
// care about the resolution path.
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
690
|
+
// care about the resolution path. `visibleRoomTs` carries the same
|
|
691
|
+
// boundary to the SERVER marker - without it the flush defaults to
|
|
692
|
+
// "everything" for the visible room, overriding a correct smaller
|
|
693
|
+
// local count.
|
|
694
|
+
c.flushLastViewedToPrivateStoreStanza(rooms, {
|
|
695
|
+
visibleRoomJID,
|
|
696
|
+
// Clamped through the same helper as the local stamp above.
|
|
697
|
+
visibleRoomTs: visibleRoomJID
|
|
698
|
+
? getFlushBoundaryTs(
|
|
699
|
+
rooms?.[visibleRoomJID],
|
|
700
|
+
state.roomHeapSlice,
|
|
701
|
+
boundaryTs
|
|
702
|
+
)
|
|
703
|
+
: undefined,
|
|
704
|
+
}).catch(() => {});
|
|
664
705
|
});
|
|
665
706
|
return () => sub.remove();
|
|
666
707
|
}, [client, config?.xmppSettings?.keepAliveInBackground]);
|
|
@@ -672,11 +713,11 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
672
713
|
// "shown" from "hidden", so the unread middleware would keep the open
|
|
673
714
|
// room's badge at 0 and messages arriving while you're elsewhere look
|
|
674
715
|
// already-read. When the host passes `isVisible`, mirror the AppState
|
|
675
|
-
// background/foreground handling: hidden → stamp lastViewed
|
|
676
|
-
// room visibility (so later messages
|
|
677
|
-
// visibility for the open room. Doing
|
|
678
|
-
// touch the chat store themselves.
|
|
679
|
-
// nothing to do.)
|
|
716
|
+
// background/foreground handling: hidden → stamp lastViewed=<newest
|
|
717
|
+
// server-acked message> + clear room visibility (so later messages
|
|
718
|
+
// count) + flush; shown → restore visibility for the open room. Doing
|
|
719
|
+
// it here means consumers never touch the chat store themselves.
|
|
720
|
+
// (Undefined = host unmounts on hide, nothing to do.)
|
|
680
721
|
// -----------------------------------------------------------
|
|
681
722
|
const chatWasVisibleRef = useRef(false);
|
|
682
723
|
useEffect(() => {
|
|
@@ -691,10 +732,30 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
691
732
|
store.dispatch(setVisibleRoom({ roomJID: activeRoomJID }));
|
|
692
733
|
}
|
|
693
734
|
} else {
|
|
735
|
+
// The read boundary (rooms.readBoundaries) is USED for the stamp
|
|
736
|
+
// and flush below but deliberately left in place - a host using
|
|
737
|
+
// `isVisible` keeps <Chat> mounted while hidden, so MessageList's
|
|
738
|
+
// own scroll-tracking ref is still intact and the redux copy must
|
|
739
|
+
// stay in sync with it. Only consumed, never Date.now(): a fast
|
|
740
|
+
// device clock would write a future marker that the forward-only
|
|
741
|
+
// private-store merge could then never correct (bug #38). And
|
|
742
|
+
// never "the newest acked message" unconditionally either - that
|
|
743
|
+
// silently marks messages the user scrolled past and never reached
|
|
744
|
+
// as read (bug #42, a regression of #33).
|
|
745
|
+
const boundaryTs = activeRoomJID
|
|
746
|
+
? state.rooms?.readBoundaries?.[activeRoomJID] ?? null
|
|
747
|
+
: null;
|
|
694
748
|
if (wasVisible && activeRoomJID) {
|
|
695
|
-
|
|
696
|
-
|
|
749
|
+
const readTs = getReadMarkerTimestamp(
|
|
750
|
+
rooms?.[activeRoomJID],
|
|
751
|
+
state.roomHeapSlice,
|
|
752
|
+
boundaryTs
|
|
697
753
|
);
|
|
754
|
+
if (readTs > 0) {
|
|
755
|
+
store.dispatch(
|
|
756
|
+
setLastViewedTimestamp({ chatJID: activeRoomJID, timestamp: readTs })
|
|
757
|
+
);
|
|
758
|
+
}
|
|
698
759
|
// Drop the "New messages" divider so it's gone when the user
|
|
699
760
|
// returns to the chat (mirrors ChatRoom's unmount cleanup).
|
|
700
761
|
store.dispatch(
|
|
@@ -705,7 +766,19 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
705
766
|
store.dispatch(clearVisibleRoom());
|
|
706
767
|
if (wasVisible && client?.flushLastViewedToPrivateStoreStanza) {
|
|
707
768
|
client
|
|
708
|
-
.flushLastViewedToPrivateStoreStanza(rooms, {
|
|
769
|
+
.flushLastViewedToPrivateStoreStanza(rooms, {
|
|
770
|
+
visibleRoomJID: activeRoomJID,
|
|
771
|
+
// Carry the same boundary to the SERVER marker - without it
|
|
772
|
+
// the flush defaults to "everything" for the visible room.
|
|
773
|
+
// Clamped exactly like the local stamp above.
|
|
774
|
+
visibleRoomTs: activeRoomJID
|
|
775
|
+
? getFlushBoundaryTs(
|
|
776
|
+
rooms?.[activeRoomJID],
|
|
777
|
+
state.roomHeapSlice,
|
|
778
|
+
boundaryTs
|
|
779
|
+
)
|
|
780
|
+
: undefined,
|
|
781
|
+
})
|
|
709
782
|
.catch(() => {});
|
|
710
783
|
}
|
|
711
784
|
}
|
|
@@ -726,26 +799,45 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
726
799
|
if (room.messages === lastMessagesRef) {return;}
|
|
727
800
|
lastMessagesRef = room.messages;
|
|
728
801
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
802
|
+
// getReadMarkerTimestamp (not a hand-rolled scan + Date.now()):
|
|
803
|
+
// it excludes pending/optimistic messages, which carry the DEVICE
|
|
804
|
+
// send time via `date` until the server echoes them back - a
|
|
805
|
+
// manual scan without that exclusion could pick up a pending
|
|
806
|
+
// message's device timestamp here and write a future marker the
|
|
807
|
+
// same way a raw Date.now() would (bug #38). And when the user has
|
|
808
|
+
// scrolled up (a boundary is set for this room), it clamps to that
|
|
809
|
+
// boundary instead of the newest message - without this, every
|
|
810
|
+
// incoming message while scrolled up advanced the marker to
|
|
811
|
+
// "newest" regardless of scroll position, and the later, smaller
|
|
812
|
+
// boundary write on leave was rejected by the forward-only
|
|
813
|
+
// private-store merge, leaving 0 unread even after a restart
|
|
814
|
+
// (customer #42, a regression of #33 reintroduced by #38).
|
|
815
|
+
const boundaryTs = s.rooms?.readBoundaries?.[jid] ?? null;
|
|
816
|
+
const newest = getReadMarkerTimestamp(room, s.roomHeapSlice, boundaryTs);
|
|
817
|
+
if (!newest || newest <= lastStampedMs) {return;}
|
|
737
818
|
lastStampedMs = newest;
|
|
738
819
|
|
|
739
820
|
store.dispatch(
|
|
740
|
-
setLastViewedTimestamp({ chatJID: jid, timestamp:
|
|
821
|
+
setLastViewedTimestamp({ chatJID: jid, timestamp: newest })
|
|
741
822
|
);
|
|
742
823
|
|
|
743
824
|
if (flushTimer) {clearTimeout(flushTimer);}
|
|
744
825
|
if (client?.flushLastViewedToPrivateStoreStanza) {
|
|
745
826
|
flushTimer = setTimeout(() => {
|
|
827
|
+
// Re-read fresh state rather than closing over `boundaryTs`:
|
|
828
|
+
// the user may have scrolled back to the bottom (clearing the
|
|
829
|
+
// boundary) in the 2s between the stamp and this flush, and
|
|
830
|
+
// the flush should reflect that.
|
|
831
|
+
const freshState = store.getState();
|
|
832
|
+
const freshBoundaryTs = freshState.rooms?.readBoundaries?.[jid] ?? null;
|
|
746
833
|
client
|
|
747
|
-
.flushLastViewedToPrivateStoreStanza(
|
|
834
|
+
.flushLastViewedToPrivateStoreStanza(freshState.rooms?.rooms, {
|
|
748
835
|
visibleRoomJID: jid,
|
|
836
|
+
visibleRoomTs: getFlushBoundaryTs(
|
|
837
|
+
freshState.rooms?.rooms?.[jid],
|
|
838
|
+
freshState.roomHeapSlice,
|
|
839
|
+
freshBoundaryTs
|
|
840
|
+
),
|
|
749
841
|
})
|
|
750
842
|
.catch(() => {});
|
|
751
843
|
}, 2000);
|
|
@@ -93,6 +93,25 @@ export const createMessageFromXml = async (
|
|
|
93
93
|
const senderLocal = senderJID.includes('@')
|
|
94
94
|
? senderJID.split('@')[0]
|
|
95
95
|
: senderJID;
|
|
96
|
+
|
|
97
|
+
// Bug #41 root cause: the resource of the MUC `from` (room@conference/
|
|
98
|
+
// occupant-id) is the SAME field getDataFromXml uses to derive
|
|
99
|
+
// `user.id` for live messages and for the wrapped MAM convention
|
|
100
|
+
// (getHistory.xmpp.ts) - it's stamped by the server itself when the
|
|
101
|
+
// sender joined the room, so it's always present and always correct.
|
|
102
|
+
// The positional convention (onMessageHistory's MAM catch-up path)
|
|
103
|
+
// never called getDataFromXml, so it fell back to splitting the
|
|
104
|
+
// sender-supplied `<data senderJID>` attr on '@' instead - usually the
|
|
105
|
+
// same value, but WRONG (picks up the room's own id, not the
|
|
106
|
+
// sender's) whenever senderJID is absent from the stanza, and simply a
|
|
107
|
+
// different derivation than the live path even when it happens to
|
|
108
|
+
// agree. Prefer the resource-of-`from` here too so every path that
|
|
109
|
+
// reaches this function resolves the same sender identity the same
|
|
110
|
+
// way, instead of MAM catch-up rows needing a later correction once
|
|
111
|
+
// the "official" fetch lands.
|
|
112
|
+
const xmppFrom = (merged.xmppFrom || '').toString();
|
|
113
|
+
const fromResource = xmppFrom.includes('/') ? xmppFrom.split('/')[1] : '';
|
|
114
|
+
|
|
96
115
|
const composed = [firstName, lastName].filter(Boolean).join(' ').trim();
|
|
97
116
|
const existingName = String(merged?.user?.name || '').trim();
|
|
98
117
|
const resolvedName =
|
|
@@ -107,7 +126,7 @@ export const createMessageFromXml = async (
|
|
|
107
126
|
...merged,
|
|
108
127
|
user: {
|
|
109
128
|
...(merged.user || {}),
|
|
110
|
-
id: merged?.user?.id || senderLocal,
|
|
129
|
+
id: merged?.user?.id || fromResource || senderLocal,
|
|
111
130
|
name: resolvedName,
|
|
112
131
|
firstName: firstName || merged?.user?.firstName,
|
|
113
132
|
lastName: lastName || merged?.user?.lastName,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ensureFilenameHasExtension, filenameFromUrl } from './mimeToExtension';
|
|
2
|
+
|
|
3
|
+
export interface DisplayFileNameOptions {
|
|
4
|
+
originalName?: string | null;
|
|
5
|
+
fileName?: string | null;
|
|
6
|
+
location?: string | null;
|
|
7
|
+
mimetype?: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Picks the best human-readable filename for a chat attachment - the
|
|
12
|
+
* single source of truth for every place a file name is rendered in the
|
|
13
|
+
* UI, saved to disk, shared, or downloaded.
|
|
14
|
+
*
|
|
15
|
+
* Priority: `originalName` (the sender's own file name, e.g. "test1.pdf")
|
|
16
|
+
* beats `fileName` (the server's stored/hashed name, e.g.
|
|
17
|
+
* "3f9a1c...c2.pdf") beats the last path segment of `location`. The
|
|
18
|
+
* stored hash name is only a fallback of last resort.
|
|
19
|
+
*
|
|
20
|
+
* Bug #40: display and save/share/download logic used to prefer the
|
|
21
|
+
* stored hash name over the original one, so an attachment sent as
|
|
22
|
+
* "test1.pdf" showed up (and got saved to the phone) as something like
|
|
23
|
+
* "3f9a1c...c2.pdf" - both directions (web -> RN and RN -> RN).
|
|
24
|
+
*
|
|
25
|
+
* Always returns a non-empty string with a valid extension: if the
|
|
26
|
+
* chosen candidate has no extension (a hash name can be `<hex>` with no
|
|
27
|
+
* extension at all, or `<hex>.<ext>`), one is appended from `mimetype`
|
|
28
|
+
* (or `.bin` as a last resort when the mime is unknown/missing too).
|
|
29
|
+
*/
|
|
30
|
+
export function getDisplayFileName(options: DisplayFileNameOptions): string {
|
|
31
|
+
const candidate =
|
|
32
|
+
(options.originalName && options.originalName.trim()) ||
|
|
33
|
+
(options.fileName && options.fileName.trim()) ||
|
|
34
|
+
filenameFromUrl(options.location) ||
|
|
35
|
+
'';
|
|
36
|
+
return ensureFilenameHasExtension(candidate, options.mimetype);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Makes a display name safe to use as an actual path segment when
|
|
41
|
+
* saving/sharing a file to the device: strips path separators (so an
|
|
42
|
+
* odd/hostile server-provided name can't escape the target directory)
|
|
43
|
+
* and control characters. Keeps the rest of the name - including the
|
|
44
|
+
* extension - intact.
|
|
45
|
+
*/
|
|
46
|
+
export function sanitizeFileNameForPath(name: string): string {
|
|
47
|
+
const cleaned = (name || '')
|
|
48
|
+
.replace(/[\/\\]/g, '_')
|
|
49
|
+
// eslint-disable-next-line no-control-regex
|
|
50
|
+
.replace(/[\x00-\x1f\x7f]/g, '')
|
|
51
|
+
.trim();
|
|
52
|
+
return cleaned || 'file';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolves a filename that is safe to write into a target directory:
|
|
57
|
+
* sanitizes it, then - if `exists` reports a collision - inserts
|
|
58
|
+
* " (1)", " (2)", ... before the extension until a free name is found,
|
|
59
|
+
* so saving two different attachments that share a display name never
|
|
60
|
+
* silently overwrites the first one.
|
|
61
|
+
*
|
|
62
|
+
* `exists` is injected so callers can back it with whatever existence
|
|
63
|
+
* check makes sense for their storage API (expo-file-system,
|
|
64
|
+
* StorageAccessFramework directory listing, ...) - this function stays
|
|
65
|
+
* pure and easy to unit test.
|
|
66
|
+
*/
|
|
67
|
+
export async function getUniqueFileName(
|
|
68
|
+
name: string,
|
|
69
|
+
exists: (candidateName: string) => Promise<boolean>
|
|
70
|
+
): Promise<string> {
|
|
71
|
+
const safeName = sanitizeFileNameForPath(name);
|
|
72
|
+
if (!(await exists(safeName))) {
|
|
73
|
+
return safeName;
|
|
74
|
+
}
|
|
75
|
+
const dotIndex = safeName.lastIndexOf('.');
|
|
76
|
+
const base = dotIndex > 0 ? safeName.slice(0, dotIndex) : safeName;
|
|
77
|
+
const ext = dotIndex > 0 ? safeName.slice(dotIndex) : '';
|
|
78
|
+
// 999 collisions is already a pathological case; fall back to a
|
|
79
|
+
// timestamp suffix rather than looping forever.
|
|
80
|
+
for (let i = 1; i < 1000; i += 1) {
|
|
81
|
+
const candidate = `${base} (${i})${ext}`;
|
|
82
|
+
if (!(await exists(candidate))) {
|
|
83
|
+
return candidate;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return `${base} (${Date.now()})${ext}`;
|
|
87
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { IMessage, IRoom } from '../types/types';
|
|
2
|
+
import { msgSortableMs } from './msgSortableMs';
|
|
3
|
+
|
|
4
|
+
// A stored/stamped read marker more than this far past the newest
|
|
5
|
+
// message we actually know about for a room is not "the user is a
|
|
6
|
+
// little ahead" - it's a leftover from a device whose clock was wrong
|
|
7
|
+
// when it stamped the marker (bug #38). Generous on purpose: this value
|
|
8
|
+
// only controls when we're willing to treat an existing marker as
|
|
9
|
+
// corrupt and self-heal it, never how far off a legitimate new write can
|
|
10
|
+
// be, so a false positive here just means a bad value survives one extra
|
|
11
|
+
// cycle, not that a good value gets rejected.
|
|
12
|
+
export const FUTURE_READ_MARKER_TOLERANCE_MS = 5 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
export interface ReadTimestampHeapState {
|
|
15
|
+
messageHeap?: IMessage[] | null;
|
|
16
|
+
failedMessages?: Record<string, unknown> | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type RoomForReadTimestamp = Pick<IRoom, 'jid' | 'messages' | 'lastViewedTimestamp'>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the "read up to here" marker for `room` using SERVER time,
|
|
23
|
+
* never the device clock.
|
|
24
|
+
*
|
|
25
|
+
* Bug #38: if the device clock is ahead when the user leaves a room, a
|
|
26
|
+
* `Date.now()` read marker lands in the future. The private-store merge
|
|
27
|
+
* (flushLastViewedToPrivateStore.ts) is forward-only ("if (prevNum >=
|
|
28
|
+
* ts) continue"), so once a future marker is written, no later
|
|
29
|
+
* *correct* write can ever replace it - the room's unread count is
|
|
30
|
+
* stuck wrong forever, on every device, from that point on. Anchoring
|
|
31
|
+
* to the newest SERVER-acknowledged message's own timestamp instead
|
|
32
|
+
* means the marker we write can never be ahead of what the server
|
|
33
|
+
* itself has recorded for the room.
|
|
34
|
+
*
|
|
35
|
+
* "Server-acknowledged" excludes:
|
|
36
|
+
* - a message still marked `pending` (the optimistic bubble shown
|
|
37
|
+
* while a send is in flight - it carries the DEVICE send time via
|
|
38
|
+
* `date`, not a server timestamp, until the server echoes it back
|
|
39
|
+
* and insertMessageWithDelimiter flips `pending` off);
|
|
40
|
+
* - a message id present in `heapState.messageHeap` (queued retry -
|
|
41
|
+
* used for offline media sends, see roomHeapSlice.ts) or
|
|
42
|
+
* `heapState.failedMessages` (a send that never reached the server
|
|
43
|
+
* at all).
|
|
44
|
+
*
|
|
45
|
+
* `msgSortableMs` (helpers/msgSortableMs.ts) reads the server-assigned microsecond
|
|
46
|
+
* timestamp encoded in the first 13 digits of the message id - the same
|
|
47
|
+
* source already used for message ordering and unread counting - so
|
|
48
|
+
* this stays consistent with the rest of the read/unread pipeline.
|
|
49
|
+
*
|
|
50
|
+
* Falls back to the room's existing `lastViewedTimestamp` (so leaving
|
|
51
|
+
* an empty or never-scrolled room doesn't regress the marker), then to
|
|
52
|
+
* `0` when there's truly nothing to anchor to yet (a brand new room
|
|
53
|
+
* with no history and no prior marker). Callers should treat `0` as
|
|
54
|
+
* "skip the write" rather than substituting a device-clock guess.
|
|
55
|
+
*/
|
|
56
|
+
export function getServerReadTimestamp(
|
|
57
|
+
room: RoomForReadTimestamp | null | undefined,
|
|
58
|
+
heapState?: ReadTimestampHeapState | null
|
|
59
|
+
): number {
|
|
60
|
+
if (!room) {return 0;}
|
|
61
|
+
|
|
62
|
+
const heapIds = new Set(
|
|
63
|
+
(heapState?.messageHeap || [])
|
|
64
|
+
.filter((m) => m && m.id != null)
|
|
65
|
+
.map((m) => String(m.id))
|
|
66
|
+
);
|
|
67
|
+
const failedIds = new Set(Object.keys(heapState?.failedMessages || {}));
|
|
68
|
+
|
|
69
|
+
let newest = 0;
|
|
70
|
+
const messages = room.messages || [];
|
|
71
|
+
for (const m of messages) {
|
|
72
|
+
if (!m || !m.id) {continue;}
|
|
73
|
+
const id = String(m.id);
|
|
74
|
+
if (id === 'delimiter-new' || id === 'delimiter-new-local') {continue;}
|
|
75
|
+
if ((m as any).pending) {continue;}
|
|
76
|
+
if (heapIds.has(id) || failedIds.has(id)) {continue;}
|
|
77
|
+
|
|
78
|
+
const ts = msgSortableMs(m);
|
|
79
|
+
if (ts > newest) {newest = ts;}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (newest > 0) {return newest;}
|
|
83
|
+
if (room.lastViewedTimestamp && room.lastViewedTimestamp > 0) {
|
|
84
|
+
return room.lastViewedTimestamp;
|
|
85
|
+
}
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Is `marker` a leftover from a device whose clock was wrong when it
|
|
91
|
+
* stamped a read marker (bug #38)?
|
|
92
|
+
*
|
|
93
|
+
* TWO independent signals must agree before we're willing to throw a
|
|
94
|
+
* stored marker away, because "waive the forward-only guard" is a
|
|
95
|
+
* destructive operation:
|
|
96
|
+
*
|
|
97
|
+
* 1. it sits well past the newest message we know the room has, AND
|
|
98
|
+
* 2. it sits well past real time.
|
|
99
|
+
*
|
|
100
|
+
* Signal 1 alone is NOT enough, and that is the whole point of this
|
|
101
|
+
* function: a device whose local cache for a room is simply stale (very
|
|
102
|
+
* common - any room whose history hasn't been synced yet this session)
|
|
103
|
+
* sees a perfectly legitimate marker written by another device that HAS
|
|
104
|
+
* seen newer messages, and would happily "heal" it backwards, resurrecting
|
|
105
|
+
* already-read messages as unread on every device. A real timestamp can
|
|
106
|
+
* never be ahead of real time, so requiring signal 2 as well means a
|
|
107
|
+
* stale cache on its own can no longer trigger the self-heal.
|
|
108
|
+
*
|
|
109
|
+
* Signal 2 alone is not used either: it would let a device whose own
|
|
110
|
+
* clock is running SLOW mistake every correct marker for a corrupt one.
|
|
111
|
+
*/
|
|
112
|
+
export function isCorruptFutureReadMarker(
|
|
113
|
+
marker: number,
|
|
114
|
+
newestAckedMs: number
|
|
115
|
+
): boolean {
|
|
116
|
+
if (!Number.isFinite(marker) || marker <= 0) {return false;}
|
|
117
|
+
if (!(newestAckedMs > 0)) {return false;}
|
|
118
|
+
if (marker <= newestAckedMs + FUTURE_READ_MARKER_TOLERANCE_MS) {return false;}
|
|
119
|
+
return marker > Date.now() + FUTURE_READ_MARKER_TOLERANCE_MS;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolve the marker to STAMP (locally and to the server) for `room`,
|
|
124
|
+
* honouring an explicit read boundary when one is set.
|
|
125
|
+
*
|
|
126
|
+
* Bug #42 (a regression of #33, reintroduced by #38): #38 made every
|
|
127
|
+
* caller stamp the newest server-acknowledged message unconditionally.
|
|
128
|
+
* That is correct for a user who is at the bottom of the room, but wrong
|
|
129
|
+
* for a user who scrolled up, received messages, and left without
|
|
130
|
+
* scrolling back down - stamping "newest" there silently marks
|
|
131
|
+
* messages they never saw as read, both locally and (because the
|
|
132
|
+
* private-store merge is forward-only) permanently on the server.
|
|
133
|
+
*
|
|
134
|
+
* `boundaryTs` is the msgSortableMs of the newest message the user
|
|
135
|
+
* actually reached (see `readBoundaries` on `RoomMessagesState`,
|
|
136
|
+
* populated by MessageList's `onReadBoundaryChange`). When it's set and
|
|
137
|
+
* positive, it wins - but is still clamped to never exceed the newest
|
|
138
|
+
* server-acknowledged message, so a stale/corrupt boundary can't stamp a
|
|
139
|
+
* marker further ahead than the room's own history supports. When no
|
|
140
|
+
* boundary is set (the common case - the user is at the bottom), this is
|
|
141
|
+
* exactly `getServerReadTimestamp`.
|
|
142
|
+
*
|
|
143
|
+
* Never returns `Date.now()`, directly or indirectly - same rule as
|
|
144
|
+
* `getServerReadTimestamp` (bug #38).
|
|
145
|
+
*/
|
|
146
|
+
export function getReadMarkerTimestamp(
|
|
147
|
+
room: RoomForReadTimestamp | null | undefined,
|
|
148
|
+
heapState?: ReadTimestampHeapState | null,
|
|
149
|
+
boundaryTs?: number | null
|
|
150
|
+
): number {
|
|
151
|
+
const newestAcked = getServerReadTimestamp(room, heapState);
|
|
152
|
+
if (
|
|
153
|
+
typeof boundaryTs === 'number' &&
|
|
154
|
+
Number.isFinite(boundaryTs) &&
|
|
155
|
+
boundaryTs > 0
|
|
156
|
+
) {
|
|
157
|
+
// `newestAcked === 0` means this room has NOTHING server-acked and no
|
|
158
|
+
// prior marker, so there is nothing to clamp the boundary against -
|
|
159
|
+
// and MessageList derives the boundary from the rendered list, which
|
|
160
|
+
// at that point can only contain pending/failed sends carrying the
|
|
161
|
+
// DEVICE clock via `date`. Returning it here would smuggle a
|
|
162
|
+
// device-clock marker back in through the boundary (bug #38). Return
|
|
163
|
+
// 0 = "nothing to anchor to, skip the write", same contract as
|
|
164
|
+
// getServerReadTimestamp.
|
|
165
|
+
return newestAcked > 0 ? Math.min(boundaryTs, newestAcked) : 0;
|
|
166
|
+
}
|
|
167
|
+
return newestAcked;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The value to hand `flushLastViewedToPrivateStore` as `visibleRoomTs`
|
|
172
|
+
* for a room being left, or `undefined` when no boundary applies (the
|
|
173
|
+
* flush then uses its own newest-acked default).
|
|
174
|
+
*
|
|
175
|
+
* Exists so the SERVER write goes through the SAME clamp as the local
|
|
176
|
+
* stamp. Passing the raw redux boundary straight through would bypass
|
|
177
|
+
* `getReadMarkerTimestamp`'s clamp on exactly the path where a bad value
|
|
178
|
+
* is permanent: the private-store merge is forward-only, so a boundary
|
|
179
|
+
* that (through a pending own message's device `date`, or a stale
|
|
180
|
+
* readBoundaries entry) sat ahead of the room's real history would be
|
|
181
|
+
* written verbatim and could never be corrected again (bug #38).
|
|
182
|
+
*/
|
|
183
|
+
export function getFlushBoundaryTs(
|
|
184
|
+
room: RoomForReadTimestamp | null | undefined,
|
|
185
|
+
heapState?: ReadTimestampHeapState | null,
|
|
186
|
+
boundaryTs?: number | null
|
|
187
|
+
): number | undefined {
|
|
188
|
+
if (
|
|
189
|
+
typeof boundaryTs !== 'number' ||
|
|
190
|
+
!Number.isFinite(boundaryTs) ||
|
|
191
|
+
boundaryTs <= 0
|
|
192
|
+
) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
const clamped = getReadMarkerTimestamp(room, heapState, boundaryTs);
|
|
196
|
+
return clamped > 0 ? clamped : undefined;
|
|
197
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { setCurrentRoom, setIsLoading } from '../roomStore/roomsSlice';
|
|
2
|
-
import { updatedChatLastTimestamps } from './updatedChatLastTimestamps';
|
|
3
2
|
import { initRoomsPresence } from './initRoomsPresence';
|
|
4
3
|
import XmppClient from '../networking/xmppClient';
|
|
5
4
|
import { IConfig, IRoom, User } from '../types/types';
|
|
@@ -48,15 +47,16 @@ const initXmppRooms = async (
|
|
|
48
47
|
console.log(res);
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
updatedChatLastTimestamps
|
|
50
|
+
// getChatsPrivateStoreRequestStanza hydrates redux itself
|
|
51
|
+
// (dispatch(applyPrivateStoreMarkers(...)) internally, clamped +
|
|
52
|
+
// forward-only) - do not also apply the raw return value here. That
|
|
53
|
+
// used to double-dispatch via updatedChatLastTimestamps' unconditional
|
|
54
|
+
// setLastViewedTimestamp, which has neither the forward-only guard
|
|
55
|
+
// nor the future-marker clamp, so it could silently re-clobber a
|
|
56
|
+
// value the clamp had just corrected (bug #38).
|
|
57
|
+
await (xmmpClient as any as XmppClient).getChatsPrivateStoreRequestStanza();
|
|
55
58
|
} else {
|
|
56
|
-
|
|
57
|
-
const roomTimestampObject: [jid: string, timestamp: string] =
|
|
58
|
-
await xmmpClient.getChatsPrivateStoreRequestStanza();
|
|
59
|
-
updatedChatLastTimestamps(roomTimestampObject, store.dispatch);
|
|
59
|
+
await xmmpClient.getChatsPrivateStoreRequestStanza();
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
if (config?.refreshTokens?.enabled) {
|