@ethora/chat-component-rn 26.5.10 → 26.5.12
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 +65 -1
- package/README.md +30 -1
- package/lib/commonjs/assets/icons.js +7 -1
- package/lib/commonjs/assets/icons.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js +31 -16
- package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatRoom.js +34 -4
- package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatWrapper.js +17 -14
- package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MediaMessage.js +298 -12
- package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/InteractionsOverlay.js +119 -0
- package/lib/commonjs/components/MessageBubble/InteractionsOverlay.js.map +1 -0
- package/lib/commonjs/components/MessageBubble/Message.js +39 -3
- package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/MessageInteractions.js +87 -39
- package/lib/commonjs/components/MessageBubble/MessageInteractions.js.map +1 -1
- package/lib/commonjs/components/Modals/AttachSheet/AttachSheet.js +153 -62
- package/lib/commonjs/components/Modals/AttachSheet/AttachSheet.js.map +1 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +20 -5
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.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 +40 -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/Modals/ModalHeaderComponent.js +10 -0
- package/lib/commonjs/components/Modals/ModalHeaderComponent.js.map +1 -1
- package/lib/commonjs/components/Modals/styledModalComponents.js +13 -4
- package/lib/commonjs/components/Modals/styledModalComponents.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/StyledComponents.js +6 -3
- package/lib/commonjs/components/styled/StyledComponents.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/components/styled/VideoMessage.js +44 -25
- package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +5 -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 +123 -0
- 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/useLogout.js +33 -8
- package/lib/commonjs/hooks/useLogout.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/api-requests/rooms.api.js +29 -4
- package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/commonjs/roomStore/roomsSlice.js +6 -0
- 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/ChatHeader.js +31 -16
- package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/module/components/MainComponents/ChatRoom.js +36 -6
- package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/module/components/MainComponents/ChatWrapper.js +17 -14
- package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/module/components/MainComponents/MediaMessage.js +301 -15
- package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/module/components/MessageBubble/InteractionsOverlay.js +112 -0
- package/lib/module/components/MessageBubble/InteractionsOverlay.js.map +1 -0
- package/lib/module/components/MessageBubble/Message.js +40 -4
- package/lib/module/components/MessageBubble/Message.js.map +1 -1
- package/lib/module/components/MessageBubble/MessageInteractions.js +89 -41
- package/lib/module/components/MessageBubble/MessageInteractions.js.map +1 -1
- package/lib/module/components/Modals/AttachSheet/AttachSheet.js +155 -64
- package/lib/module/components/Modals/AttachSheet/AttachSheet.js.map +1 -1
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +20 -5
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.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 +41 -4
- 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/Modals/ModalHeaderComponent.js +10 -0
- package/lib/module/components/Modals/ModalHeaderComponent.js.map +1 -1
- package/lib/module/components/Modals/styledModalComponents.js +13 -4
- package/lib/module/components/Modals/styledModalComponents.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/StyledComponents.js +6 -3
- package/lib/module/components/styled/StyledComponents.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/components/styled/VideoMessage.js +45 -26
- package/lib/module/components/styled/VideoMessage.js.map +1 -1
- package/lib/module/context/xmppProvider.js +5 -2
- 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 +119 -0
- 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/useLogout.js +33 -8
- package/lib/module/hooks/useLogout.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/api-requests/rooms.api.js +29 -4
- package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/module/roomStore/roomsSlice.js +6 -0
- package/lib/module/roomStore/roomsSlice.js.map +1 -1
- package/lib/typescript/assets/icons.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/InteractionsOverlay.d.ts +13 -0
- package/lib/typescript/components/MessageBubble/InteractionsOverlay.d.ts.map +1 -0
- package/lib/typescript/components/MessageBubble/Message.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/MessageInteractions.d.ts.map +1 -1
- package/lib/typescript/components/Modals/AttachSheet/AttachSheet.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.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/Modals/ModalHeaderComponent.d.ts.map +1 -1
- package/lib/typescript/components/Modals/styledModalComponents.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/StyledComponents.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/components/styled/VideoMessage.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 +38 -0
- package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -1
- package/lib/typescript/hooks/useLogout.d.ts +23 -1
- package/lib/typescript/hooks/useLogout.d.ts.map +1 -1
- package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/rooms.api.d.ts.map +1 -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 +29 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/package.json +11 -4
- package/src/assets/icons.tsx +30 -25
- package/src/components/MainComponents/ChatHeader.tsx +46 -15
- package/src/components/MainComponents/ChatRoom.tsx +38 -3
- package/src/components/MainComponents/ChatWrapper.tsx +28 -21
- package/src/components/MainComponents/MediaMessage.tsx +304 -11
- package/src/components/MessageBubble/InteractionsOverlay.tsx +121 -0
- package/src/components/MessageBubble/Message.tsx +44 -3
- package/src/components/MessageBubble/MessageInteractions.tsx +103 -52
- package/src/components/Modals/AttachSheet/AttachSheet.tsx +144 -51
- package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +25 -3
- package/src/components/Modals/FilePreviewModal/DocumentViewer.tsx +3 -40
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +48 -4
- package/src/components/Modals/FilePreviewModal/GViewWebView.tsx +142 -0
- package/src/components/Modals/FilePreviewModal/PdfView.tsx +2 -18
- package/src/components/Modals/ModalHeaderComponent.tsx +8 -1
- package/src/components/Modals/styledModalComponents.tsx +13 -4
- 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/StyledComponents.ts +6 -3
- package/src/components/styled/StyledInputComponents/MediaComponents.tsx +29 -18
- package/src/components/styled/UnsupportedType.tsx +60 -6
- package/src/components/styled/VideoMessage.tsx +21 -5
- package/src/context/xmppProvider.tsx +6 -2
- package/src/helpers/keyboardLayout.ts +5 -1
- package/src/helpers/mimeToExtension.ts +178 -0
- package/src/hooks/useEventHandlers.tsx +1 -1
- package/src/hooks/useLogout.tsx +33 -10
- package/src/hooks/useSendMessage.tsx +12 -6
- package/src/networking/api-requests/rooms.api.ts +31 -7
- package/src/roomStore/roomsSlice.ts +7 -0
- package/src/types/models/modal.model.ts +3 -0
- package/src/types/types.ts +29 -0
|
@@ -598,17 +598,20 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
598
598
|
// touch the chat store themselves. (Undefined = host unmounts on hide,
|
|
599
599
|
// nothing to do.)
|
|
600
600
|
// -----------------------------------------------------------
|
|
601
|
+
const chatWasVisibleRef = useRef(false);
|
|
601
602
|
useEffect(() => {
|
|
602
603
|
if (typeof isVisible !== 'boolean') {return;}
|
|
603
604
|
const state = store.getState();
|
|
604
605
|
const rooms = state.rooms?.rooms;
|
|
605
606
|
const activeRoomJID = state.rooms?.activeRoomJID || null;
|
|
607
|
+
const wasVisible = chatWasVisibleRef.current;
|
|
608
|
+
chatWasVisibleRef.current = isVisible;
|
|
606
609
|
if (isVisible) {
|
|
607
610
|
if (activeRoomJID) {
|
|
608
611
|
store.dispatch(setVisibleRoom({ roomJID: activeRoomJID }));
|
|
609
612
|
}
|
|
610
613
|
} else {
|
|
611
|
-
if (activeRoomJID) {
|
|
614
|
+
if (wasVisible && activeRoomJID) {
|
|
612
615
|
store.dispatch(
|
|
613
616
|
setLastViewedTimestamp({ chatJID: activeRoomJID, timestamp: Date.now() })
|
|
614
617
|
);
|
|
@@ -618,8 +621,9 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
618
621
|
deleteRoomMessage({ roomJID: activeRoomJID, messageId: 'delimiter-new' })
|
|
619
622
|
);
|
|
620
623
|
}
|
|
624
|
+
|
|
621
625
|
store.dispatch(clearVisibleRoom());
|
|
622
|
-
if (client?.flushLastViewedToPrivateStoreStanza) {
|
|
626
|
+
if (wasVisible && client?.flushLastViewedToPrivateStoreStanza) {
|
|
623
627
|
client
|
|
624
628
|
.flushLastViewedToPrivateStoreStanza(rooms, { visibleRoomJID: activeRoomJID })
|
|
625
629
|
.catch(() => {});
|
|
@@ -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;
|
|
@@ -64,6 +64,102 @@ const MIME_TO_EXT: Record<string, string> = {
|
|
|
64
64
|
|
|
65
65
|
const VALID_EXT_RE = /\.[a-z0-9]{1,8}$/i;
|
|
66
66
|
|
|
67
|
+
// Audio file extensions we recognise across the SDK. Used for the
|
|
68
|
+
// "looks like audio even when the mimetype is application/octet-stream"
|
|
69
|
+
// heuristic in MediaMessage / FilePreviewModal. Voice-message senders
|
|
70
|
+
// (web app, push voice memos) sometimes ship audio as octet-stream with
|
|
71
|
+
// no audio mime — sniffing the extension lets us still play it.
|
|
72
|
+
// `.bin` is intentionally NOT here: it's our own fallback extension for
|
|
73
|
+
// truly unknown payloads (see getExtensionForMime), not a real format.
|
|
74
|
+
const AUDIO_EXTENSIONS = [
|
|
75
|
+
'.mp3', '.m4a', '.wav', '.aac', '.ogg', '.flac',
|
|
76
|
+
'.weba', '.webm', '.opus', '.amr', '.3gp', '.3gpp',
|
|
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;
|
|
85
|
+
const AUDIO_EXT_RE = new RegExp(
|
|
86
|
+
`(${AUDIO_EXTENSIONS.map((e) => e.replace('.', '\\.')).join('|')})$`,
|
|
87
|
+
'i'
|
|
88
|
+
);
|
|
89
|
+
const IOS_AUDIO_PLAYBACK_EXT_RE = new RegExp(
|
|
90
|
+
`(${IOS_AUDIO_PLAYBACK_EXTENSIONS.map((e) => e.replace('.', '\\.')).join('|')})$`,
|
|
91
|
+
'i'
|
|
92
|
+
);
|
|
93
|
+
// Filename / URL substrings that strongly indicate a voice / audio
|
|
94
|
+
// payload even without an explicit audio extension — e.g. a web-app
|
|
95
|
+
// voicemail uploaded as application/octet-stream named "voicemail-…".
|
|
96
|
+
const AUDIO_NAME_HINT_RE =
|
|
97
|
+
/(?:^|[_\-./])(voice|voicemail|audio|audio-message|audiomessage|recording|voice-?note)(?:[_\-./]|$)/i;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Returns true when the (mime, fileName, url) triple looks like an
|
|
101
|
+
* audio payload — either by mime, by recognised extension on the
|
|
102
|
+
* filename or URL path, or by a voice-message naming hint. The hint
|
|
103
|
+
* branch is what catches voicemails sent as `application/octet-stream`
|
|
104
|
+
* with a generic `.bin` filename (customer-reported #9 voicemail).
|
|
105
|
+
*/
|
|
106
|
+
export function isLikelyAudio(
|
|
107
|
+
mime: string | undefined | null,
|
|
108
|
+
fileName?: string | null,
|
|
109
|
+
url?: string | null,
|
|
110
|
+
opts?: {
|
|
111
|
+
duration?: number | string | null;
|
|
112
|
+
waveForm?: string | null;
|
|
113
|
+
originalName?: string | null;
|
|
114
|
+
}
|
|
115
|
+
): boolean {
|
|
116
|
+
const m = (mime || '').toLowerCase();
|
|
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
|
+
}
|
|
126
|
+
// For octet-stream (and any other non-audio mime) fall through to the
|
|
127
|
+
// filename / URL heuristics. We don't restrict to octet-stream — some
|
|
128
|
+
// backends send no mime at all, or send text/plain by accident.
|
|
129
|
+
const name = (fileName || '').toLowerCase();
|
|
130
|
+
const originalName = (opts?.originalName || '').toLowerCase();
|
|
131
|
+
const urlLast = filenameFromUrl(url).toLowerCase();
|
|
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
|
+
}
|
|
139
|
+
// Last resort: voice-message naming hint anywhere in the filename, the
|
|
140
|
+
// URL's last segment, OR the URL's full path. Voicemails are commonly
|
|
141
|
+
// hosted under `/voice/abc-123` or `/voicemail/abc-123` where the last
|
|
142
|
+
// segment is an opaque id with no hint by itself.
|
|
143
|
+
const urlPath = (() => {
|
|
144
|
+
if (!url) {return '';}
|
|
145
|
+
try {
|
|
146
|
+
const noQuery = url.split('?')[0]!.split('#')[0]!;
|
|
147
|
+
return decodeURIComponent(noQuery).toLowerCase();
|
|
148
|
+
} catch {
|
|
149
|
+
return '';
|
|
150
|
+
}
|
|
151
|
+
})();
|
|
152
|
+
if (
|
|
153
|
+
AUDIO_NAME_HINT_RE.test(name) ||
|
|
154
|
+
AUDIO_NAME_HINT_RE.test(originalName) ||
|
|
155
|
+
AUDIO_NAME_HINT_RE.test(urlLast) ||
|
|
156
|
+
AUDIO_NAME_HINT_RE.test(urlPath)
|
|
157
|
+
) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
67
163
|
/**
|
|
68
164
|
* Returns the conventional extension (including the leading dot) for a
|
|
69
165
|
* MIME type, or `.bin` as a last-resort fallback so callers never end up
|
|
@@ -92,6 +188,88 @@ export function filenameFromUrl(url: string | undefined | null): string {
|
|
|
92
188
|
}
|
|
93
189
|
}
|
|
94
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
|
+
|
|
95
273
|
/**
|
|
96
274
|
* Ensures `fileName` ends with a valid extension. Strategy:
|
|
97
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,
|
package/src/hooks/useLogout.tsx
CHANGED
|
@@ -134,18 +134,41 @@ const logoutService = {
|
|
|
134
134
|
},
|
|
135
135
|
};
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Hook that returns an awaitable logout function.
|
|
139
|
+
*
|
|
140
|
+
* ```tsx
|
|
141
|
+
* const logout = useLogout();
|
|
142
|
+
* await logout();
|
|
143
|
+
* navigation.replace('SignIn'); // disk is provably clean here
|
|
144
|
+
* ```
|
|
145
|
+
*
|
|
146
|
+
* The returned function resolves AFTER all teardown work has landed:
|
|
147
|
+
* XMPP disconnect, redux reset, persisted slices wiped, stray
|
|
148
|
+
* AsyncStorage keys removed, REST cache cleared. It never rejects —
|
|
149
|
+
* any internal failure is logged via console.warn so a fire-and-forget
|
|
150
|
+
* `logout()` call (no `await`) still won't crash the host.
|
|
151
|
+
*
|
|
152
|
+
* Why awaitable: the persistence middleware debounces writes by 200ms,
|
|
153
|
+
* and the chat-slice `logout` reducer removes ETHORA_USER fire-and-
|
|
154
|
+
* forget. If the host navigated / re-mounted `<Chat>` immediately
|
|
155
|
+
* after a non-awaited call, the next bootstrap could occasionally
|
|
156
|
+
* read stale persisted state ("old chats reappear"). Awaiting the
|
|
157
|
+
* returned promise eliminates that race entirely.
|
|
158
|
+
*/
|
|
159
|
+
export const useLogout = (): (() => Promise<void>) => {
|
|
160
|
+
// dispatch is kept for backward-compat with consumers who treat the
|
|
161
|
+
// hook's presence as a redux-context requirement; not used directly
|
|
162
|
+
// here because the work happens through the module-level service.
|
|
163
|
+
useDispatch();
|
|
139
164
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
console.warn('useLogout: performLogout
|
|
145
|
-
}
|
|
165
|
+
return useCallback(async () => {
|
|
166
|
+
try {
|
|
167
|
+
await logoutService.performLogout();
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.warn('useLogout: performLogout failed', err);
|
|
170
|
+
}
|
|
146
171
|
}, []);
|
|
147
|
-
|
|
148
|
-
return handleLogout;
|
|
149
172
|
};
|
|
150
173
|
|
|
151
174
|
export { logoutService };
|
|
@@ -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,
|
|
@@ -21,21 +21,45 @@ interface ApiRoomMember {
|
|
|
21
21
|
* 1. explicit `room.jid` from the server (if present)
|
|
22
22
|
* 2. `<name>@conference.<xmppHost>` where xmppHost comes from the
|
|
23
23
|
* provider's `xmppSettings.host` saved to `chatSettingStore.config`
|
|
24
|
-
* 3.
|
|
24
|
+
* 3. derive host from `config.baseUrl` (api.<host> → xmpp.<host>)
|
|
25
|
+
* as a last-resort heuristic for misconfigured deployments
|
|
26
|
+
* 4. otherwise: skip the room with a warn log (NEVER fall back to a
|
|
27
|
+
* hardcoded vendor host — previously `xmpp.chat.ethora.com`, which
|
|
28
|
+
* manifested as a phantom "ethora" room stub on third-party servers,
|
|
29
|
+
* customer-reported #23)
|
|
25
30
|
*/
|
|
26
31
|
function dispatchRoomsFromRestItems(items: ApiRoom[]): void {
|
|
27
32
|
if (!items?.length) return;
|
|
28
33
|
const config = store.getState().chatSettingStore?.config as any;
|
|
29
|
-
|
|
34
|
+
// Best: explicit xmpp settings.
|
|
35
|
+
let host: string | undefined =
|
|
30
36
|
config?.xmppSettings?.host ||
|
|
31
|
-
config?.xmppSettings?.conference?.replace(/^conference\./, '')
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
config?.xmppSettings?.conference?.replace(/^conference\./, '');
|
|
38
|
+
// Heuristic fallback: derive xmpp host from the REST baseUrl.
|
|
39
|
+
// `https://api.foo.com/v1` → `xmpp.foo.com`. Better than a hardcoded
|
|
40
|
+
// vendor host and right in 99% of single-domain deployments.
|
|
41
|
+
if (!host && typeof config?.baseUrl === 'string') {
|
|
42
|
+
const m = /^https?:\/\/(?:api\.)?([^/]+)/i.exec(config.baseUrl);
|
|
43
|
+
if (m && m[1]) {host = `xmpp.${m[1].replace(/^api\./, '')}`;}
|
|
44
|
+
}
|
|
45
|
+
const conference = config?.xmppSettings?.conference
|
|
46
|
+
|| (host ? `conference.${host}` : undefined);
|
|
35
47
|
|
|
36
48
|
for (const item of items) {
|
|
37
49
|
if (!item) continue;
|
|
38
|
-
|
|
50
|
+
let jid = item.jid;
|
|
51
|
+
if (!jid) {
|
|
52
|
+
if (!conference) {
|
|
53
|
+
// No server-supplied JID and no way to synthesize one safely —
|
|
54
|
+
// skip rather than create a phantom room on a wrong host.
|
|
55
|
+
// Consumers who hit this should set config.xmppSettings.host.
|
|
56
|
+
console.warn(
|
|
57
|
+
`[ethora-rn] rooms.api: skipping room "${item.name}" — no item.jid and no xmppSettings.host / baseUrl to derive host from`
|
|
58
|
+
);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
jid = `${item.name}@${conference}`;
|
|
62
|
+
}
|
|
39
63
|
if (!jid.includes('@')) continue;
|
|
40
64
|
const room: IRoom = {
|
|
41
65
|
id: item._id || jid,
|
|
@@ -429,7 +429,14 @@ const reducers = {
|
|
|
429
429
|
}
|
|
430
430
|
},
|
|
431
431
|
clearVisibleRoom: (state: WritableDraft<RoomMessagesState>) => {
|
|
432
|
+
const jid = state.visibleRoomJID;
|
|
432
433
|
state.visibleRoomJID = null;
|
|
434
|
+
|
|
435
|
+
if (jid && state.rooms[jid]) {
|
|
436
|
+
const room = state.rooms[jid];
|
|
437
|
+
const t = room.lastViewedTimestamp || 0;
|
|
438
|
+
room.unreadMessages = t > 0 ? countNewerMessages(room.messages, t) : 0;
|
|
439
|
+
}
|
|
433
440
|
},
|
|
434
441
|
/**
|
|
435
442
|
* Stash a JID that a push notification asked us to open before
|
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
|
|
@@ -338,6 +348,22 @@ export interface IConfig {
|
|
|
338
348
|
disableMembers?: boolean;
|
|
339
349
|
hideMembers?: boolean;
|
|
340
350
|
disableChatHeaderMenu?: boolean;
|
|
351
|
+
/**
|
|
352
|
+
* Disables uploading / removing the chat icon from the chat-info
|
|
353
|
+
* modal. The avatar stays rendered (initials / current icon) but
|
|
354
|
+
* the press-to-pick / remove affordances are gone — so the room
|
|
355
|
+
* icon becomes read-only regardless of the user's role.
|
|
356
|
+
*/
|
|
357
|
+
disableIconEdit?: boolean;
|
|
358
|
+
/**
|
|
359
|
+
* Disables the tap on a member row in the chat-info member list —
|
|
360
|
+
* the popup that would show user-profile + per-member actions
|
|
361
|
+
* (message / copy-id) never opens. `disableMemberProfileActions`
|
|
362
|
+
* only hid the action BLOCK inside that popup; the popup itself
|
|
363
|
+
* still opened on tap. Set this when no per-member interaction is
|
|
364
|
+
* appropriate (e.g. patient-facing apps). Customer-reported #16.
|
|
365
|
+
*/
|
|
366
|
+
disableMemberTap?: boolean;
|
|
341
367
|
};
|
|
342
368
|
qrUrl?: string;
|
|
343
369
|
|
|
@@ -501,6 +527,9 @@ export interface ModalFile {
|
|
|
501
527
|
fileName: string;
|
|
502
528
|
fileURL: string;
|
|
503
529
|
mimetype: string;
|
|
530
|
+
originalName?: string;
|
|
531
|
+
duration?: number | string;
|
|
532
|
+
waveForm?: string;
|
|
504
533
|
}
|
|
505
534
|
|
|
506
535
|
// Re-export so middleware/hooks that historically import from `types`
|