@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
|
@@ -6,10 +6,12 @@ import { useDispatch } from 'react-redux';
|
|
|
6
6
|
import MessageList from './MessageList';
|
|
7
7
|
import SendInput from '../styled/SendInput';
|
|
8
8
|
import {
|
|
9
|
+
clearReadBoundary,
|
|
9
10
|
clearVisibleRoom,
|
|
10
11
|
deleteRoomMessage,
|
|
11
12
|
setEditAction,
|
|
12
13
|
setLastViewedTimestamp,
|
|
14
|
+
setReadBoundary,
|
|
13
15
|
setVisibleRoom,
|
|
14
16
|
} from '../../roomStore/roomsSlice';
|
|
15
17
|
import Loader from '../styled/Loader';
|
|
@@ -28,6 +30,7 @@ import { IConfig } from '../../types/models/config.model';
|
|
|
28
30
|
import { useRoomInitialization } from '../../hooks/useRoomInitialization';
|
|
29
31
|
import { useRoomState } from '../../hooks/useRoomState';
|
|
30
32
|
import { useChatSettingState } from '../../hooks/useChatSettingState';
|
|
33
|
+
import { isOwnMessage } from '../../helpers/isOwnMessage';
|
|
31
34
|
import CustomTypingIndicator from '../styled/StyledInputComponents/CustomTypingIndicator';
|
|
32
35
|
// import {PanGestureHandler} from 'react-native-gesture-handler';
|
|
33
36
|
import { FlatList } from 'react-native';
|
|
@@ -46,6 +49,10 @@ import {
|
|
|
46
49
|
} from 'react-native-keyboard-controller';
|
|
47
50
|
import useComposing from '../../hooks/useComposing';
|
|
48
51
|
import { store } from '../../roomStore';
|
|
52
|
+
import {
|
|
53
|
+
getFlushBoundaryTs,
|
|
54
|
+
getReadMarkerTimestamp,
|
|
55
|
+
} from '../../helpers/getServerReadTimestamp';
|
|
49
56
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
50
57
|
import {
|
|
51
58
|
getInputDockPaddingBottom,
|
|
@@ -216,47 +223,100 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
|
|
|
216
223
|
// Tracks what the user actually saw: `null` while they're at the
|
|
217
224
|
// bottom (safe to mark everything read), or the timestamp of the
|
|
218
225
|
// newest message visible when they scrolled away from it. Reported
|
|
219
|
-
// by MessageList via onReadBoundaryChange
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
226
|
+
// by MessageList via onReadBoundaryChange, and mirrored into redux
|
|
227
|
+
// (`rooms.readBoundaries`) rather than kept only in a local ref - it
|
|
228
|
+
// is the single source of truth every "leaving this room" path
|
|
229
|
+
// consults (xmppProvider's AppState background handler, its
|
|
230
|
+
// `isVisible=false` handler, the live `advance()` effect, and
|
|
231
|
+
// `useChatRoomFocus`'s `leaveRoom`), not just this component's own
|
|
232
|
+
// unmount cleanup below. Without a boundary, leaving a room (or
|
|
233
|
+
// backgrounding) while scrolled up stamped `now()` (bug #33) or,
|
|
234
|
+
// after the #38 fix, the newest acked message regardless of scroll
|
|
235
|
+
// position (bug #42) - both silently discard genuinely-unread
|
|
236
|
+
// messages.
|
|
237
|
+
//
|
|
238
|
+
// MessageList reports the boundary from onScroll, which fires on
|
|
239
|
+
// every frame of a drag (the "back at the bottom" branch re-reports
|
|
240
|
+
// `null` each time). Mirror only actual CHANGES into redux: an
|
|
241
|
+
// unconditional dispatch per scroll frame would run the whole
|
|
242
|
+
// middleware chain and, worse, reset persistenceMiddleware's 200 ms
|
|
243
|
+
// debounce on every frame, starving the persisted write for as long
|
|
244
|
+
// as the user keeps scrolling.
|
|
245
|
+
const lastBoundarySentRef = useRef<number | null>(null);
|
|
224
246
|
const handleReadBoundaryChange = useCallback((boundaryTs: number | null) => {
|
|
225
|
-
|
|
226
|
-
|
|
247
|
+
if (!activeRoomJID) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const next = boundaryTs && boundaryTs > 0 ? boundaryTs : null;
|
|
251
|
+
if (lastBoundarySentRef.current === next) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
lastBoundarySentRef.current = next;
|
|
255
|
+
dispatch(setReadBoundary({ jid: activeRoomJID, ts: next }));
|
|
256
|
+
}, [activeRoomJID, dispatch]);
|
|
227
257
|
|
|
228
258
|
useEffect(() => {
|
|
229
259
|
if (!activeRoomJID) {
|
|
230
260
|
return;
|
|
231
261
|
}
|
|
232
262
|
|
|
233
|
-
|
|
263
|
+
lastBoundarySentRef.current = null;
|
|
264
|
+
dispatch(setReadBoundary({ jid: activeRoomJID, ts: null }));
|
|
234
265
|
dispatch(setVisibleRoom({ roomJID: activeRoomJID }));
|
|
235
266
|
setIsLoadingMore(false);
|
|
236
267
|
return () => {
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
268
|
+
const state = store.getState();
|
|
269
|
+
const rooms = state.rooms?.rooms;
|
|
270
|
+
const heapState = state.roomHeapSlice;
|
|
271
|
+
const boundaryTs = state.rooms?.readBoundaries?.[activeRoomJID] ?? null;
|
|
272
|
+
// getReadMarkerTimestamp honours the boundary (the newest message
|
|
273
|
+
// the user actually reached) when one is set, and otherwise falls
|
|
274
|
+
// back to the newest server-acked message - never the device
|
|
275
|
+
// clock: a fast device clock would write a future marker that the
|
|
276
|
+
// forward-only private-store merge could never correct (bug #38).
|
|
277
|
+
const timestamp = getReadMarkerTimestamp(
|
|
278
|
+
rooms?.[activeRoomJID],
|
|
279
|
+
heapState,
|
|
280
|
+
boundaryTs,
|
|
243
281
|
);
|
|
282
|
+
if (timestamp > 0) {
|
|
283
|
+
dispatch(
|
|
284
|
+
setLastViewedTimestamp({
|
|
285
|
+
chatJID: activeRoomJID,
|
|
286
|
+
timestamp,
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
244
290
|
dispatch(clearVisibleRoom());
|
|
245
291
|
const liveClient = clientRef.current;
|
|
246
292
|
if (liveClient) {
|
|
247
293
|
liveClient
|
|
248
|
-
.flushLastViewedToPrivateStoreStanza(
|
|
294
|
+
.flushLastViewedToPrivateStoreStanza(rooms, {
|
|
249
295
|
visibleRoomJID: activeRoomJID,
|
|
250
296
|
// Carry the same boundary to the SERVER marker. Without
|
|
251
|
-
// this the flush defaults to
|
|
297
|
+
// this the flush defaults to "everything" for the visible
|
|
252
298
|
// room, so messages the user never scrolled down to come
|
|
253
|
-
// back as read on the next login
|
|
299
|
+
// back as read on the next login - the local count was
|
|
254
300
|
// right but the server overrode it.
|
|
255
|
-
|
|
301
|
+
// Clamped through the same helper as the local stamp -
|
|
302
|
+
// an unclamped boundary would bypass the newest-acked
|
|
303
|
+
// ceiling on the one path where a bad value is permanent
|
|
304
|
+
// (the private-store merge is forward-only, bug #38).
|
|
305
|
+
visibleRoomTs: getFlushBoundaryTs(
|
|
306
|
+
rooms?.[activeRoomJID],
|
|
307
|
+
heapState,
|
|
308
|
+
boundaryTs,
|
|
309
|
+
),
|
|
256
310
|
})
|
|
257
311
|
.catch(() => {});
|
|
258
312
|
}
|
|
259
313
|
dispatch(deleteRoomMessage({ roomJID: activeRoomJID, messageId: 'delimiter-new' }));
|
|
314
|
+
// The room is genuinely being left (unmount, or activeRoomJID
|
|
315
|
+
// changed to a different room) - release the boundary now that
|
|
316
|
+
// it's been consumed, so a stale value can't leak into the next
|
|
317
|
+
// time this room becomes visible.
|
|
318
|
+
dispatch(clearReadBoundary({ jid: activeRoomJID }));
|
|
319
|
+
lastBoundarySentRef.current = null;
|
|
260
320
|
setIsLoadingMore(false);
|
|
261
321
|
};
|
|
262
322
|
}, [activeRoomJID, dispatch]);
|
|
@@ -292,6 +352,8 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
|
|
|
292
352
|
const inputDockPaddingBottom = getInputDockPaddingBottom({
|
|
293
353
|
platform: Platform.OS,
|
|
294
354
|
bottomInset: insets.bottom,
|
|
355
|
+
configuredPadding: configWithEventHandlers?.inputDockPaddingBottom,
|
|
356
|
+
hostOwnsLayout: !!configWithEventHandlers?.disableKeyboardAvoidingView,
|
|
295
357
|
});
|
|
296
358
|
|
|
297
359
|
// Keyboard avoidance is delegated to react-native-keyboard-controller's
|
|
@@ -440,7 +502,7 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
|
|
|
440
502
|
CustomMessageComponent ? (
|
|
441
503
|
<CustomMessageComponent
|
|
442
504
|
message={decorated.message}
|
|
443
|
-
isUser={decorated.message
|
|
505
|
+
isUser={isOwnMessage(decorated.message, user)}
|
|
444
506
|
isReply={false}
|
|
445
507
|
/>
|
|
446
508
|
) : null
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
Text,
|
|
12
12
|
View,
|
|
13
13
|
} from 'react-native';
|
|
14
|
-
import {
|
|
14
|
+
import { isLikelyAudio } from '../../helpers/mimeToExtension';
|
|
15
|
+
import { getDisplayFileName } from '../../helpers/getDisplayFileName';
|
|
15
16
|
import { FileIcon, PlayIcon } from '../../assets/icons';
|
|
16
17
|
import { defaultMediaDims } from '../../helpers/mediaDimensions';
|
|
17
18
|
import { useChatSettingState } from '../../hooks/useChatSettingState';
|
|
@@ -148,11 +149,11 @@ const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
|
148
149
|
const messageText = appendFileToken(rawMessageText, fileToken);
|
|
149
150
|
|
|
150
151
|
if (mimeType) {
|
|
151
|
-
const displayName =
|
|
152
|
-
fileName: message?.fileName,
|
|
152
|
+
const displayName = getDisplayFileName({
|
|
153
153
|
originalName: (message as any)?.originalName,
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
fileName: message?.fileName,
|
|
155
|
+
location: rawLocation || rawMessageText,
|
|
156
|
+
mimetype: mimeType,
|
|
156
157
|
});
|
|
157
158
|
const isAudioPayload = isLikelyAudio(
|
|
158
159
|
mimeType,
|
|
@@ -180,6 +181,7 @@ const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
|
180
181
|
return (
|
|
181
182
|
<CustomMessageImage
|
|
182
183
|
fileName={displayName}
|
|
184
|
+
originalName={(message as any)?.originalName}
|
|
183
185
|
// Full-size original: opened by the preview modal.
|
|
184
186
|
fileURL={location || messageText || ''}
|
|
185
187
|
// ...and the thumbnail the bubble actually renders, which is
|
|
@@ -192,6 +194,7 @@ const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
|
192
194
|
return (
|
|
193
195
|
<CustomMessageVideo
|
|
194
196
|
fileName={displayName}
|
|
197
|
+
originalName={(message as any)?.originalName}
|
|
195
198
|
fileURL={location || ''}
|
|
196
199
|
// `messageText` is the message's locationPreview — the poster
|
|
197
200
|
// frame the backend generated for this video. Handing it over
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { FC, Fragment, useMemo } from 'react';
|
|
2
2
|
import { IConfig, IMessage } from '../../types/types';
|
|
3
|
+
import { isOwnMessage } from '../../helpers/isOwnMessage';
|
|
3
4
|
import DateLabel from '../styled/DateLabel';
|
|
4
5
|
import SystemMessage from './SystemMessage';
|
|
5
6
|
import NewMessageLabel from '../styled/NewMessageLabel';
|
|
@@ -46,7 +47,10 @@ export const MessageContainer: FC<MessageContainerProps> = ({
|
|
|
46
47
|
isReply,
|
|
47
48
|
className,
|
|
48
49
|
}) => {
|
|
49
|
-
|
|
50
|
+
// Bug #41: `message.user.id === walletAddress` used to treat two
|
|
51
|
+
// empty/undefined ids as a match, briefly rendering catch-up messages
|
|
52
|
+
// from other users as our own. isOwnMessage() never does that.
|
|
53
|
+
const isUser = isOwnMessage(message, walletAddress);
|
|
50
54
|
|
|
51
55
|
const messageDate = new Date(message.date);
|
|
52
56
|
|
|
@@ -29,6 +29,7 @@ import { ArowDownIcon } from '../../assets/icons';
|
|
|
29
29
|
import CustomTypingIndicator from '../styled/StyledInputComponents/CustomTypingIndicator';
|
|
30
30
|
import { getIconColor } from '../../helpers/getIconColor';
|
|
31
31
|
import { getChatBackgroundColor } from '../../helpers/getChatBackground';
|
|
32
|
+
import { isOwnMessage } from '../../helpers/isOwnMessage';
|
|
32
33
|
|
|
33
34
|
interface MessageListProps<TMessage extends IMessage> {
|
|
34
35
|
CustomMessage?: React.ComponentType<{
|
|
@@ -438,7 +439,7 @@ const MessageList = <TMessage extends IMessage>({
|
|
|
438
439
|
{CustomMessage && (
|
|
439
440
|
<CustomMessage
|
|
440
441
|
message={activeMessage}
|
|
441
|
-
isUser={activeMessage
|
|
442
|
+
isUser={isOwnMessage(activeMessage, user)}
|
|
442
443
|
isReply={isReply}
|
|
443
444
|
/>
|
|
444
445
|
)}
|
|
@@ -26,8 +26,6 @@ import { deleteRoom, updateRoom } from '../../../roomStore/roomsSlice';
|
|
|
26
26
|
import Loader from '../../styled/Loader';
|
|
27
27
|
import {
|
|
28
28
|
AddNewIcon,
|
|
29
|
-
BellIcon,
|
|
30
|
-
BellOffIcon,
|
|
31
29
|
DeleteIcon,
|
|
32
30
|
EditIcon,
|
|
33
31
|
LeaveIcon,
|
|
@@ -38,7 +36,7 @@ import { useChatSettingState } from '../../../hooks/useChatSettingState';
|
|
|
38
36
|
import { chatTextStyle } from '../../../helpers/typography';
|
|
39
37
|
import { getElementFont } from '../../../helpers/getElementFont';
|
|
40
38
|
import { getIconColor } from '../../../helpers/getIconColor';
|
|
41
|
-
import { deleteRoomMember
|
|
39
|
+
import { deleteRoomMember } from '../../../networking/api-requests/rooms.api';
|
|
42
40
|
import { RoomMember } from '../../../types/models/room.model';
|
|
43
41
|
import { setActiveModal, setSelectedUser } from '../../../roomStore/chatSettingsSlice';
|
|
44
42
|
import { MODAL_TYPES } from '../../../helpers/constants/MODAL_TYPES';
|
|
@@ -357,40 +355,8 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
|
|
|
357
355
|
return () => sub.remove();
|
|
358
356
|
}, [isSearchOpen]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
359
357
|
|
|
360
|
-
const muteInFlightRef = useRef(false);
|
|
361
|
-
const toggleMuted = async () => {
|
|
362
|
-
if (!activeRoom?.jid || !activeRoom?.name || muteInFlightRef.current) {return;}
|
|
363
|
-
const next = !activeRoom.muted;
|
|
364
|
-
muteInFlightRef.current = true;
|
|
365
|
-
dispatch(updateRoom({ jid: activeRoom.jid, updates: { muted: next } }));
|
|
366
|
-
try {
|
|
367
|
-
const confirmed = await setRoomMuted(activeRoom.name, next);
|
|
368
|
-
if (confirmed !== next) {
|
|
369
|
-
dispatch(updateRoom({ jid: activeRoom.jid, updates: { muted: confirmed } }));
|
|
370
|
-
}
|
|
371
|
-
} catch (error) {
|
|
372
|
-
dispatch(updateRoom({ jid: activeRoom.jid, updates: { muted: !next } }));
|
|
373
|
-
console.warn('[ethora-rn] mute toggle failed', error);
|
|
374
|
-
showToast({
|
|
375
|
-
id: Date.now().toString(),
|
|
376
|
-
title: 'Error',
|
|
377
|
-
message: t('toast.muteFailed'),
|
|
378
|
-
type: 'error',
|
|
379
|
-
});
|
|
380
|
-
} finally {
|
|
381
|
-
muteInFlightRef.current = false;
|
|
382
|
-
}
|
|
383
|
-
};
|
|
384
|
-
|
|
385
358
|
const heroActions: HeroAction[] = useMemo(
|
|
386
359
|
() => [
|
|
387
|
-
{
|
|
388
|
-
key: 'mute',
|
|
389
|
-
label: activeRoom?.muted ? t('action.muted') : t('action.unmuted'),
|
|
390
|
-
icon: (color: string) =>
|
|
391
|
-
activeRoom?.muted ? <BellOffIcon color={color} /> : <BellIcon color={color} />,
|
|
392
|
-
onPress: toggleMuted,
|
|
393
|
-
},
|
|
394
360
|
{
|
|
395
361
|
key: 'search',
|
|
396
362
|
label: t('action.search'),
|
|
@@ -410,7 +376,7 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
|
|
|
410
376
|
onPress: () => setIsReportOpen(true),
|
|
411
377
|
},
|
|
412
378
|
],
|
|
413
|
-
[t, activeRoom?.jid
|
|
379
|
+
[t, activeRoom?.jid] // eslint-disable-line react-hooks/exhaustive-deps
|
|
414
380
|
);
|
|
415
381
|
|
|
416
382
|
// "…" menu. Edit is the chat picture — the only chat detail this SDK can
|
|
@@ -31,7 +31,12 @@ import { useToast } from '../../../context/ToastContext';
|
|
|
31
31
|
import PdfViewer from './PdfView';
|
|
32
32
|
import DocumentViewer from './DocumentViewer';
|
|
33
33
|
import AudioMessage from '../../styled/AudioMessage';
|
|
34
|
-
import {
|
|
34
|
+
import { isLikelyAudio } from '../../../helpers/mimeToExtension';
|
|
35
|
+
import {
|
|
36
|
+
getDisplayFileName,
|
|
37
|
+
getUniqueFileName,
|
|
38
|
+
sanitizeFileNameForPath,
|
|
39
|
+
} from '../../../helpers/getDisplayFileName';
|
|
35
40
|
|
|
36
41
|
// MIME types Google's gview embed renders reliably. Everything else
|
|
37
42
|
// falls through to the info-card so the user can still download.
|
|
@@ -128,6 +133,19 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
128
133
|
|
|
129
134
|
if (!activeFile) {return null;}
|
|
130
135
|
|
|
136
|
+
// Bug #40: prefer the sender's original name over the server's stored
|
|
137
|
+
// hash name everywhere this modal shows or saves/shares the file.
|
|
138
|
+
// Sanitized so it's safe to use as an actual path segment (strips path
|
|
139
|
+
// separators/control chars) when writing to disk below.
|
|
140
|
+
const displayFileName = sanitizeFileNameForPath(
|
|
141
|
+
getDisplayFileName({
|
|
142
|
+
originalName: activeFile.originalName,
|
|
143
|
+
fileName: activeFile.fileName,
|
|
144
|
+
location: activeFile.fileURL,
|
|
145
|
+
mimetype: activeFile.mimetype,
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
|
|
131
149
|
const requestStoragePermission = async () => {
|
|
132
150
|
const MediaLibrary = getMediaLibrary();
|
|
133
151
|
if (!MediaLibrary?.requestPermissionsAsync) {return false;}
|
|
@@ -151,12 +169,7 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
151
169
|
}
|
|
152
170
|
|
|
153
171
|
try {
|
|
154
|
-
const
|
|
155
|
-
activeFile.fileName,
|
|
156
|
-
activeFile.mimetype
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
const filePath = FileSystem.cacheDirectory + fileName;
|
|
172
|
+
const filePath = FileSystem.cacheDirectory + displayFileName;
|
|
160
173
|
const download = await FileSystem.downloadAsync(withFileToken(activeFile.fileURL), filePath);
|
|
161
174
|
|
|
162
175
|
if (download.status === 200) {
|
|
@@ -171,17 +184,39 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
171
184
|
Alert.alert('Error', 'Failed to save the file.');
|
|
172
185
|
}
|
|
173
186
|
} catch (err) {
|
|
174
|
-
Alert.alert('Error', `Failed to save the file: ${
|
|
187
|
+
Alert.alert('Error', `Failed to save the file: ${displayFileName}`);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Best-effort existing-name lookup for the Android SAF branch below: SAF
|
|
192
|
+
// hands back content URIs, not plain names, so this decodes each one and
|
|
193
|
+
// takes its last path segment. If the provider's URI shape doesn't match
|
|
194
|
+
// what we expect, we fail open (empty set) rather than block the save -
|
|
195
|
+
// worst case we skip de-duplication, we never crash the save flow.
|
|
196
|
+
const listSafDirectoryNames = async (
|
|
197
|
+
saf: any,
|
|
198
|
+
dirUri: string
|
|
199
|
+
): Promise<Set<string>> => {
|
|
200
|
+
try {
|
|
201
|
+
const uris: string[] = await saf.readDirectoryAsync(dirUri);
|
|
202
|
+
const names = uris.map((uri) => {
|
|
203
|
+
try {
|
|
204
|
+
const decoded = decodeURIComponent(uri);
|
|
205
|
+
const last = decoded.split('/').pop() || '';
|
|
206
|
+
return last.includes(':') ? last.split(':').pop() || '' : last;
|
|
207
|
+
} catch {
|
|
208
|
+
return '';
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
return new Set(names.filter(Boolean));
|
|
212
|
+
} catch {
|
|
213
|
+
return new Set<string>();
|
|
175
214
|
}
|
|
176
215
|
};
|
|
177
216
|
|
|
178
217
|
const saveFileToDownloads = async () => {
|
|
179
218
|
try {
|
|
180
|
-
const
|
|
181
|
-
activeFile.fileName,
|
|
182
|
-
activeFile.mimetype
|
|
183
|
-
);
|
|
184
|
-
const filePath = FileSystem.cacheDirectory + fileName;
|
|
219
|
+
const filePath = FileSystem.cacheDirectory + displayFileName;
|
|
185
220
|
const download = await FileSystem.downloadAsync(withFileToken(activeFile.fileURL), filePath);
|
|
186
221
|
|
|
187
222
|
if (download.status !== 200) {
|
|
@@ -205,7 +240,16 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
205
240
|
safDirUriRef.current = dirUri;
|
|
206
241
|
}
|
|
207
242
|
|
|
208
|
-
|
|
243
|
+
// Avoid silently overwriting a different attachment that happens
|
|
244
|
+
// to share the same display name (common now that we prefer the
|
|
245
|
+
// human-picked name over the unique server hash - bug #40) by
|
|
246
|
+
// suffixing " (1)", " (2)", ... on a collision.
|
|
247
|
+
const existingNames = await listSafDirectoryNames(saf, dirUri as string);
|
|
248
|
+
const uniqueName = await getUniqueFileName(
|
|
249
|
+
displayFileName,
|
|
250
|
+
async (candidate) => existingNames.has(candidate)
|
|
251
|
+
);
|
|
252
|
+
const baseName = uniqueName.replace(/\.[^/.]+$/, '');
|
|
209
253
|
const mime = activeFile.mimetype || 'application/octet-stream';
|
|
210
254
|
const base64 = await FileSystem.readAsStringAsync(download.uri, {
|
|
211
255
|
encoding: FileSystem.EncodingType.Base64,
|
|
@@ -232,7 +276,7 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
232
276
|
// user may cancel), so we don't show a "Save successful" toast here.
|
|
233
277
|
await Share.share({
|
|
234
278
|
url: download.uri,
|
|
235
|
-
title:
|
|
279
|
+
title: displayFileName,
|
|
236
280
|
});
|
|
237
281
|
} catch (err) {
|
|
238
282
|
console.error('Error saving file:', err);
|
|
@@ -279,7 +323,7 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
279
323
|
'https://as2.ftcdn.net/v2/jpg/02/51/95/53/1000_F_251955356_FAQH0U1y1TZw3ZcdPGybwUkH90a3VAhb.jpg',
|
|
280
324
|
}}
|
|
281
325
|
resizeMode="contain"
|
|
282
|
-
accessibilityLabel={
|
|
326
|
+
accessibilityLabel={displayFileName}
|
|
283
327
|
/>
|
|
284
328
|
);
|
|
285
329
|
case activeFile.mimetype.startsWith('video/'):
|
|
@@ -333,10 +377,7 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
333
377
|
}}
|
|
334
378
|
>
|
|
335
379
|
<Text style={{ fontSize: 16, fontWeight: '600' }}>
|
|
336
|
-
{
|
|
337
|
-
activeFile.originalName || activeFile.fileName,
|
|
338
|
-
activeFile.mimetype
|
|
339
|
-
)}
|
|
380
|
+
{displayFileName}
|
|
340
381
|
</Text>
|
|
341
382
|
<AudioMessage
|
|
342
383
|
src={withFileToken(activeFile.fileURL)}
|
|
@@ -351,10 +392,6 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
351
392
|
case activeFile.mimetype === 'application/pdf':
|
|
352
393
|
return <PdfViewer pdfUrl={withFileToken(activeFile.fileURL)} />;
|
|
353
394
|
default: {
|
|
354
|
-
const displayName = ensureFilenameHasExtension(
|
|
355
|
-
activeFile.fileName,
|
|
356
|
-
activeFile.mimetype
|
|
357
|
-
);
|
|
358
395
|
// Office docs (.docx / .xlsx / .pptx / .doc / .xls / .ppt /
|
|
359
396
|
// .txt / .csv / .rtf) → render inline via Google's gview embed
|
|
360
397
|
// — fixes the "blank preview" complaint for docs (bug #9).
|
|
@@ -362,7 +399,7 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
362
399
|
return (
|
|
363
400
|
<DocumentViewer
|
|
364
401
|
url={activeFile.fileURL}
|
|
365
|
-
fileName={
|
|
402
|
+
fileName={displayFileName}
|
|
366
403
|
/>
|
|
367
404
|
);
|
|
368
405
|
}
|
|
@@ -379,7 +416,7 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
379
416
|
}}
|
|
380
417
|
>
|
|
381
418
|
<Text style={{ fontSize: 16, fontWeight: '600' }}>
|
|
382
|
-
{
|
|
419
|
+
{displayFileName}
|
|
383
420
|
</Text>
|
|
384
421
|
<Text style={{ color: '#666' }}>
|
|
385
422
|
{activeFile.mimetype || 'unknown type'}
|
|
@@ -14,9 +14,13 @@ import {
|
|
|
14
14
|
isSecureFileUrl,
|
|
15
15
|
requestFileTokenRecovery,
|
|
16
16
|
} from '../../../helpers/secureFileUrl';
|
|
17
|
+
import { getDisplayFileName } from '../../../helpers/getDisplayFileName';
|
|
17
18
|
|
|
18
19
|
interface LastMessageFileProps
|
|
19
|
-
extends Pick<
|
|
20
|
+
extends Pick<
|
|
21
|
+
LastMessage,
|
|
22
|
+
'user' | 'originalName' | 'fileName' | 'mimetype' | 'locationPreview'
|
|
23
|
+
> {}
|
|
20
24
|
|
|
21
25
|
const fallbackImage =
|
|
22
26
|
'https://as2.ftcdn.net/v2/jpg/02/51/95/53/1000_F_251955356_FAQH0U1y1TZw3ZcdPGybwUkH90a3VAhb.jpg';
|
|
@@ -24,10 +28,18 @@ const fallbackImage =
|
|
|
24
28
|
const LastMessageFile: FC<LastMessageFileProps> = ({
|
|
25
29
|
user,
|
|
26
30
|
originalName,
|
|
31
|
+
fileName,
|
|
32
|
+
mimetype,
|
|
27
33
|
locationPreview,
|
|
28
34
|
}) => {
|
|
29
35
|
const [imgSrc, setImgSrc] = useState(locationPreview);
|
|
30
36
|
const fileToken = useFileToken();
|
|
37
|
+
// Bug #40: prefer the sender's original name over the stored hash
|
|
38
|
+
// name; only fall back to the generic "file" label when neither is
|
|
39
|
+
// present at all (avoids a synthetic "media_<timestamp>.bin" here).
|
|
40
|
+
const displayName = originalName || fileName
|
|
41
|
+
? getDisplayFileName({ originalName, fileName, mimetype })
|
|
42
|
+
: 'file';
|
|
31
43
|
|
|
32
44
|
return (
|
|
33
45
|
<LastRoomMessageContainer>
|
|
@@ -57,7 +69,7 @@ const LastMessageFile: FC<LastMessageFileProps> = ({
|
|
|
57
69
|
) : (
|
|
58
70
|
<FileIcon style={{ width: '20px', height: '20px' }} />
|
|
59
71
|
)}
|
|
60
|
-
<LastRoomMessageText>{
|
|
72
|
+
<LastRoomMessageText>{displayName}</LastRoomMessageText>
|
|
61
73
|
</View>
|
|
62
74
|
</LastRoomMessageContainer>
|
|
63
75
|
);
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ShadeWrapper,
|
|
12
12
|
} from './StyledRoomComponents';
|
|
13
13
|
import { View } from 'react-native';
|
|
14
|
+
import { getDisplayFileName } from '../../../helpers/getDisplayFileName';
|
|
14
15
|
|
|
15
16
|
const PhotoContainer = styled.View`
|
|
16
17
|
position: relative;
|
|
@@ -25,14 +26,25 @@ const PhotoContainer = styled.View`
|
|
|
25
26
|
`;
|
|
26
27
|
|
|
27
28
|
interface LastMessagePhotoProps
|
|
28
|
-
extends Pick<
|
|
29
|
+
extends Pick<
|
|
30
|
+
LastMessage,
|
|
31
|
+
'user' | 'originalName' | 'fileName' | 'mimetype' | 'locationPreview'
|
|
32
|
+
> {}
|
|
29
33
|
|
|
30
34
|
const LastMessagePhoto: FC<LastMessagePhotoProps> = ({
|
|
31
35
|
user,
|
|
32
36
|
locationPreview,
|
|
33
37
|
originalName,
|
|
38
|
+
fileName,
|
|
39
|
+
mimetype,
|
|
34
40
|
}) => {
|
|
35
41
|
const fileToken = useFileToken();
|
|
42
|
+
// Bug #40: prefer the sender's original name over the stored hash
|
|
43
|
+
// name; only fall back to the generic "file" label when neither is
|
|
44
|
+
// present at all (avoids a synthetic "media_<timestamp>.bin" here).
|
|
45
|
+
const displayName = originalName || fileName
|
|
46
|
+
? getDisplayFileName({ originalName, fileName, mimetype })
|
|
47
|
+
: 'file';
|
|
36
48
|
return (
|
|
37
49
|
<LastRoomMessageContainer>
|
|
38
50
|
<LastRoomMessageName>{user?.name || ''}:</LastRoomMessageName>
|
|
@@ -48,7 +60,7 @@ const LastMessagePhoto: FC<LastMessagePhotoProps> = ({
|
|
|
48
60
|
<LastMessageImg src={appendFileToken(locationPreview, fileToken)} />
|
|
49
61
|
</ShadeWrapper>
|
|
50
62
|
</PhotoContainer>
|
|
51
|
-
<LastRoomMessageText>{
|
|
63
|
+
<LastRoomMessageText>{displayName}</LastRoomMessageText>
|
|
52
64
|
</View>
|
|
53
65
|
</LastRoomMessageContainer>
|
|
54
66
|
);
|
|
@@ -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
|
|