@ethora/chat-component-rn 26.7.8 → 26.7.9
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 +0 -2
- package/lib/commonjs/assets/icons.js +2 -2
- package/lib/commonjs/assets/icons.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatRoom.js +17 -8
- 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 +46 -31
- 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 +125 -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/isOwnMessage.js +75 -0
- package/lib/commonjs/helpers/isOwnMessage.js.map +1 -0
- package/lib/commonjs/helpers/mimeToExtension.js +6 -14
- package/lib/commonjs/helpers/mimeToExtension.js.map +1 -1
- package/lib/commonjs/hooks/useChatRoomFocus.js +17 -8
- 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/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 +83 -6
- 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 +17 -8
- 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 +47 -32
- 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 +120 -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/isOwnMessage.js +70 -0
- package/lib/module/helpers/isOwnMessage.js.map +1 -0
- package/lib/module/helpers/mimeToExtension.js +6 -13
- package/lib/module/helpers/mimeToExtension.js.map +1 -1
- package/lib/module/hooks/useChatRoomFocus.js +17 -8
- 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/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 +83 -6
- 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 +69 -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/isOwnMessage.d.ts +36 -0
- package/lib/typescript/helpers/isOwnMessage.d.ts.map +1 -0
- package/lib/typescript/helpers/mimeToExtension.d.ts +0 -15
- package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -1
- 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/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 +13 -1
- 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 +0 -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 +21 -10
- 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 +47 -24
- package/src/helpers/createMessageFromXml.ts +20 -1
- package/src/helpers/getDisplayFileName.ts +87 -0
- package/src/helpers/getServerReadTimestamp.ts +120 -0
- package/src/helpers/initXmppRooms.tsx +9 -9
- package/src/helpers/isOwnMessage.ts +90 -0
- package/src/helpers/mimeToExtension.ts +6 -22
- package/src/hooks/useChatRoomFocus.ts +14 -5
- package/src/hooks/useChatWrapperInit.ts +12 -7
- 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 +84 -6
- package/src/types/models/room.model.ts +0 -1
- package/src/types/types.ts +0 -1
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
ShadeWrapper,
|
|
11
11
|
} from './StyledRoomComponents';
|
|
12
12
|
import { Text, View } from 'react-native';
|
|
13
|
+
import { getDisplayFileName } from '../../../helpers/getDisplayFileName';
|
|
13
14
|
|
|
14
15
|
const VideoContainer = styled.View`
|
|
15
16
|
position: relative;
|
|
@@ -40,14 +41,25 @@ const PlayButton = styled.View`
|
|
|
40
41
|
`;
|
|
41
42
|
|
|
42
43
|
interface LastMessageVideoProps
|
|
43
|
-
extends Pick<
|
|
44
|
+
extends Pick<
|
|
45
|
+
LastMessage,
|
|
46
|
+
'user' | 'location' | 'originalName' | 'fileName' | 'mimetype'
|
|
47
|
+
> {}
|
|
44
48
|
|
|
45
49
|
const LastMessageVideo: FC<LastMessageVideoProps> = ({
|
|
46
50
|
user,
|
|
47
51
|
location,
|
|
48
52
|
originalName,
|
|
53
|
+
fileName,
|
|
54
|
+
mimetype,
|
|
49
55
|
}) => {
|
|
50
56
|
const fileToken = useFileToken();
|
|
57
|
+
// Bug #40: prefer the sender's original name over the stored hash
|
|
58
|
+
// name; only fall back to the generic "file" label when neither is
|
|
59
|
+
// present at all (avoids a synthetic "media_<timestamp>.bin" here).
|
|
60
|
+
const displayName = originalName || fileName
|
|
61
|
+
? getDisplayFileName({ originalName, fileName, mimetype })
|
|
62
|
+
: 'file';
|
|
51
63
|
return (
|
|
52
64
|
<LastRoomMessageContainer>
|
|
53
65
|
<LastRoomMessageName>{user?.name || ''}:</LastRoomMessageName>
|
|
@@ -64,7 +76,7 @@ const LastMessageVideo: FC<LastMessageVideoProps> = ({
|
|
|
64
76
|
</ShadeWrapper>
|
|
65
77
|
<PlayButton><Text>▶</Text></PlayButton>
|
|
66
78
|
</VideoContainer>
|
|
67
|
-
<LastRoomMessageText>{
|
|
79
|
+
<LastRoomMessageText>{displayName}</LastRoomMessageText>
|
|
68
80
|
</View>
|
|
69
81
|
</LastRoomMessageContainer>
|
|
70
82
|
);
|
|
@@ -22,6 +22,12 @@ interface CustomMessageImageProps {
|
|
|
22
22
|
/** Full-size file. Opened in the preview modal, NOT rendered inline. */
|
|
23
23
|
fileURL: string;
|
|
24
24
|
fileName: string;
|
|
25
|
+
/**
|
|
26
|
+
* The sender's original file name (bug #40) - kept alongside `fileName`
|
|
27
|
+
* so the preview modal's save/share/download logic can prefer it over
|
|
28
|
+
* the server's stored/hashed name.
|
|
29
|
+
*/
|
|
30
|
+
originalName?: string;
|
|
25
31
|
mimetype: string;
|
|
26
32
|
/**
|
|
27
33
|
* Server-generated thumbnail — what the bubble actually renders, same
|
|
@@ -42,6 +48,7 @@ const FALLBACK_IMAGE =
|
|
|
42
48
|
const CustomMessageImage: React.FC<CustomMessageImageProps> = ({
|
|
43
49
|
fileURL,
|
|
44
50
|
fileName,
|
|
51
|
+
originalName,
|
|
45
52
|
mimetype,
|
|
46
53
|
locationPreview,
|
|
47
54
|
}) => {
|
|
@@ -84,7 +91,7 @@ const CustomMessageImage: React.FC<CustomMessageImageProps> = ({
|
|
|
84
91
|
}, [displayURL]);
|
|
85
92
|
|
|
86
93
|
const handleOpen = () => {
|
|
87
|
-
dispatch(setActiveFile({ fileName, fileURL, mimetype }));
|
|
94
|
+
dispatch(setActiveFile({ fileName, fileURL, mimetype, originalName }));
|
|
88
95
|
dispatch(setActiveModal(MODAL_TYPES.FILE_PREVIEW));
|
|
89
96
|
};
|
|
90
97
|
|
|
@@ -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,9 @@ import {
|
|
|
46
46
|
clearVisibleRoom,
|
|
47
47
|
setLastViewedTimestamp,
|
|
48
48
|
deleteRoomMessage,
|
|
49
|
-
msgSortableMs,
|
|
50
49
|
setUnreadSyncing,
|
|
51
50
|
} from '../roomStore/roomsSlice';
|
|
51
|
+
import { getServerReadTimestamp } from '../helpers/getServerReadTimestamp';
|
|
52
52
|
import { runHistoryPreloadScheduler } from '../helpers/historyPreloadScheduler';
|
|
53
53
|
import { updateMessagesTillLast } from '../helpers/updateMessagesTillLast';
|
|
54
54
|
import { secureUserStorage } from '../helpers/secureUserStorage';
|
|
@@ -638,16 +638,30 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
638
638
|
// Mark the open room "not visible" while backgrounded so messages
|
|
639
639
|
// that arrive (or MAM-replay on reconnect) count as unread instead
|
|
640
640
|
// of being silently treated as read — the "mounted == visible ==
|
|
641
|
-
// read" gap. Stamp lastViewed
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
//
|
|
641
|
+
// read" gap. Stamp lastViewed=<newest server-acked message>, not
|
|
642
|
+
// Date.now(), as the read baseline: a device clock running ahead
|
|
643
|
+
// would otherwise write a future marker that the forward-only
|
|
644
|
+
// private-store merge can never correct again (bug #38). Then
|
|
645
|
+
// clear visibility and flush that marker to the server private
|
|
646
|
+
// store. Restored on the next 'active' transition above. Handled
|
|
647
|
+
// here in the provider so consumers using the component as a
|
|
648
|
+
// package get correct unread without reaching into the chat store
|
|
649
|
+
// themselves.
|
|
646
650
|
visibleBeforeBackground = visibleRoomJID;
|
|
647
651
|
if (visibleRoomJID) {
|
|
648
|
-
|
|
649
|
-
|
|
652
|
+
const readTs = getServerReadTimestamp(
|
|
653
|
+
rooms?.[visibleRoomJID],
|
|
654
|
+
state.roomHeapSlice
|
|
650
655
|
);
|
|
656
|
+
// Nothing to anchor to yet (no known messages, no prior marker)
|
|
657
|
+
// - skip the local stamp rather than fall back to the device
|
|
658
|
+
// clock. The flush below still runs so the visibility clear
|
|
659
|
+
// isn't blocked on this.
|
|
660
|
+
if (readTs > 0) {
|
|
661
|
+
store.dispatch(
|
|
662
|
+
setLastViewedTimestamp({ chatJID: visibleRoomJID, timestamp: readTs })
|
|
663
|
+
);
|
|
664
|
+
}
|
|
651
665
|
// Drop the "New messages" divider for the room we're leaving so it
|
|
652
666
|
// doesn't linger when the user comes back (mirrors ChatRoom's
|
|
653
667
|
// unmount cleanup, which never runs while the pane stays mounted).
|
|
@@ -672,11 +686,11 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
672
686
|
// "shown" from "hidden", so the unread middleware would keep the open
|
|
673
687
|
// room's badge at 0 and messages arriving while you're elsewhere look
|
|
674
688
|
// 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.)
|
|
689
|
+
// background/foreground handling: hidden → stamp lastViewed=<newest
|
|
690
|
+
// server-acked message> + clear room visibility (so later messages
|
|
691
|
+
// count) + flush; shown → restore visibility for the open room. Doing
|
|
692
|
+
// it here means consumers never touch the chat store themselves.
|
|
693
|
+
// (Undefined = host unmounts on hide, nothing to do.)
|
|
680
694
|
// -----------------------------------------------------------
|
|
681
695
|
const chatWasVisibleRef = useRef(false);
|
|
682
696
|
useEffect(() => {
|
|
@@ -692,9 +706,18 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
692
706
|
}
|
|
693
707
|
} else {
|
|
694
708
|
if (wasVisible && activeRoomJID) {
|
|
695
|
-
|
|
696
|
-
|
|
709
|
+
// Never Date.now(): a fast device clock would write a future
|
|
710
|
+
// marker that the forward-only private-store merge could then
|
|
711
|
+
// never correct (bug #38).
|
|
712
|
+
const readTs = getServerReadTimestamp(
|
|
713
|
+
rooms?.[activeRoomJID],
|
|
714
|
+
state.roomHeapSlice
|
|
697
715
|
);
|
|
716
|
+
if (readTs > 0) {
|
|
717
|
+
store.dispatch(
|
|
718
|
+
setLastViewedTimestamp({ chatJID: activeRoomJID, timestamp: readTs })
|
|
719
|
+
);
|
|
720
|
+
}
|
|
698
721
|
// Drop the "New messages" divider so it's gone when the user
|
|
699
722
|
// returns to the chat (mirrors ChatRoom's unmount cleanup).
|
|
700
723
|
store.dispatch(
|
|
@@ -726,18 +749,18 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
726
749
|
if (room.messages === lastMessagesRef) {return;}
|
|
727
750
|
lastMessagesRef = room.messages;
|
|
728
751
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
if (newest <= lastStampedMs) {return;}
|
|
752
|
+
// getServerReadTimestamp (not a hand-rolled scan + Date.now()):
|
|
753
|
+
// it excludes pending/optimistic messages, which carry the DEVICE
|
|
754
|
+
// send time via `date` until the server echoes them back - a
|
|
755
|
+
// manual scan without that exclusion could pick up a pending
|
|
756
|
+
// message's device timestamp here and write a future marker the
|
|
757
|
+
// same way a raw Date.now() would (bug #38).
|
|
758
|
+
const newest = getServerReadTimestamp(room, s.roomHeapSlice);
|
|
759
|
+
if (!newest || newest <= lastStampedMs) {return;}
|
|
737
760
|
lastStampedMs = newest;
|
|
738
761
|
|
|
739
762
|
store.dispatch(
|
|
740
|
-
setLastViewedTimestamp({ chatJID: jid, timestamp:
|
|
763
|
+
setLastViewedTimestamp({ chatJID: jid, timestamp: newest })
|
|
741
764
|
);
|
|
742
765
|
|
|
743
766
|
if (flushTimer) {clearTimeout(flushTimer);}
|
|
@@ -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,120 @@
|
|
|
1
|
+
import type { IMessage, IRoom } from '../types/types';
|
|
2
|
+
import { msgSortableMs } from '../roomStore/roomsSlice';
|
|
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` (roomsSlice.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
|
+
}
|
|
@@ -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) {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { IMessage } from '../types/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bug #41: during reconnect catch-up (app was backgrounded/offline, then
|
|
5
|
+
* comes back online), messages replayed from MAM could transiently render
|
|
6
|
+
* on the RIGHT (as if sent by the current user) before flipping to the
|
|
7
|
+
* LEFT a moment later. Root cause: `message.user.id === walletAddress`
|
|
8
|
+
* treats `undefined === undefined` (or `'' === ''`) as a match, and the
|
|
9
|
+
* two sides could independently be empty for one frame -- the message's
|
|
10
|
+
* sender id before the MAM-derived value settled, and/or the current
|
|
11
|
+
* user's id before it rehydrated. Two blank ids must never compare equal.
|
|
12
|
+
*
|
|
13
|
+
* A second, related issue: the same logical sender id can show up in
|
|
14
|
+
* different shapes depending on which code path produced it -- a bare
|
|
15
|
+
* id ("wallet123") or a bare JID with an incidental session resource
|
|
16
|
+
* ("wallet123@host/sessionResource", as `senderJID` attrs carry on the
|
|
17
|
+
* wire -- see src/assets/test.xml). In every shape used in this
|
|
18
|
+
* codebase the identifying segment is the part BEFORE the first '@' (or
|
|
19
|
+
* before '/' when there's no '@'); anything after that -- domain,
|
|
20
|
+
* resource -- is noise. Comparing raw strings with `===` fails whenever
|
|
21
|
+
* one side still carries that noise and the other doesn't. Normalizing
|
|
22
|
+
* both sides (strip resource, strip domain, lowercase) before comparing
|
|
23
|
+
* makes the check tolerant of that drift.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export interface OwnMessageUserLike {
|
|
27
|
+
id?: string | null;
|
|
28
|
+
xmppUsername?: string | null;
|
|
29
|
+
walletAddress?: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type OwnMessageCurrentUser =
|
|
33
|
+
| OwnMessageUserLike
|
|
34
|
+
| string
|
|
35
|
+
| null
|
|
36
|
+
| undefined;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Strip a MUC resource (`room@host/resource` -> `room@host`) and then a
|
|
40
|
+
* JID domain (`local@host` -> `local`), lowercasing the result. Applies
|
|
41
|
+
* to both plain ids and full/partial JIDs so either shape normalizes to
|
|
42
|
+
* the same comparable value.
|
|
43
|
+
*/
|
|
44
|
+
const normalizeId = (raw: unknown): string => {
|
|
45
|
+
const value = String(raw ?? '').trim();
|
|
46
|
+
if (!value) {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
const withoutResource = value.split('/')[0];
|
|
50
|
+
const withoutDomain = withoutResource.includes('@')
|
|
51
|
+
? withoutResource.split('@')[0]
|
|
52
|
+
: withoutResource;
|
|
53
|
+
return withoutDomain.toLowerCase();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Same precedence the rest of the app uses when it needs "who am I" as a
|
|
57
|
+
// single id: xmppUsername first, walletAddress as the fallback.
|
|
58
|
+
const resolveCurrentUserId = (currentUser: OwnMessageCurrentUser): string => {
|
|
59
|
+
if (typeof currentUser === 'string') {
|
|
60
|
+
return currentUser;
|
|
61
|
+
}
|
|
62
|
+
if (!currentUser) {
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
return (
|
|
66
|
+
currentUser.xmppUsername || currentUser.walletAddress || currentUser.id || ''
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Robustly determine whether `message` was sent by `currentUser`. Never
|
|
72
|
+
* returns true from two blank/undefined ids, and tolerates bare-id vs.
|
|
73
|
+
* full-JID mismatches between the sender id and the current user id.
|
|
74
|
+
*/
|
|
75
|
+
export const isOwnMessage = (
|
|
76
|
+
message: Pick<IMessage, 'user'> | null | undefined,
|
|
77
|
+
currentUser: OwnMessageCurrentUser
|
|
78
|
+
): boolean => {
|
|
79
|
+
const messageUserId = normalizeId(message?.user?.id);
|
|
80
|
+
if (!messageUserId) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const currentUserId = normalizeId(resolveCurrentUserId(currentUser));
|
|
85
|
+
if (!currentUserId) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return messageUserId === currentUserId;
|
|
90
|
+
};
|
|
@@ -289,25 +289,9 @@ export function ensureFilenameHasExtension(
|
|
|
289
289
|
return `${base}${getExtensionForMime(mime)}`;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
*
|
|
299
|
-
* Always returns a string with a valid extension.
|
|
300
|
-
*/
|
|
301
|
-
export function deriveDisplayFilename(opts: {
|
|
302
|
-
fileName?: string | null;
|
|
303
|
-
originalName?: string | null;
|
|
304
|
-
url?: string | null;
|
|
305
|
-
mime?: string | null;
|
|
306
|
-
}): string {
|
|
307
|
-
const candidate =
|
|
308
|
-
(opts.fileName && opts.fileName.trim()) ||
|
|
309
|
-
(opts.originalName && opts.originalName.trim()) ||
|
|
310
|
-
filenameFromUrl(opts.url) ||
|
|
311
|
-
'';
|
|
312
|
-
return ensureFilenameHasExtension(candidate, opts.mime);
|
|
313
|
-
}
|
|
292
|
+
// NOTE: the "which name wins" decision (originalName vs the stored
|
|
293
|
+
// fileName vs the URL) lives in helpers/getDisplayFileName.ts - that is
|
|
294
|
+
// the single source of truth used across display, save, share and
|
|
295
|
+
// download call sites (bug #40: this used to be duplicated here with
|
|
296
|
+
// the priority backwards, so attachments showed/saved under the
|
|
297
|
+
// server's hash name instead of the sender's original one).
|