@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
|
@@ -79,6 +79,8 @@ const Button: React.FC<ButtonProps> = ({
|
|
|
79
79
|
testID,
|
|
80
80
|
accessibilityLabel,
|
|
81
81
|
}) => {
|
|
82
|
+
const iconOnly = !loading && !children && !text && !StartIcon && !!EndIcon;
|
|
83
|
+
|
|
82
84
|
return (
|
|
83
85
|
<CustomButton
|
|
84
86
|
testID={testID}
|
|
@@ -91,17 +93,19 @@ const Button: React.FC<ButtonProps> = ({
|
|
|
91
93
|
>
|
|
92
94
|
{loading && <Loader size={24} />}
|
|
93
95
|
{!loading && StartIcon && <View style={styles.icon}>{StartIcon}</View>}
|
|
94
|
-
{!loading &&
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
{!loading && EndIcon &&
|
|
96
|
+
{!loading && children}
|
|
97
|
+
{!loading && !children && typeof text !== 'undefined' && (
|
|
98
|
+
<ButtonText
|
|
99
|
+
style={[{ color: color || 'black' }]}
|
|
100
|
+
variant={variant}
|
|
101
|
+
backgroundColor={backgroundColor}
|
|
102
|
+
>
|
|
103
|
+
{text}
|
|
104
|
+
</ButtonText>
|
|
105
|
+
)}
|
|
106
|
+
{!loading && EndIcon && (
|
|
107
|
+
<View style={iconOnly ? styles.iconOnly : styles.icon}>{EndIcon}</View>
|
|
108
|
+
)}
|
|
105
109
|
</CustomButton>
|
|
106
110
|
);
|
|
107
111
|
};
|
|
@@ -111,6 +115,11 @@ const styles = StyleSheet.create({
|
|
|
111
115
|
marginHorizontal: 4,
|
|
112
116
|
// maxWidth: '20%',
|
|
113
117
|
},
|
|
118
|
+
iconOnly: {
|
|
119
|
+
marginHorizontal: 0,
|
|
120
|
+
alignItems: 'center',
|
|
121
|
+
justifyContent: 'center',
|
|
122
|
+
},
|
|
114
123
|
});
|
|
115
124
|
|
|
116
125
|
export default Button;
|
|
@@ -8,13 +8,16 @@ import {
|
|
|
8
8
|
} from './StyledInputComponents/StyledInputComponents';
|
|
9
9
|
import { IConfig, MediaFile } from '../../types/types';
|
|
10
10
|
import Button from './Button';
|
|
11
|
-
import { SendIcon, AttachIcon } from '../../assets/icons';
|
|
12
|
-
import { KeyboardAvoidingView, View, TouchableOpacity, Alert, Linking } from 'react-native';
|
|
11
|
+
import { SendIcon, AttachIcon, RecordIcon } from '../../assets/icons';
|
|
12
|
+
import { KeyboardAvoidingView, Platform, View, Text, TouchableOpacity, Alert, Linking } from 'react-native';
|
|
13
13
|
import AttachSheet from '../Modals/AttachSheet/AttachSheet';
|
|
14
14
|
import { MediaFilePreview } from './MediaFilePreview';
|
|
15
15
|
import * as ImagePicker from 'expo-image-picker';
|
|
16
16
|
import * as DocumentPicker from 'expo-document-picker';
|
|
17
17
|
import * as ImageManipulator from 'expo-image-manipulator';
|
|
18
|
+
// `Audio` namespace gives us permissions, audio mode, presets, and the
|
|
19
|
+
// Recording class — same pattern AudioMessage uses for Audio.Sound.
|
|
20
|
+
import { Audio } from 'expo-av';
|
|
18
21
|
|
|
19
22
|
// iOS photos default to HEIC; many web backends (incl. ours) 500 on
|
|
20
23
|
// HEIC uploads because they can't decode it. Convert to JPEG before
|
|
@@ -85,10 +88,23 @@ const SendInput: React.FC<SendInputProps> = ({
|
|
|
85
88
|
}) => {
|
|
86
89
|
const [message, setMessage] = useState('');
|
|
87
90
|
const [isRecording, setIsRecording] = useState(false);
|
|
91
|
+
// Live counter (seconds) for the recording overlay. Updated by a
|
|
92
|
+
// 250ms interval started in `startRecording`.
|
|
93
|
+
const [recordingDuration, setRecordingDuration] = useState(0);
|
|
88
94
|
const [isFocused, setIsFocused] = useState(false);
|
|
89
95
|
const [filePreviews, setFilePreviews] = useState<MediaFile[]>([]);
|
|
90
96
|
const [showMediaMenu, setShowMediaMenu] = useState(false);
|
|
91
97
|
|
|
98
|
+
// expo-av's Audio.Recording instance for the in-progress recording.
|
|
99
|
+
// Kept in a ref (not state) because handlers fire synchronously and we
|
|
100
|
+
// need the latest instance without waiting for a re-render.
|
|
101
|
+
// Typed via InstanceType<typeof Audio.Recording> because direct
|
|
102
|
+
// `Audio.Recording` as a type-position lookup fails under bundler
|
|
103
|
+
// resolution even though the class is in the Audio namespace at
|
|
104
|
+
// runtime. InstanceType resolves through the typeof of the value.
|
|
105
|
+
const recordingRef = useRef<InstanceType<typeof Audio.Recording> | null>(null);
|
|
106
|
+
const recordingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
107
|
+
|
|
92
108
|
// Refs shadow `message` and `filePreviews` so handleSendClick can:
|
|
93
109
|
// (a) read the LATEST typed value even when React state hasn't
|
|
94
110
|
// flushed yet (rapid tap-burst race),
|
|
@@ -245,6 +261,148 @@ const SendInput: React.FC<SendInputProps> = ({
|
|
|
245
261
|
setShowMediaMenu(true);
|
|
246
262
|
};
|
|
247
263
|
|
|
264
|
+
// ─── Voice recording ──────────────────────────────────────────────
|
|
265
|
+
//
|
|
266
|
+
// Mirrors the web experience: tap mic → start recording → either tap
|
|
267
|
+
// X to discard, or tap send to stop + immediately upload + post the
|
|
268
|
+
// voice message. Uses expo-av's Audio.Recording (already a peer dep
|
|
269
|
+
// for AudioMessage playback). Filename is `voice-<timestamp>.m4a` with
|
|
270
|
+
// mimetype `audio/m4a` so the receiver routes through the audio
|
|
271
|
+
// branch in MediaMessage directly, no octet-stream sniffing needed.
|
|
272
|
+
|
|
273
|
+
const stopRecordingTimer = () => {
|
|
274
|
+
if (recordingTimerRef.current) {
|
|
275
|
+
clearInterval(recordingTimerRef.current);
|
|
276
|
+
recordingTimerRef.current = null;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// Always reset the iOS audio mode after teardown so AudioMessage
|
|
281
|
+
// playback elsewhere in the app isn't routed to the earpiece.
|
|
282
|
+
const restoreAudioMode = async () => {
|
|
283
|
+
try {
|
|
284
|
+
await Audio.setAudioModeAsync({
|
|
285
|
+
allowsRecordingIOS: false,
|
|
286
|
+
playsInSilentModeIOS: true,
|
|
287
|
+
});
|
|
288
|
+
} catch {
|
|
289
|
+
/* non-fatal */
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const handleStartRecording = async () => {
|
|
294
|
+
if (isRecording || recordingRef.current) {return;}
|
|
295
|
+
try {
|
|
296
|
+
const perm = await Audio.requestPermissionsAsync();
|
|
297
|
+
if (!perm.granted) {
|
|
298
|
+
promptOpenSettings(
|
|
299
|
+
'Microphone permission is needed to record voice messages.'
|
|
300
|
+
);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// iOS: recording requires `allowsRecordingIOS:true`; we'll
|
|
304
|
+
// flip it back in restoreAudioMode after stop/cancel.
|
|
305
|
+
await Audio.setAudioModeAsync({
|
|
306
|
+
allowsRecordingIOS: true,
|
|
307
|
+
playsInSilentModeIOS: true,
|
|
308
|
+
});
|
|
309
|
+
const rec = new Audio.Recording();
|
|
310
|
+
await rec.prepareToRecordAsync(
|
|
311
|
+
Audio.RecordingOptionsPresets.HIGH_QUALITY
|
|
312
|
+
);
|
|
313
|
+
await rec.startAsync();
|
|
314
|
+
recordingRef.current = rec;
|
|
315
|
+
setIsRecording(true);
|
|
316
|
+
setRecordingDuration(0);
|
|
317
|
+
const startedAt = Date.now();
|
|
318
|
+
// 250ms tick is fine for a seconds counter — keeps re-renders cheap.
|
|
319
|
+
recordingTimerRef.current = setInterval(() => {
|
|
320
|
+
setRecordingDuration(Math.floor((Date.now() - startedAt) / 1000));
|
|
321
|
+
}, 250);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
console.warn('startRecording failed', err);
|
|
324
|
+
recordingRef.current = null;
|
|
325
|
+
setIsRecording(false);
|
|
326
|
+
setRecordingDuration(0);
|
|
327
|
+
await restoreAudioMode();
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const handleCancelRecording = async () => {
|
|
332
|
+
stopRecordingTimer();
|
|
333
|
+
const rec = recordingRef.current;
|
|
334
|
+
recordingRef.current = null;
|
|
335
|
+
setIsRecording(false);
|
|
336
|
+
setRecordingDuration(0);
|
|
337
|
+
if (rec) {
|
|
338
|
+
try {
|
|
339
|
+
await rec.stopAndUnloadAsync();
|
|
340
|
+
} catch {
|
|
341
|
+
/* already stopped / unloaded — fine */
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
await restoreAudioMode();
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const handleStopAndSendRecording = async () => {
|
|
348
|
+
stopRecordingTimer();
|
|
349
|
+
const rec = recordingRef.current;
|
|
350
|
+
recordingRef.current = null;
|
|
351
|
+
setIsRecording(false);
|
|
352
|
+
const seconds = recordingDuration;
|
|
353
|
+
setRecordingDuration(0);
|
|
354
|
+
if (!rec) {
|
|
355
|
+
await restoreAudioMode();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
// Guard against zero-length taps — if the user tapped mic + send
|
|
359
|
+
// within the same tick there's nothing audible to send.
|
|
360
|
+
if (seconds < 1) {
|
|
361
|
+
try {
|
|
362
|
+
await rec.stopAndUnloadAsync();
|
|
363
|
+
} catch {}
|
|
364
|
+
await restoreAudioMode();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
await rec.stopAndUnloadAsync();
|
|
369
|
+
} catch (err) {
|
|
370
|
+
console.warn('stopAndUnloadAsync failed', err);
|
|
371
|
+
}
|
|
372
|
+
await restoreAudioMode();
|
|
373
|
+
const uri = rec.getURI();
|
|
374
|
+
if (!uri) {return;}
|
|
375
|
+
// HIGH_QUALITY preset writes AAC in an M4A container on both iOS
|
|
376
|
+
// and Android, so a single mimetype works. `voice-<ts>.m4a`
|
|
377
|
+
// matches the receiver's audio branch in MediaMessage (and
|
|
378
|
+
// isLikelyAudio for any backend that strips mimetype).
|
|
379
|
+
const ts = Date.now();
|
|
380
|
+
const file: MediaFile = {
|
|
381
|
+
uri,
|
|
382
|
+
type: 'audio/m4a',
|
|
383
|
+
name: `voice-${ts}.m4a`,
|
|
384
|
+
};
|
|
385
|
+
try {
|
|
386
|
+
await sendMedia(file, file.type);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
console.error('sendMedia voice failed', err);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// Tear down a recording in progress when the input unmounts (chat
|
|
393
|
+
// close, navigation, etc.) so the mic isn't left hot.
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
return () => {
|
|
396
|
+
stopRecordingTimer();
|
|
397
|
+
const rec = recordingRef.current;
|
|
398
|
+
recordingRef.current = null;
|
|
399
|
+
if (rec) {
|
|
400
|
+
rec.stopAndUnloadAsync().catch(() => {});
|
|
401
|
+
}
|
|
402
|
+
restoreAudioMode();
|
|
403
|
+
};
|
|
404
|
+
}, []);
|
|
405
|
+
|
|
248
406
|
const handleRemoveImage = (index: number) => {
|
|
249
407
|
const next = filePreviewsRef.current.filter((_, i) => i !== index);
|
|
250
408
|
filePreviewsRef.current = next;
|
|
@@ -332,14 +490,6 @@ const SendInput: React.FC<SendInputProps> = ({
|
|
|
332
490
|
</TouchableOpacity>
|
|
333
491
|
)}
|
|
334
492
|
<MessageInput
|
|
335
|
-
// Stable testIDs so Maestro / Detox / Appium can drive
|
|
336
|
-
// the chat-send flow reliably. accessibilityLabel
|
|
337
|
-
// mirrors the testID so iOS accessibility-id lookups
|
|
338
|
-
// also work. (TextInput in src/components/InputComponents
|
|
339
|
-
// also carries the same testIDs as a fallback, but
|
|
340
|
-
// SendInput is the input that actually ships in
|
|
341
|
-
// <ChatRoom> / <Thread> so this is the one e2e drivers
|
|
342
|
-
// hit in practice.)
|
|
343
493
|
testID="chat-message-input"
|
|
344
494
|
accessibilityLabel="chat-message-input"
|
|
345
495
|
isFocused={isFocused}
|
|
@@ -348,9 +498,6 @@ const SendInput: React.FC<SendInputProps> = ({
|
|
|
348
498
|
placeholderTextColor="#999"
|
|
349
499
|
value={message}
|
|
350
500
|
onChangeText={(text) => {
|
|
351
|
-
// Mirror into the ref synchronously so handleSendClick
|
|
352
|
-
// reads the latest value even before the React state
|
|
353
|
-
// commit lands (the source of the spam-merge bug).
|
|
354
501
|
messageRef.current = text;
|
|
355
502
|
setMessage(text);
|
|
356
503
|
}}
|
|
@@ -378,41 +525,103 @@ const SendInput: React.FC<SendInputProps> = ({
|
|
|
378
525
|
/>
|
|
379
526
|
</>
|
|
380
527
|
)}
|
|
381
|
-
{
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
528
|
+
{isRecording && (
|
|
529
|
+
<>
|
|
530
|
+
<TouchableOpacity
|
|
531
|
+
testID="chat-record-cancel"
|
|
532
|
+
accessibilityLabel="chat-record-cancel"
|
|
533
|
+
onPress={handleCancelRecording}
|
|
534
|
+
style={{
|
|
535
|
+
width: 40,
|
|
536
|
+
height: 40,
|
|
537
|
+
borderRadius: 20,
|
|
538
|
+
alignItems: 'center',
|
|
539
|
+
justifyContent: 'center',
|
|
540
|
+
backgroundColor: '#FEE2E2',
|
|
541
|
+
}}
|
|
542
|
+
activeOpacity={0.7}
|
|
543
|
+
>
|
|
544
|
+
<Text style={{ color: '#B91C1C', fontWeight: '700', fontSize: 18 }}>×</Text>
|
|
545
|
+
</TouchableOpacity>
|
|
546
|
+
{/* Live elapsed time + red recording dot. flex:1 so it
|
|
547
|
+
* occupies the middle slot the input used to have. */}
|
|
548
|
+
<View
|
|
549
|
+
style={{
|
|
550
|
+
flex: 1,
|
|
551
|
+
flexDirection: 'row',
|
|
552
|
+
alignItems: 'center',
|
|
553
|
+
gap: 8,
|
|
554
|
+
paddingHorizontal: 8,
|
|
555
|
+
minHeight: 40,
|
|
556
|
+
}}
|
|
557
|
+
>
|
|
558
|
+
<View
|
|
559
|
+
style={{
|
|
560
|
+
width: 10,
|
|
561
|
+
height: 10,
|
|
562
|
+
borderRadius: 5,
|
|
563
|
+
backgroundColor: '#EF4444',
|
|
564
|
+
}}
|
|
565
|
+
/>
|
|
566
|
+
<Text style={{ fontSize: 16, color: '#1F2937', fontWeight: '500' }}>
|
|
567
|
+
{Math.floor(recordingDuration / 60)}:
|
|
568
|
+
{(recordingDuration % 60).toString().padStart(2, '0')}
|
|
569
|
+
</Text>
|
|
570
|
+
<Text style={{ fontSize: 13, color: '#6B7280' }}>Recording…</Text>
|
|
571
|
+
</View>
|
|
572
|
+
</>
|
|
573
|
+
)}
|
|
574
|
+
{/* Right-side action button — swaps based on context:
|
|
575
|
+
* • while recording: stop-and-send (red dot in overlay
|
|
576
|
+
* is the live indicator; this is the same primary
|
|
577
|
+
* button so a single tap stops + ships).
|
|
578
|
+
* • idle + nothing typed + voice enabled: MIC (tap →
|
|
579
|
+
* start recording).
|
|
580
|
+
* • idle + has text / attachments: SEND.
|
|
581
|
+
* • idle + nothing typed + voice disabled: SEND in its
|
|
582
|
+
* muted/disabled state (so the slot reserved for
|
|
583
|
+
* consistency).
|
|
584
|
+
* Matches the web UX where one button on the right swaps
|
|
585
|
+
* between mic and send based on whether you have content. */}
|
|
586
|
+
{(() => {
|
|
587
|
+
const hasContent = !!message || filePreviews.length > 0;
|
|
588
|
+
const showMic = !isRecording && !hasContent && !!config?.enableAudio;
|
|
589
|
+
const onPress = isRecording
|
|
590
|
+
? handleStopAndSendRecording
|
|
591
|
+
: showMic
|
|
592
|
+
? handleStartRecording
|
|
593
|
+
: handleSendClick;
|
|
594
|
+
const disabled = !isRecording && !showMic && !hasContent;
|
|
595
|
+
const filled = isRecording || hasContent || showMic;
|
|
596
|
+
return (
|
|
597
|
+
<Button
|
|
598
|
+
testID={showMic ? 'chat-record-button' : 'chat-send-button'}
|
|
599
|
+
accessibilityLabel={showMic ? 'chat-record-button' : 'chat-send-button'}
|
|
600
|
+
onPress={onPress}
|
|
601
|
+
disabled={disabled}
|
|
602
|
+
EndIcon={
|
|
603
|
+
showMic ? (
|
|
604
|
+
<RecordIcon color="#FFFFFF" />
|
|
605
|
+
) : (
|
|
606
|
+
<SendIcon color={filled ? '#FFFFFF' : '#D4D4D8'} />
|
|
607
|
+
)
|
|
391
608
|
}
|
|
609
|
+
style={{
|
|
610
|
+
width: 40,
|
|
611
|
+
height: 40,
|
|
612
|
+
borderRadius: 50,
|
|
613
|
+
alignItems: 'center',
|
|
614
|
+
justifyContent: 'center',
|
|
615
|
+
marginLeft: 0,
|
|
616
|
+
marginRight: 0,
|
|
617
|
+
backgroundColor: filled
|
|
618
|
+
? config?.colors?.primary
|
|
619
|
+
: 'transparent',
|
|
620
|
+
opacity: filled ? 1 : 0.5,
|
|
621
|
+
}}
|
|
392
622
|
/>
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
width: 40,
|
|
396
|
-
height: 40,
|
|
397
|
-
borderRadius: 20,
|
|
398
|
-
alignItems: 'center',
|
|
399
|
-
justifyContent: 'center',
|
|
400
|
-
// The shared Button (CustomButton) bakes in `margin: 0 10px`,
|
|
401
|
-
// which pushed the send button 10px further from the input
|
|
402
|
-
// and from the right edge than the attach button (a raw
|
|
403
|
-
// TouchableOpacity with no margin). Zero it so both sides of
|
|
404
|
-
// the input use the row's uniform gap:8 + 16px container
|
|
405
|
-
// padding. Longhand left/right to override the styled
|
|
406
|
-
// longhand (marginHorizontal loses to marginLeft/Right in RN).
|
|
407
|
-
marginLeft: 0,
|
|
408
|
-
marginRight: 0,
|
|
409
|
-
backgroundColor:
|
|
410
|
-
message || filePreviews.length > 0
|
|
411
|
-
? config?.colors?.primary
|
|
412
|
-
: 'transparent',
|
|
413
|
-
opacity: message || filePreviews.length > 0 ? 1 : 0.5,
|
|
414
|
-
}}
|
|
415
|
-
/>
|
|
623
|
+
);
|
|
624
|
+
})()}
|
|
416
625
|
</MessageInputContainer>
|
|
417
626
|
<AttachSheet
|
|
418
627
|
visible={showMediaMenu}
|
|
@@ -49,52 +49,63 @@ export const IconButton = styled.TouchableOpacity`
|
|
|
49
49
|
pointer-events: auto;
|
|
50
50
|
`;
|
|
51
51
|
|
|
52
|
-
export const UnsupportedContainer = styled.TouchableOpacity
|
|
52
|
+
export const UnsupportedContainer = styled.TouchableOpacity<{ isUser?: boolean }>`
|
|
53
53
|
display: flex;
|
|
54
54
|
flex-direction: row;
|
|
55
55
|
align-items: center;
|
|
56
|
-
justify-content:
|
|
57
|
-
border-radius:
|
|
58
|
-
padding:
|
|
59
|
-
margin:
|
|
56
|
+
justify-content: flex-start;
|
|
57
|
+
border-radius: 12px;
|
|
58
|
+
padding: 8px;
|
|
59
|
+
margin: 8px;
|
|
60
60
|
cursor: pointer;
|
|
61
61
|
gap: 8px;
|
|
62
|
+
background-color: ${({ isUser }) => (isUser ? '#EDF5FF' : '#F7F9FC')};
|
|
63
|
+
border-width: 1px;
|
|
64
|
+
border-color: ${({ isUser }) => (isUser ? '#D7E6FF' : '#E7ECF3')};
|
|
65
|
+
min-width: 196px;
|
|
66
|
+
max-width: 260px;
|
|
62
67
|
`;
|
|
63
68
|
|
|
64
69
|
export const BackgroundFile = styled.View`
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
width: 54px;
|
|
71
|
+
height: 54px;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
background-color: #F3EAFB;
|
|
75
|
+
border-radius: 10px;
|
|
67
76
|
`;
|
|
68
77
|
|
|
69
78
|
export const FileInformation = styled.View`
|
|
79
|
+
flex: 1;
|
|
70
80
|
align-items: flex-start;
|
|
71
81
|
justify-content: space-between;
|
|
72
|
-
gap:
|
|
82
|
+
gap: 6px;
|
|
73
83
|
`;
|
|
74
84
|
|
|
75
85
|
export const FileName = styled.Text<{
|
|
76
86
|
isUser: boolean;
|
|
77
|
-
colorIsUser: string| undefined;
|
|
87
|
+
colorIsUser: string | undefined;
|
|
78
88
|
colorUsers: string | undefined;
|
|
79
89
|
}>`
|
|
80
90
|
font-size: 14px;
|
|
81
|
-
font-weight:
|
|
82
|
-
flex-
|
|
83
|
-
color: ${({isUser, colorIsUser
|
|
84
|
-
|
|
91
|
+
font-weight: 600;
|
|
92
|
+
flex-shrink: 1;
|
|
93
|
+
color: ${({ isUser, colorIsUser }) =>
|
|
94
|
+
isUser ? colorIsUser || '#1D4ED8' : '#1F2937'};
|
|
85
95
|
`;
|
|
86
96
|
|
|
87
97
|
export const FileSizeContainer = styled.View`
|
|
88
98
|
align-items: flex-start;
|
|
89
99
|
flex-direction: row;
|
|
90
|
-
background-color: #
|
|
91
|
-
padding:
|
|
92
|
-
border-radius:
|
|
100
|
+
background-color: #FFFFFF;
|
|
101
|
+
padding: 3px 8px;
|
|
102
|
+
border-radius: 999px;
|
|
93
103
|
`;
|
|
94
104
|
|
|
95
105
|
export const FileSize = styled.Text`
|
|
96
|
-
color: #
|
|
106
|
+
color: #667085;
|
|
97
107
|
overflow: hidden;
|
|
98
108
|
text-align: left;
|
|
99
|
-
font-
|
|
109
|
+
font-size: 12px;
|
|
110
|
+
font-weight: 600;
|
|
100
111
|
`;
|
|
@@ -14,9 +14,10 @@ import {
|
|
|
14
14
|
setActiveFile,
|
|
15
15
|
setActiveModal,
|
|
16
16
|
} from '../../roomStore/chatSettingsSlice';
|
|
17
|
-
import {
|
|
17
|
+
import { ActivityIndicator, View } from 'react-native';
|
|
18
18
|
import { useChatSettingState } from '../../hooks/useChatSettingState';
|
|
19
19
|
import { FileIcon } from '../../assets/icons';
|
|
20
|
+
import { getExtensionForMime } from '../../helpers/mimeToExtension';
|
|
20
21
|
|
|
21
22
|
interface FileDownloadProps {
|
|
22
23
|
fileName: string;
|
|
@@ -24,6 +25,11 @@ interface FileDownloadProps {
|
|
|
24
25
|
mimetype: string;
|
|
25
26
|
size?: string;
|
|
26
27
|
isUser: boolean;
|
|
28
|
+
originalName?: string;
|
|
29
|
+
duration?: number | string;
|
|
30
|
+
waveForm?: string;
|
|
31
|
+
pending?: boolean;
|
|
32
|
+
placeholderIcon?: React.ReactNode;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
const FileDownload: React.FC<FileDownloadProps> = ({
|
|
@@ -32,6 +38,11 @@ const FileDownload: React.FC<FileDownloadProps> = ({
|
|
|
32
38
|
mimetype,
|
|
33
39
|
size,
|
|
34
40
|
isUser,
|
|
41
|
+
originalName,
|
|
42
|
+
duration,
|
|
43
|
+
waveForm,
|
|
44
|
+
pending = false,
|
|
45
|
+
placeholderIcon,
|
|
35
46
|
}) => {
|
|
36
47
|
const dispatch = useDispatch();
|
|
37
48
|
const { config } = useChatSettingState();
|
|
@@ -73,24 +84,67 @@ const FileDownload: React.FC<FileDownloadProps> = ({
|
|
|
73
84
|
};
|
|
74
85
|
|
|
75
86
|
const handleOpen = () => {
|
|
76
|
-
|
|
87
|
+
if (!fileURL || pending) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
dispatch(
|
|
91
|
+
setActiveFile({
|
|
92
|
+
fileName,
|
|
93
|
+
fileURL,
|
|
94
|
+
mimetype,
|
|
95
|
+
originalName,
|
|
96
|
+
duration,
|
|
97
|
+
waveForm,
|
|
98
|
+
})
|
|
99
|
+
);
|
|
77
100
|
dispatch(setActiveModal(MODAL_TYPES.FILE_PREVIEW));
|
|
78
101
|
};
|
|
79
102
|
|
|
103
|
+
const extensionLabel = (() => {
|
|
104
|
+
const dotIndex = fileName.lastIndexOf('.');
|
|
105
|
+
if (dotIndex !== -1 && dotIndex < fileName.length - 1) {
|
|
106
|
+
return fileName.slice(dotIndex + 1).toUpperCase();
|
|
107
|
+
}
|
|
108
|
+
return getExtensionForMime(mimetype).replace('.', '').toUpperCase();
|
|
109
|
+
})();
|
|
110
|
+
|
|
80
111
|
return (
|
|
81
|
-
<UnsupportedContainer
|
|
112
|
+
<UnsupportedContainer
|
|
113
|
+
isUser={isUser}
|
|
114
|
+
onPress={handleOpen}
|
|
115
|
+
activeOpacity={pending ? 1 : 0.7}
|
|
116
|
+
>
|
|
82
117
|
<BackgroundFile>
|
|
83
|
-
<FileIcon />
|
|
118
|
+
{placeholderIcon || <FileIcon width={36} height={36} />}
|
|
84
119
|
</BackgroundFile>
|
|
85
120
|
<FileInformation>
|
|
86
121
|
<FileName
|
|
122
|
+
numberOfLines={1}
|
|
87
123
|
isUser={isUser}
|
|
88
124
|
colorIsUser={config?.colors?.primary}
|
|
89
125
|
colorUsers={config?.colors?.secondary}
|
|
90
126
|
>{formatFileName(fileName, 20)}</FileName>
|
|
91
|
-
{size && (
|
|
127
|
+
{(pending || size) && (
|
|
128
|
+
<FileSizeContainer>
|
|
129
|
+
{pending ? (
|
|
130
|
+
<View
|
|
131
|
+
style={{
|
|
132
|
+
flexDirection: 'row',
|
|
133
|
+
alignItems: 'center',
|
|
134
|
+
gap: 6,
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<ActivityIndicator size="small" color="#5B6B8C" />
|
|
138
|
+
<FileSize>Uploading...</FileSize>
|
|
139
|
+
</View>
|
|
140
|
+
) : (
|
|
141
|
+
<FileSize>{formatFileSize(size as string)}</FileSize>
|
|
142
|
+
)}
|
|
143
|
+
</FileSizeContainer>
|
|
144
|
+
)}
|
|
145
|
+
{!pending && !size && (
|
|
92
146
|
<FileSizeContainer>
|
|
93
|
-
<FileSize>{
|
|
147
|
+
<FileSize>{extensionLabel}</FileSize>
|
|
94
148
|
</FileSizeContainer>
|
|
95
149
|
)}
|
|
96
150
|
</FileInformation>
|