@kesha-antonov/react-native-chat 4.2.0 → 4.4.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.
Files changed (318) hide show
  1. package/README.md +253 -47
  2. package/lib/Actions.d.ts +5 -5
  3. package/lib/Actions.d.ts.map +1 -1
  4. package/lib/Actions.js +28 -39
  5. package/lib/Actions.js.map +1 -1
  6. package/lib/Avatar.d.ts.map +1 -1
  7. package/lib/Avatar.js +34 -32
  8. package/lib/Avatar.js.map +1 -1
  9. package/lib/Bubble/index.d.ts.map +1 -1
  10. package/lib/Bubble/index.js +98 -28
  11. package/lib/Bubble/index.js.map +1 -1
  12. package/lib/Bubble/styles.d.ts +28 -11
  13. package/lib/Bubble/styles.d.ts.map +1 -1
  14. package/lib/Bubble/styles.js +108 -80
  15. package/lib/Bubble/styles.js.map +1 -1
  16. package/lib/Bubble/types.d.ts +12 -7
  17. package/lib/Bubble/types.d.ts.map +1 -1
  18. package/lib/Chat/ChatKeyboardAvoidingView.d.ts +8 -3
  19. package/lib/Chat/ChatKeyboardAvoidingView.d.ts.map +1 -1
  20. package/lib/Chat/ChatKeyboardAvoidingView.js +3 -5
  21. package/lib/Chat/ChatKeyboardAvoidingView.js.map +1 -1
  22. package/lib/Chat/index.d.ts +1 -0
  23. package/lib/Chat/index.d.ts.map +1 -1
  24. package/lib/Chat/index.js +94 -28
  25. package/lib/Chat/index.js.map +1 -1
  26. package/lib/Chat/types.d.ts +66 -24
  27. package/lib/Chat/types.d.ts.map +1 -1
  28. package/lib/ChatAvatar.d.ts.map +1 -1
  29. package/lib/ChatAvatar.js +11 -17
  30. package/lib/ChatAvatar.js.map +1 -1
  31. package/lib/ChatContext.d.ts +31 -1
  32. package/lib/ChatContext.d.ts.map +1 -1
  33. package/lib/ChatContext.js +4 -0
  34. package/lib/ChatContext.js.map +1 -1
  35. package/lib/Color.d.ts.map +1 -1
  36. package/lib/Color.js +6 -2
  37. package/lib/Color.js.map +1 -1
  38. package/lib/Composer.d.ts +14 -1
  39. package/lib/Composer.d.ts.map +1 -1
  40. package/lib/Composer.js +68 -50
  41. package/lib/Composer.js.map +1 -1
  42. package/lib/Constant.d.ts +1 -0
  43. package/lib/Constant.d.ts.map +1 -1
  44. package/lib/Constant.js +1 -0
  45. package/lib/Constant.js.map +1 -1
  46. package/lib/Day/index.d.ts.map +1 -1
  47. package/lib/Day/index.js +7 -3
  48. package/lib/Day/index.js.map +1 -1
  49. package/lib/Day/styles.d.ts +6 -7
  50. package/lib/Day/styles.d.ts.map +1 -1
  51. package/lib/Day/styles.js +11 -13
  52. package/lib/Day/styles.js.map +1 -1
  53. package/lib/Icons.d.ts +24 -0
  54. package/lib/Icons.d.ts.map +1 -0
  55. package/lib/Icons.js +2 -0
  56. package/lib/Icons.js.map +1 -0
  57. package/lib/InputToolbar.d.ts +18 -5
  58. package/lib/InputToolbar.d.ts.map +1 -1
  59. package/lib/InputToolbar.js +246 -19
  60. package/lib/InputToolbar.js.map +1 -1
  61. package/lib/LoadEarlierMessages.d.ts.map +1 -1
  62. package/lib/LoadEarlierMessages.js +12 -7
  63. package/lib/LoadEarlierMessages.js.map +1 -1
  64. package/lib/Message/index.d.ts.map +1 -1
  65. package/lib/Message/index.js +21 -12
  66. package/lib/Message/index.js.map +1 -1
  67. package/lib/Message/styles.js +2 -2
  68. package/lib/Message/styles.js.map +1 -1
  69. package/lib/MessageAudio.d.ts +2 -1
  70. package/lib/MessageAudio.d.ts.map +1 -1
  71. package/lib/MessageAudio.js +121 -19
  72. package/lib/MessageAudio.js.map +1 -1
  73. package/lib/MessageLocation.d.ts +9 -0
  74. package/lib/MessageLocation.d.ts.map +1 -0
  75. package/lib/MessageLocation.js +85 -0
  76. package/lib/MessageLocation.js.map +1 -0
  77. package/lib/MessageText.d.ts +12 -1
  78. package/lib/MessageText.d.ts.map +1 -1
  79. package/lib/MessageText.js +38 -11
  80. package/lib/MessageText.js.map +1 -1
  81. package/lib/MessageVideo.d.ts +2 -1
  82. package/lib/MessageVideo.d.ts.map +1 -1
  83. package/lib/MessageVideo.js +156 -18
  84. package/lib/MessageVideo.js.map +1 -1
  85. package/lib/MessagesContainer/components/Item/index.d.ts +2 -1
  86. package/lib/MessagesContainer/components/Item/index.d.ts.map +1 -1
  87. package/lib/MessagesContainer/components/Item/index.js +23 -1
  88. package/lib/MessagesContainer/components/Item/index.js.map +1 -1
  89. package/lib/MessagesContainer/index.d.ts.map +1 -1
  90. package/lib/MessagesContainer/index.js +24 -11
  91. package/lib/MessagesContainer/index.js.map +1 -1
  92. package/lib/MessagesContainer/styles.d.ts +23 -3
  93. package/lib/MessagesContainer/styles.d.ts.map +1 -1
  94. package/lib/MessagesContainer/styles.js +25 -5
  95. package/lib/MessagesContainer/styles.js.map +1 -1
  96. package/lib/Models.d.ts +72 -0
  97. package/lib/Models.d.ts.map +1 -1
  98. package/lib/QuickReplies.d.ts +1 -1
  99. package/lib/QuickReplies.d.ts.map +1 -1
  100. package/lib/QuickReplies.js +13 -7
  101. package/lib/QuickReplies.js.map +1 -1
  102. package/lib/Reactions/MessageReactions.d.ts.map +1 -1
  103. package/lib/Reactions/MessageReactions.js +11 -10
  104. package/lib/Reactions/MessageReactions.js.map +1 -1
  105. package/lib/Reactions/ReactionPicker.d.ts.map +1 -1
  106. package/lib/Reactions/ReactionPicker.js +5 -3
  107. package/lib/Reactions/ReactionPicker.js.map +1 -1
  108. package/lib/Send.d.ts +2 -2
  109. package/lib/Send.d.ts.map +1 -1
  110. package/lib/Send.js +37 -37
  111. package/lib/Send.js.map +1 -1
  112. package/lib/SystemMessage.d.ts.map +1 -1
  113. package/lib/SystemMessage.js +8 -5
  114. package/lib/SystemMessage.js.map +1 -1
  115. package/lib/Theme.d.ts +159 -0
  116. package/lib/Theme.d.ts.map +1 -0
  117. package/lib/Theme.js +147 -0
  118. package/lib/Theme.js.map +1 -0
  119. package/lib/Time.d.ts.map +1 -1
  120. package/lib/Time.js +7 -5
  121. package/lib/Time.js.map +1 -1
  122. package/lib/TypingIndicator/index.d.ts.map +1 -1
  123. package/lib/TypingIndicator/index.js +4 -1
  124. package/lib/TypingIndicator/index.js.map +1 -1
  125. package/lib/TypingIndicator/styles.d.ts +2 -2
  126. package/lib/TypingIndicator/styles.d.ts.map +1 -1
  127. package/lib/TypingIndicator/styles.js +4 -5
  128. package/lib/TypingIndicator/styles.js.map +1 -1
  129. package/lib/components/AttachmentSheet.d.ts +28 -0
  130. package/lib/components/AttachmentSheet.d.ts.map +1 -0
  131. package/lib/components/AttachmentSheet.js +177 -0
  132. package/lib/components/AttachmentSheet.js.map +1 -0
  133. package/lib/components/BasicMarkdown.d.ts +12 -0
  134. package/lib/components/BasicMarkdown.d.ts.map +1 -0
  135. package/lib/components/BasicMarkdown.js +220 -0
  136. package/lib/components/BasicMarkdown.js.map +1 -0
  137. package/lib/components/ContextMenu.d.ts +25 -0
  138. package/lib/components/ContextMenu.d.ts.map +1 -0
  139. package/lib/components/ContextMenu.js +122 -0
  140. package/lib/components/ContextMenu.js.map +1 -0
  141. package/lib/components/Icon.d.ts +17 -0
  142. package/lib/components/Icon.d.ts.map +1 -0
  143. package/lib/components/Icon.js +19 -0
  144. package/lib/components/Icon.js.map +1 -0
  145. package/lib/components/LucideIcon.d.ts +10 -0
  146. package/lib/components/LucideIcon.d.ts.map +1 -0
  147. package/lib/components/LucideIcon.js +118 -0
  148. package/lib/components/LucideIcon.js.map +1 -0
  149. package/lib/components/MarkdownMessageText.d.ts +19 -0
  150. package/lib/components/MarkdownMessageText.d.ts.map +1 -0
  151. package/lib/components/MarkdownMessageText.js +35 -0
  152. package/lib/components/MarkdownMessageText.js.map +1 -0
  153. package/lib/components/MediaCard.d.ts +19 -0
  154. package/lib/components/MediaCard.d.ts.map +1 -0
  155. package/lib/components/MediaCard.js +102 -0
  156. package/lib/components/MediaCard.js.map +1 -0
  157. package/lib/components/MediaControls.d.ts +41 -0
  158. package/lib/components/MediaControls.d.ts.map +1 -0
  159. package/lib/components/MediaControls.js +524 -0
  160. package/lib/components/MediaControls.js.map +1 -0
  161. package/lib/components/MessageReply.d.ts.map +1 -1
  162. package/lib/components/MessageReply.js +11 -9
  163. package/lib/components/MessageReply.js.map +1 -1
  164. package/lib/components/ReplyPreview.d.ts +3 -1
  165. package/lib/components/ReplyPreview.d.ts.map +1 -1
  166. package/lib/components/ReplyPreview.js +37 -36
  167. package/lib/components/ReplyPreview.js.map +1 -1
  168. package/lib/components/SendIcon.d.ts +13 -0
  169. package/lib/components/SendIcon.d.ts.map +1 -0
  170. package/lib/components/SendIcon.js +37 -0
  171. package/lib/components/SendIcon.js.map +1 -0
  172. package/lib/components/Ticks.d.ts +25 -0
  173. package/lib/components/Ticks.d.ts.map +1 -0
  174. package/lib/components/Ticks.js +102 -0
  175. package/lib/components/Ticks.js.map +1 -0
  176. package/lib/components/VideoNoteOverlay.d.ts +25 -0
  177. package/lib/components/VideoNoteOverlay.d.ts.map +1 -0
  178. package/lib/components/VideoNoteOverlay.js +151 -0
  179. package/lib/components/VideoNoteOverlay.js.map +1 -0
  180. package/lib/components/VideoNoteRecorder.d.ts +19 -0
  181. package/lib/components/VideoNoteRecorder.d.ts.map +1 -0
  182. package/lib/components/VideoNoteRecorder.js +634 -0
  183. package/lib/components/VideoNoteRecorder.js.map +1 -0
  184. package/lib/components/VideoRecordButton.d.ts +29 -0
  185. package/lib/components/VideoRecordButton.d.ts.map +1 -0
  186. package/lib/components/VideoRecordButton.js +117 -0
  187. package/lib/components/VideoRecordButton.js.map +1 -0
  188. package/lib/components/VoiceMessageInput.d.ts +33 -0
  189. package/lib/components/VoiceMessageInput.d.ts.map +1 -0
  190. package/lib/components/VoiceMessageInput.js +403 -0
  191. package/lib/components/VoiceMessageInput.js.map +1 -0
  192. package/lib/components/WaveformPlayer.d.ts +19 -0
  193. package/lib/components/WaveformPlayer.d.ts.map +1 -0
  194. package/lib/components/WaveformPlayer.js +367 -0
  195. package/lib/components/WaveformPlayer.js.map +1 -0
  196. package/lib/components/mediaPalette.d.ts +23 -0
  197. package/lib/components/mediaPalette.d.ts.map +1 -0
  198. package/lib/components/mediaPalette.js +16 -0
  199. package/lib/components/mediaPalette.js.map +1 -0
  200. package/lib/components/mediaPlayback.d.ts +22 -0
  201. package/lib/components/mediaPlayback.d.ts.map +1 -0
  202. package/lib/components/mediaPlayback.js +34 -0
  203. package/lib/components/mediaPlayback.js.map +1 -0
  204. package/lib/dayjsLocales.d.ts +18 -0
  205. package/lib/dayjsLocales.d.ts.map +1 -0
  206. package/lib/dayjsLocales.js +36 -0
  207. package/lib/dayjsLocales.js.map +1 -0
  208. package/lib/hooks/useColorScheme.d.ts +9 -2
  209. package/lib/hooks/useColorScheme.d.ts.map +1 -1
  210. package/lib/hooks/useColorScheme.js +9 -3
  211. package/lib/hooks/useColorScheme.js.map +1 -1
  212. package/lib/hooks/useIcons.d.ts +4 -0
  213. package/lib/hooks/useIcons.d.ts.map +1 -0
  214. package/lib/hooks/useIcons.js +8 -0
  215. package/lib/hooks/useIcons.js.map +1 -0
  216. package/lib/hooks/useIsRTL.d.ts +3 -0
  217. package/lib/hooks/useIsRTL.d.ts.map +1 -0
  218. package/lib/hooks/useIsRTL.js +7 -0
  219. package/lib/hooks/useIsRTL.js.map +1 -0
  220. package/lib/hooks/useKeyboardVerticalOffset.d.ts +29 -8
  221. package/lib/hooks/useKeyboardVerticalOffset.d.ts.map +1 -1
  222. package/lib/hooks/useKeyboardVerticalOffset.js +41 -8
  223. package/lib/hooks/useKeyboardVerticalOffset.js.map +1 -1
  224. package/lib/hooks/useLabels.d.ts +4 -0
  225. package/lib/hooks/useLabels.d.ts.map +1 -0
  226. package/lib/hooks/useLabels.js +8 -0
  227. package/lib/hooks/useLabels.js.map +1 -0
  228. package/lib/hooks/useTheme.d.ts +21 -0
  229. package/lib/hooks/useTheme.d.ts.map +1 -0
  230. package/lib/hooks/useTheme.js +32 -0
  231. package/lib/hooks/useTheme.js.map +1 -0
  232. package/lib/i18n.d.ts +66 -0
  233. package/lib/i18n.d.ts.map +1 -0
  234. package/lib/i18n.js +53 -0
  235. package/lib/i18n.js.map +1 -0
  236. package/lib/index.d.ts +19 -0
  237. package/lib/index.d.ts.map +1 -1
  238. package/lib/index.js +19 -0
  239. package/lib/index.js.map +1 -1
  240. package/lib/locales/ar.d.ts +3 -0
  241. package/lib/locales/ar.d.ts.map +1 -0
  242. package/lib/locales/ar.js +22 -0
  243. package/lib/locales/ar.js.map +1 -0
  244. package/lib/locales/de.d.ts +3 -0
  245. package/lib/locales/de.d.ts.map +1 -0
  246. package/lib/locales/de.js +22 -0
  247. package/lib/locales/de.js.map +1 -0
  248. package/lib/locales/es.d.ts +3 -0
  249. package/lib/locales/es.d.ts.map +1 -0
  250. package/lib/locales/es.js +22 -0
  251. package/lib/locales/es.js.map +1 -0
  252. package/lib/locales/fr.d.ts +3 -0
  253. package/lib/locales/fr.d.ts.map +1 -0
  254. package/lib/locales/fr.js +22 -0
  255. package/lib/locales/fr.js.map +1 -0
  256. package/lib/locales/hi.d.ts +3 -0
  257. package/lib/locales/hi.d.ts.map +1 -0
  258. package/lib/locales/hi.js +22 -0
  259. package/lib/locales/hi.js.map +1 -0
  260. package/lib/locales/id.d.ts +3 -0
  261. package/lib/locales/id.d.ts.map +1 -0
  262. package/lib/locales/id.js +22 -0
  263. package/lib/locales/id.js.map +1 -0
  264. package/lib/locales/index.d.ts +19 -0
  265. package/lib/locales/index.d.ts.map +1 -0
  266. package/lib/locales/index.js +39 -0
  267. package/lib/locales/index.js.map +1 -0
  268. package/lib/locales/it.d.ts +3 -0
  269. package/lib/locales/it.d.ts.map +1 -0
  270. package/lib/locales/it.js +22 -0
  271. package/lib/locales/it.js.map +1 -0
  272. package/lib/locales/ja.d.ts +3 -0
  273. package/lib/locales/ja.d.ts.map +1 -0
  274. package/lib/locales/ja.js +22 -0
  275. package/lib/locales/ja.js.map +1 -0
  276. package/lib/locales/ko.d.ts +3 -0
  277. package/lib/locales/ko.d.ts.map +1 -0
  278. package/lib/locales/ko.js +22 -0
  279. package/lib/locales/ko.js.map +1 -0
  280. package/lib/locales/nl.d.ts +3 -0
  281. package/lib/locales/nl.d.ts.map +1 -0
  282. package/lib/locales/nl.js +22 -0
  283. package/lib/locales/nl.js.map +1 -0
  284. package/lib/locales/pl.d.ts +3 -0
  285. package/lib/locales/pl.d.ts.map +1 -0
  286. package/lib/locales/pl.js +22 -0
  287. package/lib/locales/pl.js.map +1 -0
  288. package/lib/locales/pt.d.ts +3 -0
  289. package/lib/locales/pt.d.ts.map +1 -0
  290. package/lib/locales/pt.js +22 -0
  291. package/lib/locales/pt.js.map +1 -0
  292. package/lib/locales/ru.d.ts +3 -0
  293. package/lib/locales/ru.d.ts.map +1 -0
  294. package/lib/locales/ru.js +22 -0
  295. package/lib/locales/ru.js.map +1 -0
  296. package/lib/locales/tr.d.ts +3 -0
  297. package/lib/locales/tr.d.ts.map +1 -0
  298. package/lib/locales/tr.js +22 -0
  299. package/lib/locales/tr.js.map +1 -0
  300. package/lib/locales/zh.d.ts +3 -0
  301. package/lib/locales/zh.d.ts.map +1 -0
  302. package/lib/locales/zh.js +22 -0
  303. package/lib/locales/zh.js.map +1 -0
  304. package/lib/rtl.d.ts +18 -0
  305. package/lib/rtl.d.ts.map +1 -0
  306. package/lib/rtl.js +31 -0
  307. package/lib/rtl.js.map +1 -0
  308. package/lib/types.d.ts +2 -1
  309. package/lib/types.d.ts.map +1 -1
  310. package/package.json +99 -44
  311. package/lib/MessageReply.d.ts +0 -15
  312. package/lib/MessageReply.d.ts.map +0 -1
  313. package/lib/MessageReply.js +0 -102
  314. package/lib/MessageReply.js.map +0 -1
  315. package/lib/ReplyPreview.d.ts +0 -14
  316. package/lib/ReplyPreview.d.ts.map +0 -1
  317. package/lib/ReplyPreview.js +0 -94
  318. 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