@kesha-antonov/react-native-chat 4.1.0 → 4.3.0
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/README.md +306 -62
- package/lib/Actions.d.ts +5 -5
- package/lib/Actions.d.ts.map +1 -1
- package/lib/Actions.js +28 -39
- package/lib/Actions.js.map +1 -1
- package/lib/Avatar.d.ts.map +1 -1
- package/lib/Avatar.js +22 -27
- package/lib/Avatar.js.map +1 -1
- package/lib/Bubble/index.d.ts.map +1 -1
- package/lib/Bubble/index.js +138 -37
- package/lib/Bubble/index.js.map +1 -1
- package/lib/Bubble/styles.d.ts +47 -11
- package/lib/Bubble/styles.d.ts.map +1 -1
- package/lib/Bubble/styles.js +108 -57
- package/lib/Bubble/styles.js.map +1 -1
- package/lib/Bubble/types.d.ts +16 -1
- package/lib/Bubble/types.d.ts.map +1 -1
- package/lib/Chat/ChatKeyboardAvoidingView.d.ts +21 -0
- package/lib/Chat/ChatKeyboardAvoidingView.d.ts.map +1 -0
- package/lib/Chat/ChatKeyboardAvoidingView.js +15 -0
- package/lib/Chat/ChatKeyboardAvoidingView.js.map +1 -0
- package/lib/Chat/index.d.ts.map +1 -1
- package/lib/Chat/index.js +81 -28
- package/lib/Chat/index.js.map +1 -1
- package/lib/Chat/types.d.ts +36 -6
- package/lib/Chat/types.d.ts.map +1 -1
- package/lib/ChatAvatar.d.ts.map +1 -1
- package/lib/ChatAvatar.js +11 -17
- package/lib/ChatAvatar.js.map +1 -1
- package/lib/ChatContext.d.ts +29 -1
- package/lib/ChatContext.d.ts.map +1 -1
- package/lib/ChatContext.js +3 -0
- package/lib/ChatContext.js.map +1 -1
- package/lib/Color.d.ts.map +1 -1
- package/lib/Color.js +6 -2
- package/lib/Color.js.map +1 -1
- package/lib/Composer.d.ts +14 -1
- package/lib/Composer.d.ts.map +1 -1
- package/lib/Composer.js +63 -50
- package/lib/Composer.js.map +1 -1
- package/lib/Day/index.d.ts.map +1 -1
- package/lib/Day/index.js +7 -3
- package/lib/Day/index.js.map +1 -1
- package/lib/Day/styles.d.ts +6 -7
- package/lib/Day/styles.d.ts.map +1 -1
- package/lib/Day/styles.js +11 -13
- package/lib/Day/styles.js.map +1 -1
- package/lib/Icons.d.ts +24 -0
- package/lib/Icons.d.ts.map +1 -0
- package/lib/Icons.js +2 -0
- package/lib/Icons.js.map +1 -0
- package/lib/InputToolbar.d.ts +18 -5
- package/lib/InputToolbar.d.ts.map +1 -1
- package/lib/InputToolbar.js +223 -19
- package/lib/InputToolbar.js.map +1 -1
- package/lib/LoadEarlierMessages.d.ts.map +1 -1
- package/lib/LoadEarlierMessages.js +12 -7
- package/lib/LoadEarlierMessages.js.map +1 -1
- package/lib/Message/index.d.ts.map +1 -1
- package/lib/Message/index.js +10 -9
- package/lib/Message/index.js.map +1 -1
- package/lib/Message/styles.d.ts +1 -4
- package/lib/Message/styles.d.ts.map +1 -1
- package/lib/Message/styles.js +4 -4
- package/lib/Message/styles.js.map +1 -1
- package/lib/MessageAudio.d.ts +2 -1
- package/lib/MessageAudio.d.ts.map +1 -1
- package/lib/MessageAudio.js +121 -19
- package/lib/MessageAudio.js.map +1 -1
- package/lib/MessageLocation.d.ts +9 -0
- package/lib/MessageLocation.d.ts.map +1 -0
- package/lib/MessageLocation.js +85 -0
- package/lib/MessageLocation.js.map +1 -0
- package/lib/MessageText.d.ts +12 -1
- package/lib/MessageText.d.ts.map +1 -1
- package/lib/MessageText.js +38 -11
- package/lib/MessageText.js.map +1 -1
- package/lib/MessageVideo.d.ts +2 -1
- package/lib/MessageVideo.d.ts.map +1 -1
- package/lib/MessageVideo.js +156 -18
- package/lib/MessageVideo.js.map +1 -1
- package/lib/MessagesContainer/FlashList.d.ts +9 -0
- package/lib/MessagesContainer/FlashList.d.ts.map +1 -0
- package/lib/MessagesContainer/FlashList.js +23 -0
- package/lib/MessagesContainer/FlashList.js.map +1 -0
- package/lib/MessagesContainer/components/Item/index.d.ts +2 -1
- package/lib/MessagesContainer/components/Item/index.d.ts.map +1 -1
- package/lib/MessagesContainer/components/Item/index.js +23 -1
- package/lib/MessagesContainer/components/Item/index.js.map +1 -1
- package/lib/MessagesContainer/index.d.ts.map +1 -1
- package/lib/MessagesContainer/index.js +133 -42
- package/lib/MessagesContainer/index.js.map +1 -1
- package/lib/MessagesContainer/styles.d.ts +23 -7
- package/lib/MessagesContainer/styles.d.ts.map +1 -1
- package/lib/MessagesContainer/styles.js +25 -9
- package/lib/MessagesContainer/styles.js.map +1 -1
- package/lib/MessagesContainer/types.d.ts +22 -2
- package/lib/MessagesContainer/types.d.ts.map +1 -1
- package/lib/Models.d.ts +72 -0
- package/lib/Models.d.ts.map +1 -1
- package/lib/QuickReplies.d.ts +1 -1
- package/lib/QuickReplies.d.ts.map +1 -1
- package/lib/QuickReplies.js +13 -7
- package/lib/QuickReplies.js.map +1 -1
- package/lib/Reactions/MessageReactions.d.ts.map +1 -1
- package/lib/Reactions/MessageReactions.js +11 -10
- package/lib/Reactions/MessageReactions.js.map +1 -1
- package/lib/Reactions/ReactionPicker.d.ts.map +1 -1
- package/lib/Reactions/ReactionPicker.js +5 -3
- package/lib/Reactions/ReactionPicker.js.map +1 -1
- package/lib/Send.d.ts +2 -2
- package/lib/Send.d.ts.map +1 -1
- package/lib/Send.js +37 -37
- package/lib/Send.js.map +1 -1
- package/lib/SystemMessage.d.ts.map +1 -1
- package/lib/SystemMessage.js +8 -5
- package/lib/SystemMessage.js.map +1 -1
- package/lib/Theme.d.ts +148 -0
- package/lib/Theme.d.ts.map +1 -0
- package/lib/Theme.js +142 -0
- package/lib/Theme.js.map +1 -0
- package/lib/Time.d.ts.map +1 -1
- package/lib/Time.js +7 -5
- package/lib/Time.js.map +1 -1
- package/lib/TypingIndicator/index.d.ts.map +1 -1
- package/lib/TypingIndicator/index.js +4 -1
- package/lib/TypingIndicator/index.js.map +1 -1
- package/lib/TypingIndicator/styles.d.ts +2 -2
- package/lib/TypingIndicator/styles.d.ts.map +1 -1
- package/lib/TypingIndicator/styles.js +4 -5
- package/lib/TypingIndicator/styles.js.map +1 -1
- package/lib/components/AttachmentSheet.d.ts +28 -0
- package/lib/components/AttachmentSheet.d.ts.map +1 -0
- package/lib/components/AttachmentSheet.js +177 -0
- package/lib/components/AttachmentSheet.js.map +1 -0
- package/lib/components/BasicMarkdown.d.ts +12 -0
- package/lib/components/BasicMarkdown.d.ts.map +1 -0
- package/lib/components/BasicMarkdown.js +220 -0
- package/lib/components/BasicMarkdown.js.map +1 -0
- package/lib/components/ContextMenu.d.ts +25 -0
- package/lib/components/ContextMenu.d.ts.map +1 -0
- package/lib/components/ContextMenu.js +122 -0
- package/lib/components/ContextMenu.js.map +1 -0
- package/lib/components/Icon.d.ts +17 -0
- package/lib/components/Icon.d.ts.map +1 -0
- package/lib/components/Icon.js +19 -0
- package/lib/components/Icon.js.map +1 -0
- package/lib/components/LucideIcon.d.ts +10 -0
- package/lib/components/LucideIcon.d.ts.map +1 -0
- package/lib/components/LucideIcon.js +118 -0
- package/lib/components/LucideIcon.js.map +1 -0
- package/lib/components/MarkdownMessageText.d.ts +19 -0
- package/lib/components/MarkdownMessageText.d.ts.map +1 -0
- package/lib/components/MarkdownMessageText.js +35 -0
- package/lib/components/MarkdownMessageText.js.map +1 -0
- package/lib/components/MediaCard.d.ts +19 -0
- package/lib/components/MediaCard.d.ts.map +1 -0
- package/lib/components/MediaCard.js +102 -0
- package/lib/components/MediaCard.js.map +1 -0
- package/lib/components/MediaControls.d.ts +41 -0
- package/lib/components/MediaControls.d.ts.map +1 -0
- package/lib/components/MediaControls.js +523 -0
- package/lib/components/MediaControls.js.map +1 -0
- package/lib/components/MessageReply.d.ts.map +1 -1
- package/lib/components/MessageReply.js +11 -9
- package/lib/components/MessageReply.js.map +1 -1
- package/lib/components/ReplyPreview.d.ts +3 -1
- package/lib/components/ReplyPreview.d.ts.map +1 -1
- package/lib/components/ReplyPreview.js +36 -36
- package/lib/components/ReplyPreview.js.map +1 -1
- package/lib/components/SendIcon.d.ts +13 -0
- package/lib/components/SendIcon.d.ts.map +1 -0
- package/lib/components/SendIcon.js +37 -0
- package/lib/components/SendIcon.js.map +1 -0
- package/lib/components/Ticks.d.ts +25 -0
- package/lib/components/Ticks.d.ts.map +1 -0
- package/lib/components/Ticks.js +102 -0
- package/lib/components/Ticks.js.map +1 -0
- package/lib/components/VideoNoteOverlay.d.ts +25 -0
- package/lib/components/VideoNoteOverlay.d.ts.map +1 -0
- package/lib/components/VideoNoteOverlay.js +151 -0
- package/lib/components/VideoNoteOverlay.js.map +1 -0
- package/lib/components/VideoNoteRecorder.d.ts +19 -0
- package/lib/components/VideoNoteRecorder.d.ts.map +1 -0
- package/lib/components/VideoNoteRecorder.js +634 -0
- package/lib/components/VideoNoteRecorder.js.map +1 -0
- package/lib/components/VideoRecordButton.d.ts +29 -0
- package/lib/components/VideoRecordButton.d.ts.map +1 -0
- package/lib/components/VideoRecordButton.js +117 -0
- package/lib/components/VideoRecordButton.js.map +1 -0
- package/lib/components/VoiceMessageInput.d.ts +33 -0
- package/lib/components/VoiceMessageInput.d.ts.map +1 -0
- package/lib/components/VoiceMessageInput.js +396 -0
- package/lib/components/VoiceMessageInput.js.map +1 -0
- package/lib/components/WaveformPlayer.d.ts +19 -0
- package/lib/components/WaveformPlayer.d.ts.map +1 -0
- package/lib/components/WaveformPlayer.js +367 -0
- package/lib/components/WaveformPlayer.js.map +1 -0
- package/lib/components/mediaPalette.d.ts +23 -0
- package/lib/components/mediaPalette.d.ts.map +1 -0
- package/lib/components/mediaPalette.js +16 -0
- package/lib/components/mediaPalette.js.map +1 -0
- package/lib/components/mediaPlayback.d.ts +22 -0
- package/lib/components/mediaPlayback.d.ts.map +1 -0
- package/lib/components/mediaPlayback.js +34 -0
- package/lib/components/mediaPlayback.js.map +1 -0
- package/lib/hooks/useColorScheme.d.ts +9 -2
- package/lib/hooks/useColorScheme.d.ts.map +1 -1
- package/lib/hooks/useColorScheme.js +9 -3
- package/lib/hooks/useColorScheme.js.map +1 -1
- package/lib/hooks/useHasKeyboardProvider.d.ts +19 -0
- package/lib/hooks/useHasKeyboardProvider.d.ts.map +1 -0
- package/lib/hooks/useHasKeyboardProvider.js +31 -0
- package/lib/hooks/useHasKeyboardProvider.js.map +1 -0
- package/lib/hooks/useIcons.d.ts +4 -0
- package/lib/hooks/useIcons.d.ts.map +1 -0
- package/lib/hooks/useIcons.js +8 -0
- package/lib/hooks/useIcons.js.map +1 -0
- package/lib/hooks/useIsKeyboardVisible.d.ts +12 -0
- package/lib/hooks/useIsKeyboardVisible.d.ts.map +1 -0
- package/lib/hooks/useIsKeyboardVisible.js +31 -0
- package/lib/hooks/useIsKeyboardVisible.js.map +1 -0
- package/lib/hooks/useKeyboardVerticalOffset.d.ts +28 -0
- package/lib/hooks/useKeyboardVerticalOffset.d.ts.map +1 -0
- package/lib/hooks/useKeyboardVerticalOffset.js +43 -0
- package/lib/hooks/useKeyboardVerticalOffset.js.map +1 -0
- package/lib/hooks/useLabels.d.ts +4 -0
- package/lib/hooks/useLabels.d.ts.map +1 -0
- package/lib/hooks/useLabels.js +8 -0
- package/lib/hooks/useLabels.js.map +1 -0
- package/lib/hooks/useTheme.d.ts +21 -0
- package/lib/hooks/useTheme.d.ts.map +1 -0
- package/lib/hooks/useTheme.js +32 -0
- package/lib/hooks/useTheme.js.map +1 -0
- package/lib/i18n.d.ts +59 -0
- package/lib/i18n.d.ts.map +1 -0
- package/lib/i18n.js +122 -0
- package/lib/i18n.js.map +1 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +17 -0
- package/lib/index.js.map +1 -1
- package/lib/types.d.ts +2 -1
- package/lib/types.d.ts.map +1 -1
- package/package.json +103 -41
- package/lib/MessageReply.d.ts +0 -15
- package/lib/MessageReply.d.ts.map +0 -1
- package/lib/MessageReply.js +0 -102
- package/lib/MessageReply.js.map +0 -1
- package/lib/ReplyPreview.d.ts +0 -14
- package/lib/ReplyPreview.d.ts.map +0 -1
- package/lib/ReplyPreview.js +0 -94
- package/lib/ReplyPreview.js.map +0 -1
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { Pressable, StyleSheet, Text, View, useWindowDimensions } from 'react-native';
|
|
3
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
4
|
+
import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
|
|
5
|
+
import { useLabels } from '../hooks/useLabels';
|
|
6
|
+
import { useTheme } from '../hooks/useTheme';
|
|
7
|
+
import { Icon } from './Icon';
|
|
8
|
+
import { CameraIcon, CloseIcon, FlashIcon, FlipCameraIcon, PauseIcon, PlayIcon } from './MediaControls';
|
|
9
|
+
import { SendIcon } from './SendIcon';
|
|
10
|
+
// Optional camera. Resolved through a try/catch require so the bundle works
|
|
11
|
+
// whether or not the consumer installed `react-native-vision-camera`.
|
|
12
|
+
let visionCamera = null;
|
|
13
|
+
try {
|
|
14
|
+
visionCamera = require('react-native-vision-camera');
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
visionCamera = null;
|
|
18
|
+
}
|
|
19
|
+
// Optional SVG, used to draw the circular progress ring around the note. When
|
|
20
|
+
// absent the recorder still works, just without the ring.
|
|
21
|
+
let Svg = null;
|
|
22
|
+
let SvgCircle = null;
|
|
23
|
+
try {
|
|
24
|
+
const svg = require('react-native-svg');
|
|
25
|
+
Svg = svg.Svg ?? svg.default;
|
|
26
|
+
SvgCircle = svg.Circle;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
Svg = null;
|
|
30
|
+
SvgCircle = null;
|
|
31
|
+
}
|
|
32
|
+
export const isVisionCameraAvailable = !!visionCamera?.Camera;
|
|
33
|
+
// Whether the native vision-camera module is actually linked into the running
|
|
34
|
+
// binary. The JS package can be installed while the native module is not built
|
|
35
|
+
// in (e.g. a simulator running a binary compiled before the dependency was
|
|
36
|
+
// added). `getAvailableCameraDevices()` throws in that case, so we probe it
|
|
37
|
+
// once, outside of render, and fall back to the POC recorder when it fails.
|
|
38
|
+
export const isVisionCameraNativeReady = (() => {
|
|
39
|
+
if (!visionCamera?.Camera?.getAvailableCameraDevices)
|
|
40
|
+
return false;
|
|
41
|
+
try {
|
|
42
|
+
visionCamera.Camera.getAvailableCameraDevices();
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
// The preview fills most of the screen width, as Telegram's does.
|
|
50
|
+
const NOTE_WIDTH_RATIO = 0.88;
|
|
51
|
+
const MAX_ROUND_SIZE = 360;
|
|
52
|
+
/** Gap between the circle's edge and the progress arc. */
|
|
53
|
+
const RING_GAP = 10;
|
|
54
|
+
const RING_STROKE = 3;
|
|
55
|
+
/** Vertical space the bottom controls occupy, kept clear of the preview. */
|
|
56
|
+
const CONTROLS_HEIGHT = 170;
|
|
57
|
+
const withFileScheme = (path) => path.startsWith('file://') || path.startsWith('http') ? path : `file://${path}`;
|
|
58
|
+
/**
|
|
59
|
+
* `m:ss,t` with tenths, like Telegram's recording bar - the moving decimal is
|
|
60
|
+
* what tells you the recorder is actually live.
|
|
61
|
+
*/
|
|
62
|
+
const formatElapsedTenths = (ms) => {
|
|
63
|
+
const tenthsTotal = Math.floor(ms / 100);
|
|
64
|
+
const tenths = tenthsTotal % 10;
|
|
65
|
+
const secs = Math.floor(tenthsTotal / 10) % 60;
|
|
66
|
+
const mins = Math.floor(tenthsTotal / 600);
|
|
67
|
+
return `${mins}:${secs.toString().padStart(2, '0')},${tenths}`;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Telegram-style round video note recorder: records the front camera into a
|
|
71
|
+
* circular preview. When react-native-vision-camera's native module is missing
|
|
72
|
+
* (a simulator, or a binary built before the dependency was added) it shows an
|
|
73
|
+
* unavailable state with the shutter disabled rather than fabricating a note.
|
|
74
|
+
* The two paths are separate components so the vision-camera hooks are only ever
|
|
75
|
+
* called when the native module is present (no crash, stable hook order).
|
|
76
|
+
*/
|
|
77
|
+
export function VideoNoteRecorder(props) {
|
|
78
|
+
return isVisionCameraNativeReady
|
|
79
|
+
? <LiveVideoNote {...props}/>
|
|
80
|
+
: <UnavailableVideoNote onClose={props.onClose}/>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Shared recorder UI: round preview + progress ring + timer + shutter.
|
|
84
|
+
*
|
|
85
|
+
* The shutter is hold-to-record, with the same three-way gesture as a voice
|
|
86
|
+
* note - release to send, slide up to lock for hands-free, slide left to
|
|
87
|
+
* cancel - so the muscle memory carries over between the two recorders.
|
|
88
|
+
*/
|
|
89
|
+
function RecorderChrome({ preview, elapsed, maxMs, isRecording, isLocked, onStart, onStop, onCancel, onLock, onClose, onFlipCamera, onToggleTorch, isTorchOn = false, onTogglePause, isPaused = false, cancelLabel, hint, isDisabled = false, }) {
|
|
90
|
+
const progress = Math.min(1, elapsed / maxMs);
|
|
91
|
+
const { voice } = useTheme();
|
|
92
|
+
// Telegram's preview is most of the screen width, not a fixed 220pt puck.
|
|
93
|
+
const { width } = useWindowDimensions();
|
|
94
|
+
const roundSize = Math.min(width * NOTE_WIDTH_RATIO, MAX_ROUND_SIZE);
|
|
95
|
+
const ringRadius = roundSize / 2 + RING_GAP;
|
|
96
|
+
const ringCircumference = 2 * Math.PI * ringRadius;
|
|
97
|
+
const ringBox = (ringRadius + RING_STROKE) * 2;
|
|
98
|
+
const translateX = useSharedValue(0);
|
|
99
|
+
const translateY = useSharedValue(0);
|
|
100
|
+
const cancelArmed = useSharedValue(0);
|
|
101
|
+
const lockedSV = useSharedValue(0);
|
|
102
|
+
const [isCancelArmed, setIsCancelArmed] = useState(false);
|
|
103
|
+
const cancelThreshold = voice.cancelThreshold;
|
|
104
|
+
const lockThreshold = voice.lockThreshold;
|
|
105
|
+
// Keep the shared lock flag in step when the parent locks or resets.
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
lockedSV.value = isLocked ? 1 : 0;
|
|
108
|
+
if (!isRecording) {
|
|
109
|
+
translateX.value = 0;
|
|
110
|
+
translateY.value = 0;
|
|
111
|
+
cancelArmed.value = 0;
|
|
112
|
+
setIsCancelArmed(false);
|
|
113
|
+
}
|
|
114
|
+
}, [isLocked, isRecording, lockedSV, translateX, translateY, cancelArmed]);
|
|
115
|
+
const shutterGesture = useMemo(() => Gesture.Pan()
|
|
116
|
+
.enabled(!isDisabled)
|
|
117
|
+
.onBegin(() => {
|
|
118
|
+
if (lockedSV.value > 0)
|
|
119
|
+
return;
|
|
120
|
+
runOnJS(onStart)();
|
|
121
|
+
})
|
|
122
|
+
.onUpdate(event => {
|
|
123
|
+
if (lockedSV.value > 0)
|
|
124
|
+
return;
|
|
125
|
+
const x = Math.max(-(cancelThreshold + 60), Math.min(0, event.translationX));
|
|
126
|
+
const y = Math.max(-(lockThreshold + 12), Math.min(0, event.translationY));
|
|
127
|
+
translateX.value = x;
|
|
128
|
+
translateY.value = y;
|
|
129
|
+
const armed = x <= -cancelThreshold ? 1 : 0;
|
|
130
|
+
if (armed !== cancelArmed.value) {
|
|
131
|
+
cancelArmed.value = armed;
|
|
132
|
+
runOnJS(setIsCancelArmed)(armed > 0);
|
|
133
|
+
}
|
|
134
|
+
if (-y >= lockThreshold) {
|
|
135
|
+
lockedSV.value = 1;
|
|
136
|
+
translateX.value = withTiming(0, { duration: 150 });
|
|
137
|
+
translateY.value = withTiming(0, { duration: 150 });
|
|
138
|
+
runOnJS(onLock)();
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
.onFinalize(() => {
|
|
142
|
+
// Locked: the finger lifts but recording continues.
|
|
143
|
+
if (lockedSV.value > 0) {
|
|
144
|
+
translateX.value = withTiming(0, { duration: 150 });
|
|
145
|
+
translateY.value = withTiming(0, { duration: 150 });
|
|
146
|
+
cancelArmed.value = 0;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const cancelled = cancelArmed.value > 0;
|
|
150
|
+
translateX.value = withTiming(0, { duration: 150 });
|
|
151
|
+
translateY.value = withTiming(0, { duration: 150 });
|
|
152
|
+
cancelArmed.value = 0;
|
|
153
|
+
runOnJS(cancelled ? onCancel : onStop)();
|
|
154
|
+
}), [isDisabled, onStart, onStop, onCancel, onLock, translateX, translateY, cancelArmed, lockedSV, cancelThreshold, lockThreshold]);
|
|
155
|
+
const shutterStyle = useAnimatedStyle(() => ({
|
|
156
|
+
transform: [{ translateX: translateX.value }, { translateY: translateY.value }],
|
|
157
|
+
}));
|
|
158
|
+
return (<View style={styles.overlay}>
|
|
159
|
+
{/* Explicit close only. A tap-anywhere scrim would swallow the release of
|
|
160
|
+
the very press that opened the recorder, closing it instantly. */}
|
|
161
|
+
{!isRecording && (<Pressable onPress={onClose} style={styles.closeButton} accessibilityLabel='close'>
|
|
162
|
+
<CloseIcon color='#fff' size={22}/>
|
|
163
|
+
</Pressable>)}
|
|
164
|
+
|
|
165
|
+
<View style={[styles.ringWrapper, { width: ringBox, height: ringBox }]} pointerEvents='box-none'>
|
|
166
|
+
{SvgCircle && progress > 0 && (<Svg width={ringBox} height={ringBox} style={StyleSheet.absoluteFill} pointerEvents='none'>
|
|
167
|
+
{/* No track behind it: Telegram draws only the elapsed arc. */}
|
|
168
|
+
<SvgCircle cx={ringBox / 2} cy={ringBox / 2} r={ringRadius} stroke='#FFFFFF' strokeWidth={RING_STROKE} fill='none' strokeLinecap='round' strokeDasharray={ringCircumference} strokeDashoffset={ringCircumference * (1 - progress)} transform={`rotate(-90 ${ringBox / 2} ${ringBox / 2})`}/>
|
|
169
|
+
</Svg>)}
|
|
170
|
+
|
|
171
|
+
<View style={[styles.round, { width: roundSize, height: roundSize, borderRadius: roundSize / 2 }]}>
|
|
172
|
+
{preview}
|
|
173
|
+
</View>
|
|
174
|
+
</View>
|
|
175
|
+
|
|
176
|
+
<View style={styles.controls} pointerEvents='box-none'>
|
|
177
|
+
<View style={styles.controlsRow} pointerEvents='box-none'>
|
|
178
|
+
{/* Camera flip and torch share one pill on the left, as in Telegram.
|
|
179
|
+
With neither available the pill would be an empty white blob. */}
|
|
180
|
+
<View style={(onFlipCamera || onToggleTorch) ? styles.utilityPill : undefined}>
|
|
181
|
+
{onFlipCamera && (<Pressable onPress={onFlipCamera} hitSlop={8} accessibilityRole='button' accessibilityLabel='switch camera' style={styles.utilityButton}>
|
|
182
|
+
<FlipCameraIcon color={styles.utilityColor.color} size={22}/>
|
|
183
|
+
</Pressable>)}
|
|
184
|
+
{onToggleTorch && (<Pressable onPress={onToggleTorch} hitSlop={8} accessibilityRole='button' accessibilityState={{ selected: isTorchOn }} accessibilityLabel='flash' style={styles.utilityButton}>
|
|
185
|
+
<FlashIcon color={isTorchOn ? '#F5A623' : styles.utilityColor.color} size={22}/>
|
|
186
|
+
</Pressable>)}
|
|
187
|
+
</View>
|
|
188
|
+
|
|
189
|
+
{isRecording && onTogglePause && (<Pressable onPress={onTogglePause} accessibilityRole='button' accessibilityLabel={isPaused ? 'resume recording' : 'pause recording'} style={styles.pauseButton}>
|
|
190
|
+
{isPaused
|
|
191
|
+
? <PlayIcon color={styles.utilityColor.color} size={16}/>
|
|
192
|
+
: <PauseIcon color={styles.utilityColor.color} size={16}/>}
|
|
193
|
+
</Pressable>)}
|
|
194
|
+
</View>
|
|
195
|
+
|
|
196
|
+
<View style={styles.bottomRow} pointerEvents='box-none'>
|
|
197
|
+
{isRecording
|
|
198
|
+
? (<>
|
|
199
|
+
<View style={styles.statusBar}>
|
|
200
|
+
<View style={styles.recDot}/>
|
|
201
|
+
<Text style={styles.statusTime}>{formatElapsedTenths(elapsed)}</Text>
|
|
202
|
+
<Pressable onPress={onCancel} hitSlop={8} accessibilityRole='button' accessibilityLabel='cancel recording' style={styles.cancelWrap}>
|
|
203
|
+
<Text style={styles.cancelText}>{cancelLabel}</Text>
|
|
204
|
+
</Pressable>
|
|
205
|
+
{/* Balances the timer so CANCEL sits centred in the bar. */}
|
|
206
|
+
<View style={styles.statusSpacer}/>
|
|
207
|
+
</View>
|
|
208
|
+
|
|
209
|
+
<Pressable onPress={onStop} accessibilityRole='button' accessibilityLabel='send video message' style={styles.sendButton}>
|
|
210
|
+
<SendIcon color='#fff' size={24}/>
|
|
211
|
+
</Pressable>
|
|
212
|
+
</>)
|
|
213
|
+
: (<View style={styles.shutterRow} pointerEvents='box-none'>
|
|
214
|
+
<GestureDetector gesture={shutterGesture}>
|
|
215
|
+
<Animated.View accessibilityRole='button' accessibilityState={{ disabled: isDisabled }} accessibilityLabel='start recording' style={[
|
|
216
|
+
styles.recordButton,
|
|
217
|
+
isDisabled && styles.recordButtonDisabled,
|
|
218
|
+
isCancelArmed && styles.recordButtonCancelArmed,
|
|
219
|
+
shutterStyle,
|
|
220
|
+
]}>
|
|
221
|
+
<View style={styles.recordInner}/>
|
|
222
|
+
</Animated.View>
|
|
223
|
+
</GestureDetector>
|
|
224
|
+
<Text style={styles.hint}>{hint}</Text>
|
|
225
|
+
</View>)}
|
|
226
|
+
</View>
|
|
227
|
+
</View>
|
|
228
|
+
</View>);
|
|
229
|
+
}
|
|
230
|
+
/** Reusable elapsed-time ticker that auto-stops at `maxMs`. */
|
|
231
|
+
function useRecordingTimer(maxMs, onMax) {
|
|
232
|
+
const [elapsed, setElapsed] = useState(0);
|
|
233
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
234
|
+
const startedAtRef = useRef(0);
|
|
235
|
+
const timerRef = useRef(null);
|
|
236
|
+
const onMaxRef = useRef(onMax);
|
|
237
|
+
onMaxRef.current = onMax;
|
|
238
|
+
const clear = useCallback(() => {
|
|
239
|
+
if (timerRef.current) {
|
|
240
|
+
clearInterval(timerRef.current);
|
|
241
|
+
timerRef.current = null;
|
|
242
|
+
}
|
|
243
|
+
}, []);
|
|
244
|
+
useEffect(() => clear, [clear]);
|
|
245
|
+
const begin = useCallback(() => {
|
|
246
|
+
startedAtRef.current = Date.now();
|
|
247
|
+
setElapsed(0);
|
|
248
|
+
setIsRecording(true);
|
|
249
|
+
clear();
|
|
250
|
+
timerRef.current = setInterval(() => {
|
|
251
|
+
const ms = Date.now() - startedAtRef.current;
|
|
252
|
+
setElapsed(ms);
|
|
253
|
+
if (ms >= maxMs)
|
|
254
|
+
onMaxRef.current();
|
|
255
|
+
}, 100);
|
|
256
|
+
}, [clear, maxMs]);
|
|
257
|
+
const end = useCallback(() => {
|
|
258
|
+
clear();
|
|
259
|
+
setIsRecording(false);
|
|
260
|
+
}, [clear]);
|
|
261
|
+
// Pausing freezes the clock; resuming shifts the start point forward by the
|
|
262
|
+
// paused span so the elapsed time still matches the recorded footage.
|
|
263
|
+
const pausedAtRef = useRef(0);
|
|
264
|
+
const pause = useCallback(() => {
|
|
265
|
+
pausedAtRef.current = Date.now();
|
|
266
|
+
clear();
|
|
267
|
+
}, [clear]);
|
|
268
|
+
const resume = useCallback(() => {
|
|
269
|
+
if (pausedAtRef.current > 0) {
|
|
270
|
+
startedAtRef.current += Date.now() - pausedAtRef.current;
|
|
271
|
+
pausedAtRef.current = 0;
|
|
272
|
+
}
|
|
273
|
+
clear();
|
|
274
|
+
timerRef.current = setInterval(() => {
|
|
275
|
+
const ms = Date.now() - startedAtRef.current;
|
|
276
|
+
setElapsed(ms);
|
|
277
|
+
if (ms >= maxMs)
|
|
278
|
+
onMaxRef.current();
|
|
279
|
+
}, 100);
|
|
280
|
+
}, [clear, maxMs]);
|
|
281
|
+
return { elapsed, isRecording, begin, end, pause, resume };
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* No native camera module: an honest dead-end. The shutter is visibly disabled
|
|
285
|
+
* and nothing can be sent - previously this path fabricated a note from a
|
|
286
|
+
* hardcoded remote sample clip.
|
|
287
|
+
*/
|
|
288
|
+
function UnavailableVideoNote({ onClose }) {
|
|
289
|
+
const labels = useLabels();
|
|
290
|
+
return (<RecorderChrome elapsed={0} maxMs={1} isRecording={false} isLocked={false} isDisabled onStart={() => { }} onStop={() => { }} onCancel={onClose} onLock={() => { }} onClose={onClose} cancelLabel={labels.cancel} hint={labels.noCamera} preview={(<View style={styles.placeholder}>
|
|
291
|
+
<Icon name='camera' color='rgba(255,255,255,0.6)' size={44} fallback={<CameraIcon color='rgba(255,255,255,0.6)' size={44}/>}/>
|
|
292
|
+
<Text style={styles.placeholderText}>{labels.noCamera}</Text>
|
|
293
|
+
</View>)}/>);
|
|
294
|
+
}
|
|
295
|
+
/** Real path: front camera into the round preview (only when native is ready). */
|
|
296
|
+
function LiveVideoNote({ config, onClose, onSend, }) {
|
|
297
|
+
const labels = useLabels();
|
|
298
|
+
const Camera = visionCamera.Camera;
|
|
299
|
+
// Telegram lets you flip to the rear camera; a note starts on the front one.
|
|
300
|
+
const [facing, setFacing] = useState('front');
|
|
301
|
+
const device = visionCamera.useCameraDevice(facing);
|
|
302
|
+
const otherDevice = visionCamera.useCameraDevice(facing === 'front' ? 'back' : 'front');
|
|
303
|
+
const { hasPermission, requestPermission } = visionCamera.useCameraPermission();
|
|
304
|
+
const microphone = visionCamera.useMicrophonePermission?.() ?? { hasPermission: true, requestPermission: async () => true };
|
|
305
|
+
const cameraRef = useRef(null);
|
|
306
|
+
const maxMs = (config?.maxDuration ?? 60) * 1000;
|
|
307
|
+
const minDurationMs = config?.minDurationMs ?? 800;
|
|
308
|
+
const startedAtRef = useRef(0);
|
|
309
|
+
// Set once the user abandons the recorder, so a capture callback that lands
|
|
310
|
+
// afterwards discards its file instead of posting the note they cancelled.
|
|
311
|
+
const abortedRef = useRef(false);
|
|
312
|
+
const micHasPermission = microphone.hasPermission;
|
|
313
|
+
const micRequestPermission = microphone.requestPermission;
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
if (!hasPermission)
|
|
316
|
+
requestPermission?.();
|
|
317
|
+
if (!micHasPermission)
|
|
318
|
+
micRequestPermission?.();
|
|
319
|
+
}, [hasPermission, requestPermission, micHasPermission, micRequestPermission]);
|
|
320
|
+
// A permission the user actively refused should surface, not leave a dead UI.
|
|
321
|
+
const onPermissionDenied = config?.onPermissionDenied;
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
if (hasPermission === false)
|
|
324
|
+
onPermissionDenied?.();
|
|
325
|
+
}, [hasPermission, onPermissionDenied]);
|
|
326
|
+
const finish = useCallback((uri) => {
|
|
327
|
+
const duration = startedAtRef.current > 0 ? Date.now() - startedAtRef.current : 0;
|
|
328
|
+
if (uri && !abortedRef.current && duration >= minDurationMs)
|
|
329
|
+
onSend?.({
|
|
330
|
+
video: uri,
|
|
331
|
+
videoNote: true,
|
|
332
|
+
duration: Math.round(duration / 1000),
|
|
333
|
+
}, false);
|
|
334
|
+
startedAtRef.current = 0;
|
|
335
|
+
onClose();
|
|
336
|
+
}, [onSend, onClose, minDurationMs]);
|
|
337
|
+
const stopRef = useRef(() => { });
|
|
338
|
+
const { elapsed, isRecording, begin, end, pause, resume } = useRecordingTimer(maxMs, () => stopRef.current());
|
|
339
|
+
const stopRecording = useCallback(async () => {
|
|
340
|
+
end();
|
|
341
|
+
try {
|
|
342
|
+
await cameraRef.current?.stopRecording?.();
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
config?.onError?.(error);
|
|
346
|
+
finish(null);
|
|
347
|
+
}
|
|
348
|
+
}, [end, config, finish]);
|
|
349
|
+
stopRef.current = stopRecording;
|
|
350
|
+
// The camera is only usable with both a device and permission; without this
|
|
351
|
+
// the shutter called startRecording on a null ref, threw, and silently
|
|
352
|
+
// dismissed the whole sheet.
|
|
353
|
+
const canRecord = !!device && hasPermission === true;
|
|
354
|
+
const startRecording = useCallback(() => {
|
|
355
|
+
if (!canRecord)
|
|
356
|
+
return;
|
|
357
|
+
begin();
|
|
358
|
+
startedAtRef.current = Date.now();
|
|
359
|
+
try {
|
|
360
|
+
cameraRef.current.startRecording({
|
|
361
|
+
onRecordingFinished: (video) => finish(withFileScheme(video.path)),
|
|
362
|
+
onRecordingError: (error) => {
|
|
363
|
+
config?.onError?.(error);
|
|
364
|
+
finish(null);
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
config?.onError?.(error);
|
|
370
|
+
finish(null);
|
|
371
|
+
}
|
|
372
|
+
}, [begin, finish, config, canRecord]);
|
|
373
|
+
// Abandoning the recorder must stop the capture: otherwise vision-camera keeps
|
|
374
|
+
// recording into an orphaned file and its completion callback posts the note.
|
|
375
|
+
const abort = useCallback(() => {
|
|
376
|
+
abortedRef.current = true;
|
|
377
|
+
end();
|
|
378
|
+
try {
|
|
379
|
+
const stopped = cameraRef.current?.stopRecording?.();
|
|
380
|
+
stopped?.catch?.(() => { });
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
// nothing was recording
|
|
384
|
+
}
|
|
385
|
+
onClose();
|
|
386
|
+
}, [end, onClose]);
|
|
387
|
+
// Covers teardown routes that bypass the X button (Android hardware back,
|
|
388
|
+
// a parent closing the modal): mark aborted and stop the capture.
|
|
389
|
+
useEffect(() => () => {
|
|
390
|
+
abortedRef.current = true;
|
|
391
|
+
try {
|
|
392
|
+
const stopped = cameraRef.current?.stopRecording?.();
|
|
393
|
+
stopped?.catch?.(() => { });
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
// nothing was recording
|
|
397
|
+
}
|
|
398
|
+
}, []);
|
|
399
|
+
// Locking mirrors the voice recorder: slide up while holding the shutter and
|
|
400
|
+
// recording continues hands-free until Send or the trash is pressed.
|
|
401
|
+
const [isLocked, setIsLocked] = useState(false);
|
|
402
|
+
const [isTorchOn, setIsTorchOn] = useState(false);
|
|
403
|
+
const [isPaused, setIsPaused] = useState(false);
|
|
404
|
+
const onShutterStart = useCallback(() => {
|
|
405
|
+
if (isRecording)
|
|
406
|
+
return;
|
|
407
|
+
startRecording();
|
|
408
|
+
}, [isRecording, startRecording]);
|
|
409
|
+
const onShutterStop = useCallback(() => {
|
|
410
|
+
setIsLocked(false);
|
|
411
|
+
stopRecording();
|
|
412
|
+
}, [stopRecording]);
|
|
413
|
+
const onShutterCancel = useCallback(() => {
|
|
414
|
+
setIsLocked(false);
|
|
415
|
+
abortedRef.current = true;
|
|
416
|
+
end();
|
|
417
|
+
try {
|
|
418
|
+
const stopped = cameraRef.current?.stopRecording?.();
|
|
419
|
+
stopped?.catch?.(() => { });
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
// nothing was recording
|
|
423
|
+
}
|
|
424
|
+
// Reset so the recorder stays open for another take, as Telegram does.
|
|
425
|
+
abortedRef.current = false;
|
|
426
|
+
startedAtRef.current = 0;
|
|
427
|
+
}, [end]);
|
|
428
|
+
const flipCamera = useCallback(() => {
|
|
429
|
+
setFacing(current => (current === 'front' ? 'back' : 'front'));
|
|
430
|
+
}, []);
|
|
431
|
+
const toggleTorch = useCallback(() => {
|
|
432
|
+
setIsTorchOn(current => !current);
|
|
433
|
+
}, []);
|
|
434
|
+
// vision-camera exposes pause/resume only on some versions; probe the ref
|
|
435
|
+
// rather than assume, so the control is hidden when it cannot work.
|
|
436
|
+
const canPause = typeof cameraRef.current?.pauseRecording === 'function';
|
|
437
|
+
const togglePause = useCallback(async () => {
|
|
438
|
+
try {
|
|
439
|
+
if (isPaused) {
|
|
440
|
+
await cameraRef.current?.resumeRecording?.();
|
|
441
|
+
setIsPaused(false);
|
|
442
|
+
resume();
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
await cameraRef.current?.pauseRecording?.();
|
|
446
|
+
setIsPaused(true);
|
|
447
|
+
pause();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
config?.onError?.(error);
|
|
452
|
+
}
|
|
453
|
+
}, [isPaused, config, pause, resume]);
|
|
454
|
+
const hint = !canRecord
|
|
455
|
+
? (device ? labels.cameraPermission : labels.noCamera)
|
|
456
|
+
: isLocked
|
|
457
|
+
? labels.stopAndSend
|
|
458
|
+
: isRecording
|
|
459
|
+
? labels.releaseToSend
|
|
460
|
+
: labels.holdToRecord;
|
|
461
|
+
return (<RecorderChrome elapsed={elapsed} maxMs={maxMs} isRecording={isRecording} isLocked={isLocked} isDisabled={!canRecord} onStart={onShutterStart} onStop={onShutterStop} onCancel={onShutterCancel} onLock={() => setIsLocked(true)} onClose={abort} onFlipCamera={otherDevice ? flipCamera : undefined} onToggleTorch={device?.hasTorch ? toggleTorch : undefined} isTorchOn={isTorchOn} onTogglePause={isRecording && canPause ? togglePause : undefined} isPaused={isPaused} cancelLabel={labels.cancel} hint={hint} preview={!device
|
|
462
|
+
? <Text style={styles.placeholderText}>{labels.noCamera}</Text>
|
|
463
|
+
: hasPermission
|
|
464
|
+
? (<Camera ref={cameraRef} style={StyleSheet.absoluteFill} device={device} isActive video audio={microphone.hasPermission} torch={isTorchOn ? 'on' : 'off'}/>)
|
|
465
|
+
: <Text style={styles.placeholderText}>{labels.cameraPermission}</Text>}/>);
|
|
466
|
+
}
|
|
467
|
+
const styles = StyleSheet.create({
|
|
468
|
+
// Dim, not black: Telegram keeps the conversation visible behind the recorder.
|
|
469
|
+
overlay: {
|
|
470
|
+
flex: 1,
|
|
471
|
+
backgroundColor: 'rgba(0, 0, 0, 0.45)',
|
|
472
|
+
alignItems: 'center',
|
|
473
|
+
justifyContent: 'center',
|
|
474
|
+
// Reserve the control strip so the circle centres in the space above it
|
|
475
|
+
// rather than sitting under the shutter and the status bar.
|
|
476
|
+
paddingBottom: CONTROLS_HEIGHT,
|
|
477
|
+
},
|
|
478
|
+
ringWrapper: {
|
|
479
|
+
alignItems: 'center',
|
|
480
|
+
justifyContent: 'center',
|
|
481
|
+
},
|
|
482
|
+
round: {
|
|
483
|
+
overflow: 'hidden',
|
|
484
|
+
backgroundColor: '#111',
|
|
485
|
+
alignItems: 'center',
|
|
486
|
+
justifyContent: 'center',
|
|
487
|
+
},
|
|
488
|
+
placeholder: {
|
|
489
|
+
alignItems: 'center',
|
|
490
|
+
justifyContent: 'center',
|
|
491
|
+
paddingHorizontal: 24,
|
|
492
|
+
gap: 8,
|
|
493
|
+
},
|
|
494
|
+
placeholderText: {
|
|
495
|
+
color: 'rgba(255,255,255,0.7)',
|
|
496
|
+
fontSize: 13,
|
|
497
|
+
textAlign: 'center',
|
|
498
|
+
paddingHorizontal: 24,
|
|
499
|
+
},
|
|
500
|
+
// Everything below the circle, pinned to the bottom of the screen.
|
|
501
|
+
controls: {
|
|
502
|
+
position: 'absolute',
|
|
503
|
+
left: 16,
|
|
504
|
+
right: 16,
|
|
505
|
+
bottom: 28,
|
|
506
|
+
gap: 12,
|
|
507
|
+
},
|
|
508
|
+
controlsRow: {
|
|
509
|
+
flexDirection: 'row',
|
|
510
|
+
alignItems: 'center',
|
|
511
|
+
justifyContent: 'space-between',
|
|
512
|
+
},
|
|
513
|
+
// White pill holding the flip / flash controls.
|
|
514
|
+
utilityPill: {
|
|
515
|
+
flexDirection: 'row',
|
|
516
|
+
alignItems: 'center',
|
|
517
|
+
gap: 4,
|
|
518
|
+
paddingHorizontal: 6,
|
|
519
|
+
paddingVertical: 4,
|
|
520
|
+
borderRadius: 24,
|
|
521
|
+
backgroundColor: 'rgba(255,255,255,0.92)',
|
|
522
|
+
},
|
|
523
|
+
utilityButton: {
|
|
524
|
+
width: 40,
|
|
525
|
+
height: 40,
|
|
526
|
+
alignItems: 'center',
|
|
527
|
+
justifyContent: 'center',
|
|
528
|
+
},
|
|
529
|
+
utilityColor: {
|
|
530
|
+
color: '#1C1C1E',
|
|
531
|
+
},
|
|
532
|
+
pauseButton: {
|
|
533
|
+
width: 48,
|
|
534
|
+
height: 48,
|
|
535
|
+
borderRadius: 24,
|
|
536
|
+
backgroundColor: 'rgba(255,255,255,0.92)',
|
|
537
|
+
alignItems: 'center',
|
|
538
|
+
justifyContent: 'center',
|
|
539
|
+
},
|
|
540
|
+
bottomRow: {
|
|
541
|
+
flexDirection: 'row',
|
|
542
|
+
alignItems: 'center',
|
|
543
|
+
gap: 12,
|
|
544
|
+
},
|
|
545
|
+
// Red dot + tenths timer + centred CANCEL, in one white bar.
|
|
546
|
+
statusBar: {
|
|
547
|
+
flex: 1,
|
|
548
|
+
flexDirection: 'row',
|
|
549
|
+
alignItems: 'center',
|
|
550
|
+
height: 52,
|
|
551
|
+
paddingHorizontal: 16,
|
|
552
|
+
borderRadius: 26,
|
|
553
|
+
backgroundColor: 'rgba(255,255,255,0.92)',
|
|
554
|
+
},
|
|
555
|
+
recDot: {
|
|
556
|
+
width: 9,
|
|
557
|
+
height: 9,
|
|
558
|
+
borderRadius: 4.5,
|
|
559
|
+
backgroundColor: '#FF3B30',
|
|
560
|
+
marginRight: 10,
|
|
561
|
+
},
|
|
562
|
+
statusTime: {
|
|
563
|
+
width: 56,
|
|
564
|
+
fontSize: 15,
|
|
565
|
+
color: '#1C1C1E',
|
|
566
|
+
fontVariant: ['tabular-nums'],
|
|
567
|
+
},
|
|
568
|
+
cancelWrap: {
|
|
569
|
+
flex: 1,
|
|
570
|
+
alignItems: 'center',
|
|
571
|
+
},
|
|
572
|
+
cancelText: {
|
|
573
|
+
fontSize: 15,
|
|
574
|
+
fontWeight: '600',
|
|
575
|
+
letterSpacing: 0.5,
|
|
576
|
+
color: '#3390EC',
|
|
577
|
+
},
|
|
578
|
+
// Mirrors `statusTime` so CANCEL is centred against the bar, not the free space.
|
|
579
|
+
statusSpacer: {
|
|
580
|
+
width: 56,
|
|
581
|
+
},
|
|
582
|
+
sendButton: {
|
|
583
|
+
width: 56,
|
|
584
|
+
height: 56,
|
|
585
|
+
borderRadius: 28,
|
|
586
|
+
backgroundColor: '#3390EC',
|
|
587
|
+
alignItems: 'center',
|
|
588
|
+
justifyContent: 'center',
|
|
589
|
+
},
|
|
590
|
+
// Pre-recording state: the hold-to-record shutter and its hint.
|
|
591
|
+
shutterRow: {
|
|
592
|
+
flex: 1,
|
|
593
|
+
alignItems: 'center',
|
|
594
|
+
gap: 12,
|
|
595
|
+
},
|
|
596
|
+
recordButton: {
|
|
597
|
+
width: 72,
|
|
598
|
+
height: 72,
|
|
599
|
+
borderRadius: 36,
|
|
600
|
+
borderWidth: 4,
|
|
601
|
+
borderColor: '#fff',
|
|
602
|
+
alignItems: 'center',
|
|
603
|
+
justifyContent: 'center',
|
|
604
|
+
},
|
|
605
|
+
recordInner: {
|
|
606
|
+
width: 56,
|
|
607
|
+
height: 56,
|
|
608
|
+
borderRadius: 28,
|
|
609
|
+
backgroundColor: '#E74C3C',
|
|
610
|
+
},
|
|
611
|
+
recordButtonDisabled: {
|
|
612
|
+
opacity: 0.4,
|
|
613
|
+
},
|
|
614
|
+
// Slid past the cancel threshold: the shutter reads as destructive.
|
|
615
|
+
recordButtonCancelArmed: {
|
|
616
|
+
borderColor: '#E74C3C',
|
|
617
|
+
},
|
|
618
|
+
closeButton: {
|
|
619
|
+
position: 'absolute',
|
|
620
|
+
top: 56,
|
|
621
|
+
right: 20,
|
|
622
|
+
width: 44,
|
|
623
|
+
height: 44,
|
|
624
|
+
alignItems: 'center',
|
|
625
|
+
justifyContent: 'center',
|
|
626
|
+
},
|
|
627
|
+
hint: {
|
|
628
|
+
color: 'rgba(255,255,255,0.85)',
|
|
629
|
+
fontSize: 14,
|
|
630
|
+
textAlign: 'center',
|
|
631
|
+
paddingHorizontal: 24,
|
|
632
|
+
},
|
|
633
|
+
});
|
|
634
|
+
//# sourceMappingURL=VideoNoteRecorder.js.map
|