@ethora/chat-component-rn 26.5.11 → 26.5.13
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 +6 -0
- package/README.md +1 -0
- package/lib/commonjs/assets/icons.js +7 -1
- package/lib/commonjs/assets/icons.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatRoom.js +58 -22
- package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MediaMessage.js +288 -5
- package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ReduxWrapper.js +24 -15
- package/lib/commonjs/components/MainComponents/ReduxWrapper.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/MessageInteractions.js +23 -3
- package/lib/commonjs/components/MessageBubble/MessageInteractions.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/DocumentViewer.js +5 -44
- package/lib/commonjs/components/Modals/FilePreviewModal/DocumentViewer.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +36 -3
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/GViewWebView.js +149 -0
- package/lib/commonjs/components/Modals/FilePreviewModal/GViewWebView.js.map +1 -0
- package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js +4 -17
- package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastAudioMessage.js +19 -33
- package/lib/commonjs/components/RoomComponents/styled/LastAudioMessage.js.map +1 -1
- package/lib/commonjs/components/styled/AudioMessage.js +643 -66
- package/lib/commonjs/components/styled/AudioMessage.js.map +1 -1
- package/lib/commonjs/components/styled/Button.js +9 -3
- package/lib/commonjs/components/styled/Button.js.map +1 -1
- package/lib/commonjs/components/styled/SendInput.js +239 -40
- package/lib/commonjs/components/styled/SendInput.js.map +1 -1
- package/lib/commonjs/components/styled/StyledInputComponents/MediaComponents.js +31 -18
- package/lib/commonjs/components/styled/StyledInputComponents/MediaComponents.js.map +1 -1
- package/lib/commonjs/components/styled/UnsupportedType.js +47 -5
- package/lib/commonjs/components/styled/UnsupportedType.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +68 -2
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/keyboardLayout.js +5 -3
- package/lib/commonjs/helpers/keyboardLayout.js.map +1 -1
- package/lib/commonjs/helpers/mimeToExtension.js +70 -3
- package/lib/commonjs/helpers/mimeToExtension.js.map +1 -1
- package/lib/commonjs/hooks/useEventHandlers.js +1 -1
- package/lib/commonjs/hooks/useEventHandlers.js.map +1 -1
- package/lib/commonjs/hooks/useSendMessage.js +6 -3
- package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
- package/lib/commonjs/networking/xmppClient.js +26 -1
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/roomStore/Middleware/unreadMidlleware.js +10 -10
- package/lib/commonjs/roomStore/Middleware/unreadMidlleware.js.map +1 -1
- package/lib/commonjs/roomStore/roomsSlice.js +65 -4
- package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
- package/lib/module/assets/icons.js +7 -1
- package/lib/module/assets/icons.js.map +1 -1
- package/lib/module/components/MainComponents/ChatRoom.js +59 -23
- package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/module/components/MainComponents/MediaMessage.js +290 -7
- package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/module/components/MainComponents/ReduxWrapper.js +24 -15
- package/lib/module/components/MainComponents/ReduxWrapper.js.map +1 -1
- package/lib/module/components/MessageBubble/MessageInteractions.js +24 -4
- package/lib/module/components/MessageBubble/MessageInteractions.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/DocumentViewer.js +5 -45
- package/lib/module/components/Modals/FilePreviewModal/DocumentViewer.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +36 -3
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/GViewWebView.js +144 -0
- package/lib/module/components/Modals/FilePreviewModal/GViewWebView.js.map +1 -0
- package/lib/module/components/Modals/FilePreviewModal/PdfView.js +3 -17
- package/lib/module/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastAudioMessage.js +20 -34
- package/lib/module/components/RoomComponents/styled/LastAudioMessage.js.map +1 -1
- package/lib/module/components/styled/AudioMessage.js +644 -67
- package/lib/module/components/styled/AudioMessage.js.map +1 -1
- package/lib/module/components/styled/Button.js +9 -3
- package/lib/module/components/styled/Button.js.map +1 -1
- package/lib/module/components/styled/SendInput.js +240 -42
- package/lib/module/components/styled/SendInput.js.map +1 -1
- package/lib/module/components/styled/StyledInputComponents/MediaComponents.js +31 -18
- package/lib/module/components/styled/StyledInputComponents/MediaComponents.js.map +1 -1
- package/lib/module/components/styled/UnsupportedType.js +47 -5
- package/lib/module/components/styled/UnsupportedType.js.map +1 -1
- package/lib/module/context/xmppProvider.js +70 -4
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/keyboardLayout.js +4 -2
- package/lib/module/helpers/keyboardLayout.js.map +1 -1
- package/lib/module/helpers/mimeToExtension.js +67 -3
- package/lib/module/helpers/mimeToExtension.js.map +1 -1
- package/lib/module/hooks/useEventHandlers.js +1 -1
- package/lib/module/hooks/useEventHandlers.js.map +1 -1
- package/lib/module/hooks/useSendMessage.js +6 -3
- package/lib/module/hooks/useSendMessage.js.map +1 -1
- package/lib/module/networking/xmppClient.js +26 -1
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/roomStore/Middleware/unreadMidlleware.js +10 -10
- package/lib/module/roomStore/Middleware/unreadMidlleware.js.map +1 -1
- package/lib/module/roomStore/roomsSlice.js +63 -3
- package/lib/module/roomStore/roomsSlice.js.map +1 -1
- 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/ReduxWrapper.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/MessageInteractions.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/DocumentViewer.d.ts +0 -7
- package/lib/typescript/components/Modals/FilePreviewModal/DocumentViewer.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/GViewWebView.d.ts +8 -0
- package/lib/typescript/components/Modals/FilePreviewModal/GViewWebView.d.ts.map +1 -0
- package/lib/typescript/components/Modals/FilePreviewModal/PdfView.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastAudioMessage.d.ts.map +1 -1
- package/lib/typescript/components/styled/AudioMessage.d.ts +8 -2
- package/lib/typescript/components/styled/AudioMessage.d.ts.map +1 -1
- package/lib/typescript/components/styled/Button.d.ts.map +1 -1
- package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
- package/lib/typescript/components/styled/StyledInputComponents/MediaComponents.d.ts +5 -1
- package/lib/typescript/components/styled/StyledInputComponents/MediaComponents.d.ts.map +1 -1
- package/lib/typescript/components/styled/UnsupportedType.d.ts +5 -0
- package/lib/typescript/components/styled/UnsupportedType.d.ts.map +1 -1
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/helpers/keyboardLayout.d.ts +3 -1
- package/lib/typescript/helpers/keyboardLayout.d.ts.map +1 -1
- package/lib/typescript/helpers/mimeToExtension.d.ts +31 -1
- package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -1
- package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/roomStore/Middleware/unreadMidlleware.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts +18 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
- package/lib/typescript/types/models/modal.model.d.ts +3 -0
- package/lib/typescript/types/models/modal.model.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +15 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/assets/icons.tsx +30 -25
- package/src/components/MainComponents/ChatRoom.tsx +61 -23
- package/src/components/MainComponents/MediaMessage.tsx +295 -5
- package/src/components/MainComponents/ReduxWrapper.tsx +23 -9
- package/src/components/MessageBubble/MessageInteractions.tsx +35 -3
- package/src/components/Modals/FilePreviewModal/DocumentViewer.tsx +3 -40
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +39 -3
- package/src/components/Modals/FilePreviewModal/GViewWebView.tsx +142 -0
- package/src/components/Modals/FilePreviewModal/PdfView.tsx +2 -18
- package/src/components/RoomComponents/styled/LastAudioMessage.tsx +20 -35
- package/src/components/styled/AudioMessage.tsx +724 -59
- package/src/components/styled/Button.tsx +20 -11
- package/src/components/styled/SendInput.tsx +255 -46
- package/src/components/styled/StyledInputComponents/MediaComponents.tsx +29 -18
- package/src/components/styled/UnsupportedType.tsx +60 -6
- package/src/context/xmppProvider.tsx +61 -3
- package/src/helpers/keyboardLayout.ts +5 -1
- package/src/helpers/mimeToExtension.ts +116 -2
- package/src/hooks/useEventHandlers.tsx +1 -1
- package/src/hooks/useSendMessage.tsx +12 -6
- package/src/networking/xmppClient.ts +24 -1
- package/src/roomStore/Middleware/unreadMidlleware.tsx +11 -8
- package/src/roomStore/roomsSlice.ts +66 -2
- package/src/types/models/modal.model.ts +3 -0
- package/src/types/types.ts +32 -0
|
@@ -37,13 +37,14 @@ import { ensureScopedChatCache } from '../helpers/ensureScopedChatCache';
|
|
|
37
37
|
import { getRooms as prefetchRoomsViaRest } from '../networking/api-requests/rooms.api';
|
|
38
38
|
import { allRoomPresences } from '../networking/xmpp/allRoomPresences.xmpp';
|
|
39
39
|
import { store } from '../roomStore';
|
|
40
|
-
import { logout, setStoreClient } from '../roomStore/chatSettingsSlice';
|
|
40
|
+
import { logout, setStoreClient, setConfig } from '../roomStore/chatSettingsSlice';
|
|
41
41
|
import {
|
|
42
42
|
setLogoutState,
|
|
43
43
|
setVisibleRoom,
|
|
44
44
|
clearVisibleRoom,
|
|
45
45
|
setLastViewedTimestamp,
|
|
46
46
|
deleteRoomMessage,
|
|
47
|
+
msgSortableMs,
|
|
47
48
|
} from '../roomStore/roomsSlice';
|
|
48
49
|
import { runHistoryPreloadScheduler } from '../helpers/historyPreloadScheduler';
|
|
49
50
|
import { asyncLocalStorage } from '../hooks/useLocalStorage';
|
|
@@ -343,6 +344,10 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
343
344
|
devPushLog('rn', `initBeforeLoad: user resolved (${resolved.xmppUsername})`);
|
|
344
345
|
applyResolvedUserToStore(resolved);
|
|
345
346
|
|
|
347
|
+
if (config) {
|
|
348
|
+
store.dispatch(setConfig(config));
|
|
349
|
+
}
|
|
350
|
+
|
|
346
351
|
// Kick off REST /chats/my; allRoomPresences below needs the
|
|
347
352
|
// room list to be in redux, otherwise it joins 0 MUCs and the
|
|
348
353
|
// user receives zero realtime messages until they manually tap
|
|
@@ -598,17 +603,20 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
598
603
|
// touch the chat store themselves. (Undefined = host unmounts on hide,
|
|
599
604
|
// nothing to do.)
|
|
600
605
|
// -----------------------------------------------------------
|
|
606
|
+
const chatWasVisibleRef = useRef(false);
|
|
601
607
|
useEffect(() => {
|
|
602
608
|
if (typeof isVisible !== 'boolean') {return;}
|
|
603
609
|
const state = store.getState();
|
|
604
610
|
const rooms = state.rooms?.rooms;
|
|
605
611
|
const activeRoomJID = state.rooms?.activeRoomJID || null;
|
|
612
|
+
const wasVisible = chatWasVisibleRef.current;
|
|
613
|
+
chatWasVisibleRef.current = isVisible;
|
|
606
614
|
if (isVisible) {
|
|
607
615
|
if (activeRoomJID) {
|
|
608
616
|
store.dispatch(setVisibleRoom({ roomJID: activeRoomJID }));
|
|
609
617
|
}
|
|
610
618
|
} else {
|
|
611
|
-
if (activeRoomJID) {
|
|
619
|
+
if (wasVisible && activeRoomJID) {
|
|
612
620
|
store.dispatch(
|
|
613
621
|
setLastViewedTimestamp({ chatJID: activeRoomJID, timestamp: Date.now() })
|
|
614
622
|
);
|
|
@@ -618,8 +626,9 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
618
626
|
deleteRoomMessage({ roomJID: activeRoomJID, messageId: 'delimiter-new' })
|
|
619
627
|
);
|
|
620
628
|
}
|
|
629
|
+
|
|
621
630
|
store.dispatch(clearVisibleRoom());
|
|
622
|
-
if (client?.flushLastViewedToPrivateStoreStanza) {
|
|
631
|
+
if (wasVisible && client?.flushLastViewedToPrivateStoreStanza) {
|
|
623
632
|
client
|
|
624
633
|
.flushLastViewedToPrivateStoreStanza(rooms, { visibleRoomJID: activeRoomJID })
|
|
625
634
|
.catch(() => {});
|
|
@@ -627,6 +636,55 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
627
636
|
}
|
|
628
637
|
}, [isVisible, client]);
|
|
629
638
|
|
|
639
|
+
useEffect(() => {
|
|
640
|
+
if (isVisible !== true) {return;}
|
|
641
|
+
let lastMessagesRef: any = null;
|
|
642
|
+
let lastStampedMs = 0;
|
|
643
|
+
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
644
|
+
|
|
645
|
+
const advance = () => {
|
|
646
|
+
const s = store.getState();
|
|
647
|
+
const jid = s.rooms?.activeRoomJID;
|
|
648
|
+
if (!jid) {return;}
|
|
649
|
+
const room = s.rooms?.rooms?.[jid];
|
|
650
|
+
if (!room) {return;}
|
|
651
|
+
if (room.messages === lastMessagesRef) {return;}
|
|
652
|
+
lastMessagesRef = room.messages;
|
|
653
|
+
|
|
654
|
+
let newest = 0;
|
|
655
|
+
const list = room.messages || [];
|
|
656
|
+
for (const m of list) {
|
|
657
|
+
if (!m || m.id === 'delimiter-new') {continue;}
|
|
658
|
+
const ms = msgSortableMs(m);
|
|
659
|
+
if (ms > newest) {newest = ms;}
|
|
660
|
+
}
|
|
661
|
+
if (newest <= lastStampedMs) {return;}
|
|
662
|
+
lastStampedMs = newest;
|
|
663
|
+
|
|
664
|
+
store.dispatch(
|
|
665
|
+
setLastViewedTimestamp({ chatJID: jid, timestamp: Date.now() })
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
if (flushTimer) {clearTimeout(flushTimer);}
|
|
669
|
+
if (client?.flushLastViewedToPrivateStoreStanza) {
|
|
670
|
+
flushTimer = setTimeout(() => {
|
|
671
|
+
client
|
|
672
|
+
.flushLastViewedToPrivateStoreStanza(store.getState().rooms?.rooms, {
|
|
673
|
+
visibleRoomJID: jid,
|
|
674
|
+
})
|
|
675
|
+
.catch(() => {});
|
|
676
|
+
}, 2000);
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
advance();
|
|
681
|
+
const unsub = store.subscribe(advance);
|
|
682
|
+
return () => {
|
|
683
|
+
unsub();
|
|
684
|
+
if (flushTimer) {clearTimeout(flushTimer);}
|
|
685
|
+
};
|
|
686
|
+
}, [isVisible, client]);
|
|
687
|
+
|
|
630
688
|
// -----------------------------------------------------------
|
|
631
689
|
// NetInfo → proactive reconnect on network restore. Without this the
|
|
632
690
|
// only reconnect trigger is xmpp.js noticing the socket is dead, which
|
|
@@ -2,8 +2,11 @@ type KeyboardLayoutArgs = {
|
|
|
2
2
|
platform: string;
|
|
3
3
|
configuredOffset?: number;
|
|
4
4
|
bottomInset?: number;
|
|
5
|
+
keyboardVisible?: boolean;
|
|
5
6
|
};
|
|
6
7
|
|
|
8
|
+
export const ANDROID_INPUT_DOCK_GAP = 12;
|
|
9
|
+
|
|
7
10
|
export const getKeyboardVerticalOffset = ({
|
|
8
11
|
platform,
|
|
9
12
|
configuredOffset = 0,
|
|
@@ -14,5 +17,6 @@ export const getKeyboardVerticalOffset = ({
|
|
|
14
17
|
export const getInputDockPaddingBottom = ({
|
|
15
18
|
platform,
|
|
16
19
|
bottomInset = 0,
|
|
20
|
+
keyboardVisible = false,
|
|
17
21
|
}: Omit<KeyboardLayoutArgs, 'configuredOffset'>): number =>
|
|
18
|
-
platform === 'ios' ? bottomInset :
|
|
22
|
+
platform === 'ios' ? bottomInset : ANDROID_INPUT_DOCK_GAP;
|
|
@@ -75,10 +75,21 @@ const AUDIO_EXTENSIONS = [
|
|
|
75
75
|
'.mp3', '.m4a', '.wav', '.aac', '.ogg', '.flac',
|
|
76
76
|
'.weba', '.webm', '.opus', '.amr', '.3gp', '.3gpp',
|
|
77
77
|
] as const;
|
|
78
|
+
const IOS_AUDIO_PLAYBACK_EXTENSIONS = [
|
|
79
|
+
'.mp3', '.m4a', '.mp4', '.wav', '.aac', '.caf',
|
|
80
|
+
'.aif', '.aiff', '.amr', '.3gp', '.3gpp',
|
|
81
|
+
] as const;
|
|
82
|
+
const IOS_AUDIO_UNSUPPORTED_EXTENSIONS = [
|
|
83
|
+
'.webm', '.weba', '.opus', '.ogg',
|
|
84
|
+
] as const;
|
|
78
85
|
const AUDIO_EXT_RE = new RegExp(
|
|
79
86
|
`(${AUDIO_EXTENSIONS.map((e) => e.replace('.', '\\.')).join('|')})$`,
|
|
80
87
|
'i'
|
|
81
88
|
);
|
|
89
|
+
const IOS_AUDIO_PLAYBACK_EXT_RE = new RegExp(
|
|
90
|
+
`(${IOS_AUDIO_PLAYBACK_EXTENSIONS.map((e) => e.replace('.', '\\.')).join('|')})$`,
|
|
91
|
+
'i'
|
|
92
|
+
);
|
|
82
93
|
// Filename / URL substrings that strongly indicate a voice / audio
|
|
83
94
|
// payload even without an explicit audio extension — e.g. a web-app
|
|
84
95
|
// voicemail uploaded as application/octet-stream named "voicemail-…".
|
|
@@ -95,16 +106,36 @@ const AUDIO_NAME_HINT_RE =
|
|
|
95
106
|
export function isLikelyAudio(
|
|
96
107
|
mime: string | undefined | null,
|
|
97
108
|
fileName?: string | null,
|
|
98
|
-
url?: string | null
|
|
109
|
+
url?: string | null,
|
|
110
|
+
opts?: {
|
|
111
|
+
duration?: number | string | null;
|
|
112
|
+
waveForm?: string | null;
|
|
113
|
+
originalName?: string | null;
|
|
114
|
+
}
|
|
99
115
|
): boolean {
|
|
100
116
|
const m = (mime || '').toLowerCase();
|
|
101
117
|
if (m.startsWith('audio/')) {return true;}
|
|
118
|
+
const durationNum =
|
|
119
|
+
typeof opts?.duration === 'string'
|
|
120
|
+
? Number(opts.duration)
|
|
121
|
+
: opts?.duration ?? 0;
|
|
122
|
+
if (Number.isFinite(durationNum) && durationNum > 0) {return true;}
|
|
123
|
+
if (typeof opts?.waveForm === 'string' && opts.waveForm.trim().length > 0) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
102
126
|
// For octet-stream (and any other non-audio mime) fall through to the
|
|
103
127
|
// filename / URL heuristics. We don't restrict to octet-stream — some
|
|
104
128
|
// backends send no mime at all, or send text/plain by accident.
|
|
105
129
|
const name = (fileName || '').toLowerCase();
|
|
130
|
+
const originalName = (opts?.originalName || '').toLowerCase();
|
|
106
131
|
const urlLast = filenameFromUrl(url).toLowerCase();
|
|
107
|
-
if (
|
|
132
|
+
if (
|
|
133
|
+
AUDIO_EXT_RE.test(name) ||
|
|
134
|
+
AUDIO_EXT_RE.test(originalName) ||
|
|
135
|
+
AUDIO_EXT_RE.test(urlLast)
|
|
136
|
+
) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
108
139
|
// Last resort: voice-message naming hint anywhere in the filename, the
|
|
109
140
|
// URL's last segment, OR the URL's full path. Voicemails are commonly
|
|
110
141
|
// hosted under `/voice/abc-123` or `/voicemail/abc-123` where the last
|
|
@@ -120,6 +151,7 @@ export function isLikelyAudio(
|
|
|
120
151
|
})();
|
|
121
152
|
if (
|
|
122
153
|
AUDIO_NAME_HINT_RE.test(name) ||
|
|
154
|
+
AUDIO_NAME_HINT_RE.test(originalName) ||
|
|
123
155
|
AUDIO_NAME_HINT_RE.test(urlLast) ||
|
|
124
156
|
AUDIO_NAME_HINT_RE.test(urlPath)
|
|
125
157
|
) {
|
|
@@ -156,6 +188,88 @@ export function filenameFromUrl(url: string | undefined | null): string {
|
|
|
156
188
|
}
|
|
157
189
|
}
|
|
158
190
|
|
|
191
|
+
const getLowerExtension = (value: string | undefined | null) => {
|
|
192
|
+
if (!value) {return '';}
|
|
193
|
+
const match = value.toLowerCase().match(VALID_EXT_RE);
|
|
194
|
+
return match ? match[0] : '';
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* AVFoundation on iOS is stricter than browsers: a remote URL served as
|
|
199
|
+
* `application/octet-stream` with an opaque `.bin` name may fail before
|
|
200
|
+
* probing the actual audio payload. When the payload already looks like
|
|
201
|
+
* audio, cache it locally with an iOS-playable extension before playback.
|
|
202
|
+
*/
|
|
203
|
+
export function shouldCacheAudioForIosPlayback(opts: {
|
|
204
|
+
src?: string | null;
|
|
205
|
+
mime?: string | null;
|
|
206
|
+
fileName?: string | null;
|
|
207
|
+
originalName?: string | null;
|
|
208
|
+
duration?: number | string | null;
|
|
209
|
+
waveForm?: string | null;
|
|
210
|
+
}): boolean {
|
|
211
|
+
const src = opts.src || '';
|
|
212
|
+
if (!/^https?:\/\//i.test(src)) {return false;}
|
|
213
|
+
|
|
214
|
+
const mime = (opts.mime || '').toLowerCase().split(';')[0]!.trim();
|
|
215
|
+
const urlName = filenameFromUrl(src);
|
|
216
|
+
const candidates = [urlName, opts.fileName || '', opts.originalName || ''];
|
|
217
|
+
const hasIosPlayableName = candidates.some((value) =>
|
|
218
|
+
IOS_AUDIO_PLAYBACK_EXT_RE.test(value)
|
|
219
|
+
);
|
|
220
|
+
if (hasIosPlayableName && mime.startsWith('audio/')) {return false;}
|
|
221
|
+
|
|
222
|
+
return isLikelyAudio(mime, opts.fileName, src, {
|
|
223
|
+
duration: opts.duration,
|
|
224
|
+
waveForm: opts.waveForm,
|
|
225
|
+
originalName: opts.originalName,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function getIosAudioPlaybackCacheExtension(opts: {
|
|
230
|
+
mime?: string | null;
|
|
231
|
+
fileName?: string | null;
|
|
232
|
+
originalName?: string | null;
|
|
233
|
+
url?: string | null;
|
|
234
|
+
}): string {
|
|
235
|
+
const candidates = [
|
|
236
|
+
opts.fileName,
|
|
237
|
+
opts.originalName,
|
|
238
|
+
filenameFromUrl(opts.url),
|
|
239
|
+
getExtensionForMime(opts.mime),
|
|
240
|
+
];
|
|
241
|
+
for (const candidate of candidates) {
|
|
242
|
+
const ext = getLowerExtension(candidate);
|
|
243
|
+
if (ext && IOS_AUDIO_PLAYBACK_EXT_RE.test(ext)) {
|
|
244
|
+
return ext === '.mp4' ? '.m4a' : ext;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return '.m4a';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function isUnsupportedAudioForIosPlayback(opts: {
|
|
251
|
+
mime?: string | null;
|
|
252
|
+
fileName?: string | null;
|
|
253
|
+
originalName?: string | null;
|
|
254
|
+
url?: string | null;
|
|
255
|
+
}): boolean {
|
|
256
|
+
const mime = (opts.mime || '').toLowerCase().split(';')[0]!.trim();
|
|
257
|
+
if (mime === 'audio/webm' || mime === 'audio/ogg') {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
const candidates = [
|
|
261
|
+
opts.fileName,
|
|
262
|
+
opts.originalName,
|
|
263
|
+
filenameFromUrl(opts.url),
|
|
264
|
+
];
|
|
265
|
+
return candidates.some((candidate) => {
|
|
266
|
+
const ext = getLowerExtension(candidate);
|
|
267
|
+
return IOS_AUDIO_UNSUPPORTED_EXTENSIONS.includes(
|
|
268
|
+
ext as (typeof IOS_AUDIO_UNSUPPORTED_EXTENSIONS)[number]
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
159
273
|
/**
|
|
160
274
|
* Ensures `fileName` ends with a valid extension. Strategy:
|
|
161
275
|
* 1. If the input already has an extension, return it unchanged.
|
|
@@ -57,7 +57,7 @@ export const useEventHandlers = (config?: IConfig): EventHandlersHook => {
|
|
|
57
57
|
if (config?.eventHandlers?.onMessageFailed) {
|
|
58
58
|
config.eventHandlers.onMessageFailed(event);
|
|
59
59
|
}
|
|
60
|
-
console.
|
|
60
|
+
console.warn('Message failed details:', {
|
|
61
61
|
message: event.message,
|
|
62
62
|
roomJID: event.roomJID,
|
|
63
63
|
messageType: event.messageType,
|
|
@@ -313,6 +313,11 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
313
313
|
`${user?.firstName || ''} ${user?.lastName || ''}`.trim() || selfId;
|
|
314
314
|
const fileSizeStr = data?.size != null ? String(data.size) : '';
|
|
315
315
|
|
|
316
|
+
const normalizedFileUri =
|
|
317
|
+
typeof data?.uri === 'string'
|
|
318
|
+
? data.uri.replace(/^assets-library:\/\//, 'file://')
|
|
319
|
+
: '';
|
|
320
|
+
|
|
316
321
|
if (!config?.disableSentLogic && !existingId) {
|
|
317
322
|
dispatch(
|
|
318
323
|
addRoomMessage({
|
|
@@ -335,8 +340,8 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
335
340
|
isSystemMessage: 'false',
|
|
336
341
|
isMediafile: 'true',
|
|
337
342
|
fileName: data?.name,
|
|
338
|
-
location:
|
|
339
|
-
locationPreview:
|
|
343
|
+
location: normalizedFileUri,
|
|
344
|
+
locationPreview: normalizedFileUri,
|
|
340
345
|
mimetype: type,
|
|
341
346
|
originalName: data?.name,
|
|
342
347
|
size: fileSizeStr,
|
|
@@ -408,10 +413,7 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
408
413
|
// legacy library items — those can't be read as files. Coerce
|
|
409
414
|
// to a file:// scheme when possible so the body is a real blob.
|
|
410
415
|
const fileBlob = {
|
|
411
|
-
uri:
|
|
412
|
-
typeof data?.uri === 'string'
|
|
413
|
-
? data.uri.replace(/^assets-library:\/\//, 'file://')
|
|
414
|
-
: data?.uri,
|
|
416
|
+
uri: normalizedFileUri || data?.uri,
|
|
415
417
|
type:
|
|
416
418
|
data?.type ||
|
|
417
419
|
data?.mimeType ||
|
|
@@ -492,6 +494,10 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
492
494
|
store.getState()?.chatSettingStore?.client,
|
|
493
495
|
].find((c: any) => c && c.status === 'online');
|
|
494
496
|
if (effectiveMediaClient) {
|
|
497
|
+
effectiveMediaClient.setActiveRoomJid?.(activeRoomJID);
|
|
498
|
+
await Promise.resolve(
|
|
499
|
+
effectiveMediaClient.prioritizeRoomPresence?.(activeRoomJID)
|
|
500
|
+
).catch(() => false);
|
|
495
501
|
effectiveMediaClient.sendMediaMessageStanza(
|
|
496
502
|
activeRoomJID,
|
|
497
503
|
messagePayload,
|
|
@@ -23,6 +23,8 @@ import { getRooms } from './xmpp/getRooms.xmpp';
|
|
|
23
23
|
import { handleStanza } from './xmpp/handleStanzas.xmpp';
|
|
24
24
|
import { pushLog as devPushLog } from '../utils/devLogger';
|
|
25
25
|
import { normalizeRoomJid } from '../helpers/normalizeRoomJid';
|
|
26
|
+
import { store } from '../roomStore';
|
|
27
|
+
import { applyPrivateStoreMarkers } from '../roomStore/roomsSlice';
|
|
26
28
|
import {
|
|
27
29
|
clearOutboundSends,
|
|
28
30
|
enqueueOutboundSend,
|
|
@@ -870,7 +872,28 @@ export class XmppClient {
|
|
|
870
872
|
getChatsPrivateStoreRequestStanza = async () => {
|
|
871
873
|
if (this.disableLastRead) {return null;}
|
|
872
874
|
try {
|
|
873
|
-
|
|
875
|
+
const markers = await getChatsPrivateStoreRequest(this.client);
|
|
876
|
+
// Hydrate the fetched server-side read markers into redux. Without
|
|
877
|
+
// this the markers were fetched on every init/reconnect and then
|
|
878
|
+
// silently dropped, so any room the user hadn't locally opened+left
|
|
879
|
+
// this session kept `lastViewedTimestamp: 0` and the unread
|
|
880
|
+
// middleware's `> 0` gate skipped it forever — `useUnread()` stayed
|
|
881
|
+
// at 0 with no badge. Done here on the single READ method so every
|
|
882
|
+
// caller (provider init, reconnect, ChatWrapper, push handlers, …)
|
|
883
|
+
// hydrates automatically and a future caller can't reintroduce the
|
|
884
|
+
// drop. The write helpers call the lower-level
|
|
885
|
+
// `getChatsPrivateStoreRequest` directly and intentionally skip this.
|
|
886
|
+
if (markers && typeof markers === 'object') {
|
|
887
|
+
const normalized: Record<string, number> = {};
|
|
888
|
+
for (const jid of Object.keys(markers as Record<string, unknown>)) {
|
|
889
|
+
const n = Number((markers as Record<string, unknown>)[jid]);
|
|
890
|
+
if (jid && Number.isFinite(n) && n > 0) {normalized[jid] = n;}
|
|
891
|
+
}
|
|
892
|
+
if (Object.keys(normalized).length > 0) {
|
|
893
|
+
store.dispatch(applyPrivateStoreMarkers(normalized));
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return markers;
|
|
874
897
|
} catch (error) {
|
|
875
898
|
console.log(error);
|
|
876
899
|
return null;
|
|
@@ -64,6 +64,10 @@ const TRIGGER_ACTIONS = new Set([
|
|
|
64
64
|
'roomMessages/setRoomMessages',
|
|
65
65
|
'roomMessages/editRoomMessage',
|
|
66
66
|
'roomMessages/setLastViewedTimestamp',
|
|
67
|
+
// Hydrating server-side read markers can move several rooms' baselines
|
|
68
|
+
// at once; recompute so the badges reflect the freshly-applied markers
|
|
69
|
+
// (with own-message filtering this middleware adds on top).
|
|
70
|
+
'roomMessages/applyPrivateStoreMarkers',
|
|
67
71
|
'roomMessages/setCurrentRoom',
|
|
68
72
|
'roomMessages/setVisibleRoom',
|
|
69
73
|
'roomMessages/clearVisibleRoom',
|
|
@@ -130,14 +134,13 @@ export const unreadMiddleware: Middleware =
|
|
|
130
134
|
}
|
|
131
135
|
if (!(room.lastViewedTimestamp > 0)) {return;}
|
|
132
136
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
const fingerprint = `${currentMessagesLength}|${room.lastViewedTimestamp || 0}`;
|
|
137
|
+
const msgs = room.messages;
|
|
138
|
+
const currentMessagesLength = msgs?.length || 0;
|
|
139
|
+
const firstId = currentMessagesLength ? String(msgs![0]?.id ?? '') : '';
|
|
140
|
+
const lastId = currentMessagesLength
|
|
141
|
+
? String(msgs![currentMessagesLength - 1]?.id ?? '')
|
|
142
|
+
: '';
|
|
143
|
+
const fingerprint = `${currentMessagesLength}|${room.lastViewedTimestamp || 0}|${firstId}|${lastId}`;
|
|
141
144
|
if (triggerCache[jid] === fingerprint) {return;}
|
|
142
145
|
triggerCache[jid] = fingerprint;
|
|
143
146
|
|
|
@@ -98,6 +98,12 @@ export interface RoomMessagesState {
|
|
|
98
98
|
loadingText?: string;
|
|
99
99
|
usersSet?: Record<string, any>;
|
|
100
100
|
pendingNotificationJid?: string | null;
|
|
101
|
+
// Server-side read markers fetched from the XMPP private store
|
|
102
|
+
// (`{ roomJID: lastViewedMs }`). Kept here so a room that loads AFTER
|
|
103
|
+
// the markers were fetched still inherits its baseline via `addRoom`
|
|
104
|
+
// — see `applyPrivateStoreMarkers`. Re-fetched every init/reconnect,
|
|
105
|
+
// so it is intentionally NOT persisted.
|
|
106
|
+
privateStoreMarkers: Record<string, number>;
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
const initialState: RoomMessagesState = {
|
|
@@ -112,6 +118,7 @@ const initialState: RoomMessagesState = {
|
|
|
112
118
|
text: '',
|
|
113
119
|
},
|
|
114
120
|
pendingNotificationJid: null,
|
|
121
|
+
privateStoreMarkers: {},
|
|
115
122
|
};
|
|
116
123
|
|
|
117
124
|
const isValidRoomJid = (jid: unknown): jid is string => {
|
|
@@ -160,11 +167,19 @@ const reducers = {
|
|
|
160
167
|
// the previous session, so cold-start showed no unread badge for
|
|
161
168
|
// messages received while the app was closed (bug #19/#20). A real
|
|
162
169
|
// (non-zero) explicit value still wins; otherwise keep the existing
|
|
163
|
-
// value; otherwise
|
|
170
|
+
// value; otherwise fall back to the server-side read marker fetched
|
|
171
|
+
// from the private store (so a room the user has NEVER opened this
|
|
172
|
+
// session still gets a real baseline — without it the unread
|
|
173
|
+
// middleware's `lastViewedTimestamp > 0` gate skipped the room
|
|
174
|
+
// forever and the badge never lit up); otherwise anchor to the
|
|
175
|
+
// newest message in the payload.
|
|
176
|
+
const serverMarker = state.privateStoreMarkers?.[roomData.jid] || 0;
|
|
164
177
|
if (roomData.lastViewedTimestamp != null && roomData.lastViewedTimestamp !== 0) {
|
|
165
178
|
lastViewed = roomData.lastViewedTimestamp;
|
|
166
|
-
} else if (existing?.lastViewedTimestamp != null) {
|
|
179
|
+
} else if (existing?.lastViewedTimestamp != null && existing.lastViewedTimestamp > 0) {
|
|
167
180
|
lastViewed = existing.lastViewedTimestamp;
|
|
181
|
+
} else if (serverMarker > 0) {
|
|
182
|
+
lastViewed = serverMarker;
|
|
168
183
|
} else {
|
|
169
184
|
const msgs = roomData.messages || [];
|
|
170
185
|
let newest = 0;
|
|
@@ -346,6 +361,7 @@ const reducers = {
|
|
|
346
361
|
deleteAllRooms(state: WritableDraft<RoomMessagesState>) {
|
|
347
362
|
state.rooms = {};
|
|
348
363
|
state.visibleRoomJID = null;
|
|
364
|
+
state.privateStoreMarkers = {};
|
|
349
365
|
},
|
|
350
366
|
setComposing(
|
|
351
367
|
state: WritableDraft<RoomMessagesState>,
|
|
@@ -392,6 +408,45 @@ const reducers = {
|
|
|
392
408
|
}
|
|
393
409
|
}
|
|
394
410
|
},
|
|
411
|
+
/**
|
|
412
|
+
* Apply server-side read markers fetched from the XMPP private store
|
|
413
|
+
* (`{ roomJID: lastViewedMs }`) to redux. This is the hydration that
|
|
414
|
+
* was missing: `getChatsPrivateStoreRequest` fetched the markers on
|
|
415
|
+
* every init/reconnect and every caller then DISCARDED the result, so
|
|
416
|
+
* rooms kept `lastViewedTimestamp: 0` and the unread middleware's
|
|
417
|
+
* `> 0` gate skipped them forever — `useUnread()` stayed at 0 for any
|
|
418
|
+
* room the user hadn't locally opened+left this session. Centralised
|
|
419
|
+
* here (and consulted by `addRoom`) so BOTH already-loaded rooms and
|
|
420
|
+
* rooms that load later pick up the baseline.
|
|
421
|
+
*
|
|
422
|
+
* Monotonic: a marker only ever moves a baseline FORWARD, so a stale
|
|
423
|
+
* server value can't resurrect already-read messages and a more-recent
|
|
424
|
+
* local read (tab blur stamping Date.now()) always wins.
|
|
425
|
+
*/
|
|
426
|
+
applyPrivateStoreMarkers: (
|
|
427
|
+
state: WritableDraft<RoomMessagesState>,
|
|
428
|
+
action: PayloadAction<Record<string, number>>
|
|
429
|
+
) => {
|
|
430
|
+
const markers = action.payload;
|
|
431
|
+
if (!markers || typeof markers !== 'object') {return;}
|
|
432
|
+
if (!state.privateStoreMarkers) {state.privateStoreMarkers = {};}
|
|
433
|
+
for (const jid of Object.keys(markers)) {
|
|
434
|
+
const ts = Number(markers[jid]);
|
|
435
|
+
if (!jid || !Number.isFinite(ts) || ts <= 0) {continue;}
|
|
436
|
+
// Remember the marker so rooms that load LATER (via addRoom)
|
|
437
|
+
// inherit it even though they don't exist in the store yet.
|
|
438
|
+
if (ts > (state.privateStoreMarkers[jid] || 0)) {
|
|
439
|
+
state.privateStoreMarkers[jid] = ts;
|
|
440
|
+
}
|
|
441
|
+
// Upgrade an already-loaded room's baseline + recompute its badge.
|
|
442
|
+
// Only ever forward, so a later local read isn't clobbered.
|
|
443
|
+
const room = state.rooms[jid];
|
|
444
|
+
if (room && ts > (room.lastViewedTimestamp || 0)) {
|
|
445
|
+
room.lastViewedTimestamp = ts;
|
|
446
|
+
room.unreadMessages = countNewerMessages(room.messages, ts);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
},
|
|
395
450
|
setRoomRole: (
|
|
396
451
|
state: WritableDraft<RoomMessagesState>,
|
|
397
452
|
action: PayloadAction<{ chatJID: string; role: string }>
|
|
@@ -429,7 +484,14 @@ const reducers = {
|
|
|
429
484
|
}
|
|
430
485
|
},
|
|
431
486
|
clearVisibleRoom: (state: WritableDraft<RoomMessagesState>) => {
|
|
487
|
+
const jid = state.visibleRoomJID;
|
|
432
488
|
state.visibleRoomJID = null;
|
|
489
|
+
|
|
490
|
+
if (jid && state.rooms[jid]) {
|
|
491
|
+
const room = state.rooms[jid];
|
|
492
|
+
const t = room.lastViewedTimestamp || 0;
|
|
493
|
+
room.unreadMessages = t > 0 ? countNewerMessages(room.messages, t) : 0;
|
|
494
|
+
}
|
|
433
495
|
},
|
|
434
496
|
/**
|
|
435
497
|
* Stash a JID that a push notification asked us to open before
|
|
@@ -476,6 +538,7 @@ const reducers = {
|
|
|
476
538
|
state.activeRoomJID = null;
|
|
477
539
|
state.visibleRoomJID = null;
|
|
478
540
|
state.isLoading = false;
|
|
541
|
+
state.privateStoreMarkers = {};
|
|
479
542
|
},
|
|
480
543
|
setActiveMessage: (
|
|
481
544
|
state: WritableDraft<RoomMessagesState>,
|
|
@@ -667,6 +730,7 @@ export const {
|
|
|
667
730
|
setComposing,
|
|
668
731
|
setIsLoading,
|
|
669
732
|
setLastViewedTimestamp,
|
|
733
|
+
applyPrivateStoreMarkers,
|
|
670
734
|
setRoomNoMessages,
|
|
671
735
|
setCurrentRoom,
|
|
672
736
|
setVisibleRoom,
|
package/src/types/types.ts
CHANGED
|
@@ -248,6 +248,16 @@ export interface IConfig {
|
|
|
248
248
|
// ----- header / nav -----
|
|
249
249
|
disableHeader?: boolean;
|
|
250
250
|
disableMedia?: boolean;
|
|
251
|
+
/**
|
|
252
|
+
* Enable in-chat voice-message recording. **Opt-in — off by default.**
|
|
253
|
+
* When `true`, an idle input (no text, no attachments) shows a
|
|
254
|
+
* microphone in the send-button slot; tap → start recording → stop &
|
|
255
|
+
* send. iOS apps need `NSMicrophoneUsageDescription` in Info.plist
|
|
256
|
+
* (add via `expo-av`'s plugin block in `app.json`). Receiving voice
|
|
257
|
+
* messages from other clients (incl. legacy web `.bin` voicemails) is
|
|
258
|
+
* independent of this flag — incoming audio plays regardless.
|
|
259
|
+
*/
|
|
260
|
+
enableAudio?: boolean;
|
|
251
261
|
chatHeaderBurgerMenu?: boolean;
|
|
252
262
|
// Hide the burger icon entirely (the icon that opens the chatHeader-
|
|
253
263
|
// BurgerMenu dropdown). `chatHeaderBurgerMenu` controls dropdown
|
|
@@ -362,6 +372,25 @@ export interface IConfig {
|
|
|
362
372
|
chatRoomStyles?: ViewStyle;
|
|
363
373
|
|
|
364
374
|
keyboardVerticalOffset?: number;
|
|
375
|
+
// Opt OUT of the built-in keyboard handling. By default <Chat> mounts
|
|
376
|
+
// its own react-native-keyboard-controller KeyboardProvider +
|
|
377
|
+
// KeyboardAvoidingView (behavior="padding"). If the HOST app already
|
|
378
|
+
// wraps <Chat> in its own KeyboardProvider / KeyboardAvoidingView, the
|
|
379
|
+
// built-in one becomes a SECOND avoider over the same tree — two of them
|
|
380
|
+
// animating padding on keyboard open is the Android flicker in bug #6.
|
|
381
|
+
// Set this so the component renders a plain View instead and drops its
|
|
382
|
+
// KeyboardProvider, leaving the host to own keyboard handling outright.
|
|
383
|
+
disableKeyboardAvoidingView?: boolean;
|
|
384
|
+
// Built-in keyboard strategy: instead of the default KeyboardAvoidingView
|
|
385
|
+
// (which adds bottom padding to the WHOLE chat tree — the message list
|
|
386
|
+
// reflows on keyboard open, the Android "messages jump/flash" in bug #6),
|
|
387
|
+
// wrap ONLY the input dock in a react-native-keyboard-controller
|
|
388
|
+
// KeyboardStickyView so just the input tracks the keyboard and the list
|
|
389
|
+
// is never resized. Best for edge-to-edge Android hosts (where the OS
|
|
390
|
+
// doesn't also resize the window). Ignored when
|
|
391
|
+
// `disableKeyboardAvoidingView` is set. Needs on-device tuning of any
|
|
392
|
+
// `keyboardVerticalOffset`.
|
|
393
|
+
keyboardStickyInput?: boolean;
|
|
365
394
|
|
|
366
395
|
// ----- interactions / messages -----
|
|
367
396
|
disableInteractions?: boolean;
|
|
@@ -517,6 +546,9 @@ export interface ModalFile {
|
|
|
517
546
|
fileName: string;
|
|
518
547
|
fileURL: string;
|
|
519
548
|
mimetype: string;
|
|
549
|
+
originalName?: string;
|
|
550
|
+
duration?: number | string;
|
|
551
|
+
waveForm?: string;
|
|
520
552
|
}
|
|
521
553
|
|
|
522
554
|
// Re-export so middleware/hooks that historically import from `types`
|