@ethora/chat-component-rn 26.10.9 → 26.10.10

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 (154) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/README.md +1 -1
  3. package/lib/commonjs/components/MainComponents/AlbumGrid.js +6 -2
  4. package/lib/commonjs/components/MainComponents/AlbumGrid.js.map +1 -1
  5. package/lib/commonjs/components/MainComponents/ChatHeader.js +16 -11
  6. package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
  7. package/lib/commonjs/components/MainComponents/MediaMessage.js +13 -0
  8. package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
  9. package/lib/commonjs/components/MainComponents/RoomList.js +7 -16
  10. package/lib/commonjs/components/MainComponents/RoomList.js.map +1 -1
  11. package/lib/commonjs/components/MessageBubble/Message.js +27 -1
  12. package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
  13. package/lib/commonjs/components/Modals/ChatProfileModal/ChatMediaGrid.js +159 -0
  14. package/lib/commonjs/components/Modals/ChatProfileModal/ChatMediaGrid.js.map +1 -0
  15. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +203 -134
  16. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  17. package/lib/commonjs/components/Modals/ChatProfileModal/LeaveChatModal.js +180 -0
  18. package/lib/commonjs/components/Modals/ChatProfileModal/LeaveChatModal.js.map +1 -0
  19. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +100 -162
  20. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  21. package/lib/commonjs/components/Modals/FilePreviewModal/MediaViewer.js +824 -0
  22. package/lib/commonjs/components/Modals/FilePreviewModal/MediaViewer.js.map +1 -0
  23. package/lib/commonjs/components/Modals/FilePreviewModal/useSaveFile.js +179 -0
  24. package/lib/commonjs/components/Modals/FilePreviewModal/useSaveFile.js.map +1 -0
  25. package/lib/commonjs/components/Modals/MessageSearchModal/MessageSearchModal.js +22 -5
  26. package/lib/commonjs/components/Modals/MessageSearchModal/MessageSearchModal.js.map +1 -1
  27. package/lib/commonjs/components/styled/MessageImage.js +4 -1
  28. package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
  29. package/lib/commonjs/components/styled/VideoMessage.js +5 -1
  30. package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
  31. package/lib/commonjs/context/MediaSourceContext.js +18 -0
  32. package/lib/commonjs/context/MediaSourceContext.js.map +1 -0
  33. package/lib/commonjs/helpers/constants/MODAL_COMPONENTS.js +3 -1
  34. package/lib/commonjs/helpers/constants/MODAL_COMPONENTS.js.map +1 -1
  35. package/lib/commonjs/helpers/constants/MODAL_TYPES.js +2 -0
  36. package/lib/commonjs/helpers/constants/MODAL_TYPES.js.map +1 -1
  37. package/lib/commonjs/helpers/publicChatsEntry.js +8 -6
  38. package/lib/commonjs/helpers/publicChatsEntry.js.map +1 -1
  39. package/lib/commonjs/helpers/roomMedia.js +118 -0
  40. package/lib/commonjs/helpers/roomMedia.js.map +1 -0
  41. package/lib/commonjs/i18n/strings.js +36 -0
  42. package/lib/commonjs/i18n/strings.js.map +1 -1
  43. package/lib/commonjs/networking/api-requests/rooms.api.js +9 -13
  44. package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
  45. package/lib/commonjs/version.js +1 -1
  46. package/lib/commonjs/version.js.map +1 -1
  47. package/lib/module/components/MainComponents/AlbumGrid.js +6 -2
  48. package/lib/module/components/MainComponents/AlbumGrid.js.map +1 -1
  49. package/lib/module/components/MainComponents/ChatHeader.js +18 -13
  50. package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
  51. package/lib/module/components/MainComponents/MediaMessage.js +13 -0
  52. package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
  53. package/lib/module/components/MainComponents/RoomList.js +7 -16
  54. package/lib/module/components/MainComponents/RoomList.js.map +1 -1
  55. package/lib/module/components/MessageBubble/Message.js +27 -1
  56. package/lib/module/components/MessageBubble/Message.js.map +1 -1
  57. package/lib/module/components/Modals/ChatProfileModal/ChatMediaGrid.js +154 -0
  58. package/lib/module/components/Modals/ChatProfileModal/ChatMediaGrid.js.map +1 -0
  59. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +204 -136
  60. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  61. package/lib/module/components/Modals/ChatProfileModal/LeaveChatModal.js +175 -0
  62. package/lib/module/components/Modals/ChatProfileModal/LeaveChatModal.js.map +1 -0
  63. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +100 -163
  64. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  65. package/lib/module/components/Modals/FilePreviewModal/MediaViewer.js +819 -0
  66. package/lib/module/components/Modals/FilePreviewModal/MediaViewer.js.map +1 -0
  67. package/lib/module/components/Modals/FilePreviewModal/useSaveFile.js +172 -0
  68. package/lib/module/components/Modals/FilePreviewModal/useSaveFile.js.map +1 -0
  69. package/lib/module/components/Modals/MessageSearchModal/MessageSearchModal.js +20 -4
  70. package/lib/module/components/Modals/MessageSearchModal/MessageSearchModal.js.map +1 -1
  71. package/lib/module/components/styled/MessageImage.js +4 -1
  72. package/lib/module/components/styled/MessageImage.js.map +1 -1
  73. package/lib/module/components/styled/VideoMessage.js +5 -1
  74. package/lib/module/components/styled/VideoMessage.js.map +1 -1
  75. package/lib/module/context/MediaSourceContext.js +13 -0
  76. package/lib/module/context/MediaSourceContext.js.map +1 -0
  77. package/lib/module/helpers/constants/MODAL_COMPONENTS.js +2 -1
  78. package/lib/module/helpers/constants/MODAL_COMPONENTS.js.map +1 -1
  79. package/lib/module/helpers/constants/MODAL_TYPES.js +2 -0
  80. package/lib/module/helpers/constants/MODAL_TYPES.js.map +1 -1
  81. package/lib/module/helpers/publicChatsEntry.js +8 -6
  82. package/lib/module/helpers/publicChatsEntry.js.map +1 -1
  83. package/lib/module/helpers/roomMedia.js +112 -0
  84. package/lib/module/helpers/roomMedia.js.map +1 -0
  85. package/lib/module/i18n/strings.js +36 -0
  86. package/lib/module/i18n/strings.js.map +1 -1
  87. package/lib/module/networking/api-requests/rooms.api.js +9 -13
  88. package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
  89. package/lib/module/version.js +1 -1
  90. package/lib/module/version.js.map +1 -1
  91. package/lib/typescript/components/MainComponents/AlbumGrid.d.ts.map +1 -1
  92. package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
  93. package/lib/typescript/components/MainComponents/MediaMessage.d.ts +3 -1
  94. package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
  95. package/lib/typescript/components/MainComponents/RoomList.d.ts.map +1 -1
  96. package/lib/typescript/components/MessageBubble/Message.d.ts.map +1 -1
  97. package/lib/typescript/components/Modals/ChatProfileModal/ChatMediaGrid.d.ts +16 -0
  98. package/lib/typescript/components/Modals/ChatProfileModal/ChatMediaGrid.d.ts.map +1 -0
  99. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
  100. package/lib/typescript/components/Modals/ChatProfileModal/LeaveChatModal.d.ts +18 -0
  101. package/lib/typescript/components/Modals/ChatProfileModal/LeaveChatModal.d.ts.map +1 -0
  102. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts +16 -0
  103. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  104. package/lib/typescript/components/Modals/FilePreviewModal/MediaViewer.d.ts +27 -0
  105. package/lib/typescript/components/Modals/FilePreviewModal/MediaViewer.d.ts.map +1 -0
  106. package/lib/typescript/components/Modals/FilePreviewModal/useSaveFile.d.ts +9 -0
  107. package/lib/typescript/components/Modals/FilePreviewModal/useSaveFile.d.ts.map +1 -0
  108. package/lib/typescript/components/Modals/MessageSearchModal/MessageSearchModal.d.ts +8 -0
  109. package/lib/typescript/components/Modals/MessageSearchModal/MessageSearchModal.d.ts.map +1 -1
  110. package/lib/typescript/components/styled/MessageImage.d.ts.map +1 -1
  111. package/lib/typescript/components/styled/VideoMessage.d.ts.map +1 -1
  112. package/lib/typescript/context/MediaSourceContext.d.ts +4 -0
  113. package/lib/typescript/context/MediaSourceContext.d.ts.map +1 -0
  114. package/lib/typescript/helpers/constants/MODAL_COMPONENTS.d.ts.map +1 -1
  115. package/lib/typescript/helpers/constants/MODAL_TYPES.d.ts +2 -0
  116. package/lib/typescript/helpers/constants/MODAL_TYPES.d.ts.map +1 -1
  117. package/lib/typescript/helpers/publicChatsEntry.d.ts +7 -5
  118. package/lib/typescript/helpers/publicChatsEntry.d.ts.map +1 -1
  119. package/lib/typescript/helpers/roomMedia.d.ts +26 -0
  120. package/lib/typescript/helpers/roomMedia.d.ts.map +1 -0
  121. package/lib/typescript/i18n/strings.d.ts.map +1 -1
  122. package/lib/typescript/networking/api-requests/rooms.api.d.ts +6 -5
  123. package/lib/typescript/networking/api-requests/rooms.api.d.ts.map +1 -1
  124. package/lib/typescript/types/models/modal.model.d.ts +18 -0
  125. package/lib/typescript/types/models/modal.model.d.ts.map +1 -1
  126. package/lib/typescript/types/types.d.ts +18 -0
  127. package/lib/typescript/types/types.d.ts.map +1 -1
  128. package/lib/typescript/version.d.ts +1 -1
  129. package/lib/typescript/version.d.ts.map +1 -1
  130. package/package.json +9 -2
  131. package/src/components/MainComponents/AlbumGrid.tsx +5 -1
  132. package/src/components/MainComponents/ChatHeader.tsx +16 -11
  133. package/src/components/MainComponents/MediaMessage.tsx +13 -2
  134. package/src/components/MainComponents/RoomList.tsx +8 -19
  135. package/src/components/MessageBubble/Message.tsx +24 -1
  136. package/src/components/Modals/ChatProfileModal/ChatMediaGrid.tsx +174 -0
  137. package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +119 -48
  138. package/src/components/Modals/ChatProfileModal/LeaveChatModal.tsx +191 -0
  139. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +115 -185
  140. package/src/components/Modals/FilePreviewModal/MediaViewer.tsx +880 -0
  141. package/src/components/Modals/FilePreviewModal/useSaveFile.ts +207 -0
  142. package/src/components/Modals/MessageSearchModal/MessageSearchModal.tsx +56 -30
  143. package/src/components/styled/MessageImage.tsx +3 -1
  144. package/src/components/styled/VideoMessage.tsx +5 -1
  145. package/src/context/MediaSourceContext.tsx +14 -0
  146. package/src/helpers/constants/MODAL_COMPONENTS.tsx +4 -1
  147. package/src/helpers/constants/MODAL_TYPES.ts +2 -0
  148. package/src/helpers/publicChatsEntry.ts +8 -7
  149. package/src/helpers/roomMedia.ts +126 -0
  150. package/src/i18n/strings.ts +36 -0
  151. package/src/networking/api-requests/rooms.api.ts +13 -12
  152. package/src/types/models/modal.model.ts +16 -0
  153. package/src/types/types.ts +16 -0
  154. package/src/version.ts +1 -1
@@ -0,0 +1,819 @@
1
+ "use strict";
2
+
3
+ /** @format */
4
+
5
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
+ import { ActivityIndicator, Animated, BackHandler, Image, Pressable, StatusBar, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native';
7
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
8
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
9
+ import { useVideoPlayer, VideoView } from 'expo-video';
10
+ import { useDispatch } from 'react-redux';
11
+ import { BackIcon, DeleteIcon, PauseIcon, PlayIcon, ReplyIcon, SaveIcon } from '../../../assets/icons';
12
+ import { withFileToken } from '../../../helpers/secureFileUrl';
13
+ import { downloadVideoToCache, findCachedVideo, isRemoteUrl, isStreamingUnsupported, markStreamingUnsupported } from '../../../helpers/videoCache';
14
+ import { setDeleteModal } from '../../../roomStore/chatSettingsSlice';
15
+ import { setActiveMessage, setEditAction } from '../../../roomStore/roomsSlice';
16
+ import { useT, useUiLocale } from '../../../i18n/useT';
17
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
+ const WHITE = '#FFFFFF';
19
+ const BAR_BACKGROUND = 'rgba(0, 0, 0, 0.45)';
20
+ /** Drag (fraction of the height) or fling speed that closes the viewer. */
21
+ const CLOSE_DISTANCE = 0.15;
22
+ const CLOSE_VELOCITY = 900;
23
+ /** Swipe (fraction of the width) or fling speed that turns the page. */
24
+ const PAGE_DISTANCE = 0.25;
25
+ const PAGE_VELOCITY = 700;
26
+ /** A playing video takes the chrome away after this long without a touch. */
27
+ const AUTO_HIDE_MS = 3000;
28
+ const formatTime = seconds => {
29
+ const total = Math.max(0, Math.floor(seconds || 0));
30
+ const m = Math.floor(total / 60);
31
+ const s = total % 60;
32
+ return `${m}:${s < 10 ? '0' : ''}${s}`;
33
+ };
34
+ /**
35
+ * Full-screen photo / video viewer that behaves like Telegram's. A tap shows
36
+ * or hides everything but the media (a fade both ways); a drag up or down
37
+ * pulls the media off with the backdrop fading behind it, and a long enough
38
+ * drag or a fling closes the viewer; a sideways swipe turns the page in an
39
+ * album. The bottom bar carries Reply (where replies are on) and Delete (on
40
+ * one's own message). A video has its own controls in the chrome: tapping
41
+ * them works the player instead of hiding the chrome, and while it plays the
42
+ * chrome goes away by itself after a few seconds.
43
+ */
44
+ const MediaViewer = ({
45
+ file,
46
+ onClose,
47
+ onPage,
48
+ onSave,
49
+ onReplyOpened
50
+ }) => {
51
+ const t = useT();
52
+ const locale = useUiLocale();
53
+ const dispatch = useDispatch();
54
+ const insets = useSafeAreaInsets();
55
+ const {
56
+ width,
57
+ height
58
+ } = useWindowDimensions();
59
+ const appear = useRef(new Animated.Value(0)).current;
60
+ const dragX = useRef(new Animated.Value(0)).current;
61
+ const dragY = useRef(new Animated.Value(0)).current;
62
+ const chrome = useRef(new Animated.Value(1)).current;
63
+ const [chromeVisible, setChromeVisible] = useState(true);
64
+ const [bottomBarHeight, setBottomBarHeight] = useState(0);
65
+ const closing = useRef(false);
66
+ const axis = useRef(null);
67
+ const pendingSlide = useRef(0);
68
+ const playing = useRef(false);
69
+ const hideTimer = useRef(null);
70
+ const siblings = file.siblings ?? [];
71
+ const index = file.index ?? 0;
72
+ const hasPrev = siblings.length > 1 && index > 0;
73
+ const hasNext = siblings.length > 1 && index < siblings.length - 1;
74
+ const isVideo = (file.mimetype || '').startsWith('video/');
75
+ const source = file.source;
76
+ const canReply = !!source?.replyTo;
77
+ const canDelete = !!source?.own;
78
+ useEffect(() => {
79
+ Animated.timing(appear, {
80
+ toValue: 1,
81
+ duration: 220,
82
+ useNativeDriver: true
83
+ }).start();
84
+ }, [appear]);
85
+
86
+ // ---- chrome ------------------------------------------------------------
87
+
88
+ const cancelAutoHide = useCallback(() => {
89
+ if (hideTimer.current) {
90
+ clearTimeout(hideTimer.current);
91
+ }
92
+ hideTimer.current = null;
93
+ }, []);
94
+ const showChrome = useCallback(visible => {
95
+ setChromeVisible(visible);
96
+ Animated.timing(chrome, {
97
+ toValue: visible ? 1 : 0,
98
+ duration: 180,
99
+ useNativeDriver: true
100
+ }).start();
101
+ }, [chrome]);
102
+ const scheduleAutoHide = useCallback(() => {
103
+ cancelAutoHide();
104
+ if (!playing.current) {
105
+ return;
106
+ }
107
+ hideTimer.current = setTimeout(() => showChrome(false), AUTO_HIDE_MS);
108
+ }, [cancelAutoHide, showChrome]);
109
+ useEffect(() => cancelAutoHide, [cancelAutoHide]);
110
+ const toggleChrome = () => {
111
+ const next = !chromeVisible;
112
+ showChrome(next);
113
+ if (next) {
114
+ scheduleAutoHide();
115
+ } else {
116
+ cancelAutoHide();
117
+ }
118
+ };
119
+ const handlePlayback = useCallback(isPlaying => {
120
+ playing.current = isPlaying;
121
+ if (isPlaying) {
122
+ scheduleAutoHide();
123
+ } else {
124
+ cancelAutoHide();
125
+ }
126
+ }, [cancelAutoHide, scheduleAutoHide]);
127
+
128
+ // ---- close / page --------------------------------------------------------
129
+
130
+ const close = useCallback((direction = 0) => {
131
+ if (closing.current) {
132
+ return;
133
+ }
134
+ closing.current = true;
135
+ cancelAutoHide();
136
+ const animations = [Animated.timing(appear, {
137
+ toValue: 0,
138
+ duration: 200,
139
+ useNativeDriver: true
140
+ })];
141
+ if (direction !== 0) {
142
+ animations.push(Animated.timing(dragY, {
143
+ toValue: direction * height,
144
+ duration: 200,
145
+ useNativeDriver: true
146
+ }));
147
+ }
148
+ Animated.parallel(animations).start(() => onClose());
149
+ }, [appear, cancelAutoHide, dragY, height, onClose]);
150
+ useEffect(() => {
151
+ const sub = BackHandler.addEventListener('hardwareBackPress', () => {
152
+ close();
153
+ return true;
154
+ });
155
+ return () => sub.remove();
156
+ }, [close]);
157
+ const springBack = value => Animated.spring(value, {
158
+ toValue: 0,
159
+ useNativeDriver: true,
160
+ bounciness: 4
161
+ }).start();
162
+ const turnPage = useCallback(direction => {
163
+ Animated.timing(dragX, {
164
+ toValue: -direction * width,
165
+ duration: 160,
166
+ useNativeDriver: true
167
+ }).start(() => {
168
+ pendingSlide.current = direction;
169
+ onPage?.(index + direction);
170
+ });
171
+ }, [dragX, index, onPage, width]);
172
+
173
+ // The new file mounts off-screen on the side it comes from, then slides in.
174
+ useEffect(() => {
175
+ const direction = pendingSlide.current;
176
+ if (!direction) {
177
+ return;
178
+ }
179
+ pendingSlide.current = 0;
180
+ playing.current = false;
181
+ dragX.setValue(direction * width);
182
+ Animated.timing(dragX, {
183
+ toValue: 0,
184
+ duration: 180,
185
+ useNativeDriver: true
186
+ }).start();
187
+ }, [dragX, file.fileURL, width]);
188
+
189
+ // ---- gestures ------------------------------------------------------------
190
+
191
+ const latest = useRef({
192
+ hasPrev,
193
+ hasNext,
194
+ width,
195
+ height,
196
+ close,
197
+ turnPage
198
+ });
199
+ latest.current = {
200
+ hasPrev,
201
+ hasNext,
202
+ width,
203
+ height,
204
+ close,
205
+ turnPage
206
+ };
207
+ const pageable = siblings.length > 1;
208
+ const pan = useMemo(() => Gesture.Pan().runOnJS(true).activeOffsetY([-12, 12]).activeOffsetX(pageable ? [-12, 12] : [-10000, 10000]).onStart(e => {
209
+ axis.current = pageable && Math.abs(e.translationX) > Math.abs(e.translationY) ? 'x' : 'y';
210
+ }).onUpdate(e => {
211
+ const l = latest.current;
212
+ if (axis.current === 'x') {
213
+ // Past the first or last file the page only gives a little.
214
+ const edge = e.translationX > 0 && !l.hasPrev || e.translationX < 0 && !l.hasNext;
215
+ dragX.setValue(edge ? e.translationX * 0.3 : e.translationX);
216
+ } else {
217
+ dragY.setValue(e.translationY);
218
+ }
219
+ }).onEnd(e => {
220
+ const l = latest.current;
221
+ if (axis.current === 'x') {
222
+ if ((e.translationX < -l.width * PAGE_DISTANCE || e.velocityX < -PAGE_VELOCITY) && l.hasNext) {
223
+ l.turnPage(1);
224
+ } else if ((e.translationX > l.width * PAGE_DISTANCE || e.velocityX > PAGE_VELOCITY) && l.hasPrev) {
225
+ l.turnPage(-1);
226
+ } else {
227
+ springBack(dragX);
228
+ }
229
+ } else if (Math.abs(e.translationY) > l.height * CLOSE_DISTANCE || Math.abs(e.velocityY) > CLOSE_VELOCITY) {
230
+ l.close(Math.sign(e.translationY || e.velocityY) || 1);
231
+ } else {
232
+ springBack(dragY);
233
+ }
234
+ axis.current = null;
235
+ }),
236
+ // eslint-disable-next-line react-hooks/exhaustive-deps
237
+ [pageable]);
238
+
239
+ // ---- actions -------------------------------------------------------------
240
+
241
+ const handleReply = () => {
242
+ if (!source?.replyTo) {
243
+ return;
244
+ }
245
+ const target = source.replyTo;
246
+ close();
247
+ dispatch(setEditAction({
248
+ isEdit: false
249
+ }));
250
+ dispatch(setActiveMessage({
251
+ id: target.id,
252
+ chatJID: target.chatJID
253
+ }));
254
+ onReplyOpened?.();
255
+ };
256
+ const handleDelete = () => {
257
+ if (!source?.own) {
258
+ return;
259
+ }
260
+ const {
261
+ roomJid,
262
+ messageId
263
+ } = source;
264
+ close();
265
+ dispatch(setDeleteModal({
266
+ isDeleteModal: true,
267
+ roomJid,
268
+ messageId
269
+ }));
270
+ };
271
+
272
+ // ---- styles from the animated values -------------------------------------
273
+
274
+ const dragFade = dragY.interpolate({
275
+ inputRange: [-height / 2, 0, height / 2],
276
+ outputRange: [0.15, 1, 0.15],
277
+ extrapolate: 'clamp'
278
+ });
279
+ const dragShrink = dragY.interpolate({
280
+ inputRange: [-height, 0, height],
281
+ outputRange: [0.85, 1, 0.85],
282
+ extrapolate: 'clamp'
283
+ });
284
+ const backdropOpacity = Animated.multiply(appear, dragFade);
285
+ const mediaStyle = {
286
+ transform: [{
287
+ translateX: dragX
288
+ }, {
289
+ translateY: dragY
290
+ }, {
291
+ scale: Animated.multiply(appear.interpolate({
292
+ inputRange: [0, 1],
293
+ outputRange: [0.92, 1]
294
+ }), dragShrink)
295
+ }],
296
+ opacity: appear
297
+ };
298
+ // The chrome steps aside while the media is being dragged.
299
+ const chromeOpacity = Animated.multiply(Animated.multiply(chrome, appear), dragY.interpolate({
300
+ inputRange: [-60, 0, 60],
301
+ outputRange: [0, 1, 0],
302
+ extrapolate: 'clamp'
303
+ }));
304
+ const chromePointerEvents = chromeVisible ? 'box-none' : 'none';
305
+ const subtitle = [source?.timestamp ? new Date(source.timestamp).toLocaleString(locale, {
306
+ day: 'numeric',
307
+ month: 'short',
308
+ hour: '2-digit',
309
+ minute: '2-digit'
310
+ }) : null, siblings.length > 1 ? t('attachment.position', {
311
+ index: index + 1,
312
+ total: siblings.length
313
+ }) : null].filter(Boolean).join(' · ');
314
+ const showBottomBar = canReply || canDelete;
315
+ return /*#__PURE__*/_jsxs(View, {
316
+ style: StyleSheet.absoluteFill,
317
+ testID: "media-viewer",
318
+ children: [/*#__PURE__*/_jsx(StatusBar, {
319
+ barStyle: "light-content",
320
+ hidden: !chromeVisible,
321
+ animated: true
322
+ }), /*#__PURE__*/_jsx(Animated.View, {
323
+ style: [StyleSheet.absoluteFill, styles.backdrop, {
324
+ opacity: backdropOpacity
325
+ }]
326
+ }), /*#__PURE__*/_jsx(GestureDetector, {
327
+ gesture: pan,
328
+ children: /*#__PURE__*/_jsxs(View, {
329
+ style: StyleSheet.absoluteFill,
330
+ children: [/*#__PURE__*/_jsx(Pressable, {
331
+ testID: "media-viewer-surface",
332
+ style: StyleSheet.absoluteFill,
333
+ onPress: toggleChrome,
334
+ children: /*#__PURE__*/_jsx(Animated.View, {
335
+ style: [StyleSheet.absoluteFill, mediaStyle],
336
+ pointerEvents: "none",
337
+ children: isVideo ? null : /*#__PURE__*/_jsx(ViewerImage, {
338
+ uri: file.fileURL
339
+ }, file.fileURL)
340
+ })
341
+ }), isVideo && /*#__PURE__*/_jsx(ViewerVideo, {
342
+ uri: withFileToken(file.fileURL),
343
+ mimetype: file.mimetype,
344
+ mediaStyle: mediaStyle,
345
+ chromeOpacity: chromeOpacity,
346
+ chromePointerEvents: chromePointerEvents,
347
+ bottomOffset: (showBottomBar ? bottomBarHeight : insets.bottom) + 8,
348
+ outerGesture: pan,
349
+ onPlayback: handlePlayback,
350
+ onActivity: scheduleAutoHide
351
+ }, file.fileURL), /*#__PURE__*/_jsxs(Animated.View, {
352
+ pointerEvents: chromePointerEvents,
353
+ style: [styles.topBar, {
354
+ paddingTop: insets.top + 6,
355
+ opacity: chromeOpacity
356
+ }],
357
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
358
+ testID: "media-viewer-back",
359
+ accessibilityRole: "button",
360
+ accessibilityLabel: t('action.close'),
361
+ hitSlop: 12,
362
+ style: styles.iconButton,
363
+ onPress: () => close(),
364
+ children: /*#__PURE__*/_jsx(BackIcon, {
365
+ color: WHITE
366
+ })
367
+ }), /*#__PURE__*/_jsxs(View, {
368
+ style: styles.titleBlock,
369
+ children: [/*#__PURE__*/_jsx(Text, {
370
+ numberOfLines: 1,
371
+ style: styles.title,
372
+ children: source?.senderName || t('modal.filePreview.title')
373
+ }), !!subtitle && /*#__PURE__*/_jsx(Text, {
374
+ numberOfLines: 1,
375
+ style: styles.subtitle,
376
+ children: subtitle
377
+ })]
378
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
379
+ testID: "media-viewer-save",
380
+ accessibilityRole: "button",
381
+ accessibilityLabel: t('action.save'),
382
+ hitSlop: 12,
383
+ style: styles.iconButton,
384
+ onPress: onSave,
385
+ children: /*#__PURE__*/_jsx(SaveIcon, {
386
+ color: WHITE
387
+ })
388
+ })]
389
+ }), showBottomBar && /*#__PURE__*/_jsxs(Animated.View, {
390
+ pointerEvents: chromePointerEvents,
391
+ onLayout: e => setBottomBarHeight(e.nativeEvent.layout.height),
392
+ style: [styles.bottomBar, {
393
+ paddingBottom: insets.bottom + 8,
394
+ opacity: chromeOpacity
395
+ }],
396
+ children: [canReply ? /*#__PURE__*/_jsx(TouchableOpacity, {
397
+ testID: "media-viewer-reply",
398
+ accessibilityRole: "button",
399
+ accessibilityLabel: t('action.reply'),
400
+ hitSlop: 12,
401
+ style: styles.iconButton,
402
+ onPress: handleReply,
403
+ children: /*#__PURE__*/_jsx(ReplyIcon, {
404
+ color: WHITE
405
+ })
406
+ }) : /*#__PURE__*/_jsx(View, {
407
+ style: styles.iconButton
408
+ }), canDelete && /*#__PURE__*/_jsx(TouchableOpacity, {
409
+ testID: "media-viewer-delete",
410
+ accessibilityRole: "button",
411
+ accessibilityLabel: t('action.delete'),
412
+ hitSlop: 12,
413
+ style: styles.iconButton,
414
+ onPress: handleDelete,
415
+ children: /*#__PURE__*/_jsx(DeleteIcon, {
416
+ fill: WHITE
417
+ })
418
+ })]
419
+ })]
420
+ })
421
+ })]
422
+ });
423
+ };
424
+ const ViewerImage = ({
425
+ uri
426
+ }) => {
427
+ const [loaded, setLoaded] = useState(false);
428
+ return /*#__PURE__*/_jsxs(_Fragment, {
429
+ children: [/*#__PURE__*/_jsx(Image, {
430
+ source: {
431
+ uri: withFileToken(uri)
432
+ },
433
+ style: StyleSheet.absoluteFill,
434
+ resizeMode: "contain",
435
+ onLoad: () => setLoaded(true),
436
+ onError: () => setLoaded(true)
437
+ }), !loaded && /*#__PURE__*/_jsx(View, {
438
+ style: styles.center,
439
+ children: /*#__PURE__*/_jsx(ActivityIndicator, {
440
+ color: WHITE
441
+ })
442
+ })]
443
+ });
444
+ };
445
+ /**
446
+ * The video page: the surface in the media layer, the play / pause button
447
+ * and the scrubber in the chrome layer. Streams when the server allows it
448
+ * and falls back to a downloaded copy (AVPlayer refuses servers that ignore
449
+ * byte ranges), which is also what a reopened video plays from.
450
+ */
451
+ const ViewerVideo = ({
452
+ uri,
453
+ mimetype,
454
+ mediaStyle,
455
+ chromeOpacity,
456
+ chromePointerEvents,
457
+ bottomOffset,
458
+ outerGesture,
459
+ onPlayback,
460
+ onActivity
461
+ }) => {
462
+ const t = useT();
463
+ const remote = isRemoteUrl(uri);
464
+ // What the player is given; null until we know where to play from.
465
+ const [source, setSource] = useState(remote ? null : uri);
466
+ // null = not downloading, otherwise 0..1 (0 while the size is unknown).
467
+ const [progress, setProgress] = useState(null);
468
+ const [failed, setFailed] = useState(false);
469
+ const [isPlaying, setIsPlaying] = useState(false);
470
+ const [time, setTime] = useState(0);
471
+ const [duration, setDuration] = useState(0);
472
+ const [scrubTime, setScrubTime] = useState(null);
473
+ const [trackWidth, setTrackWidth] = useState(0);
474
+ const ended = useRef(false);
475
+ // Opens playing, like Telegram; a pause sticks across a fallback copy.
476
+ const wantsPlay = useRef(true);
477
+ const downloadStarted = useRef(false);
478
+ const mounted = useRef(true);
479
+ useEffect(() => () => {
480
+ mounted.current = false;
481
+ }, []);
482
+ const download = useCallback(() => {
483
+ if (downloadStarted.current) {
484
+ return;
485
+ }
486
+ downloadStarted.current = true;
487
+ setProgress(0);
488
+ downloadVideoToCache(uri, mimetype, fraction => {
489
+ if (mounted.current) {
490
+ setProgress(fraction);
491
+ }
492
+ }).then(localUri => {
493
+ if (!mounted.current) {
494
+ return;
495
+ }
496
+ setProgress(null);
497
+ setSource(localUri);
498
+ }).catch(err => {
499
+ console.warn('video download failed', err);
500
+ if (!mounted.current) {
501
+ return;
502
+ }
503
+ setProgress(null);
504
+ setFailed(true);
505
+ });
506
+ }, [mimetype, uri]);
507
+ useEffect(() => {
508
+ if (!remote) {
509
+ return;
510
+ }
511
+ let active = true;
512
+ findCachedVideo(uri, mimetype).then(cached => {
513
+ if (!active) {
514
+ return;
515
+ }
516
+ if (cached) {
517
+ setSource(cached);
518
+ } else if (isStreamingUnsupported(uri)) {
519
+ download();
520
+ } else {
521
+ setSource(uri);
522
+ }
523
+ });
524
+ return () => {
525
+ active = false;
526
+ };
527
+ }, [download, mimetype, remote, uri]);
528
+ const player = useVideoPlayer(source, p => {
529
+ p.muted = false;
530
+ p.timeUpdateEventInterval = 0.25;
531
+ if (wantsPlay.current) {
532
+ p.play();
533
+ }
534
+ });
535
+ useEffect(() => {
536
+ const subs = [player.addListener('playingChange', ({
537
+ isPlaying: next
538
+ }) => {
539
+ setIsPlaying(next);
540
+ onPlayback(next);
541
+ }), player.addListener('timeUpdate', ({
542
+ currentTime
543
+ }) => setTime(currentTime)), player.addListener('playToEnd', () => {
544
+ ended.current = true;
545
+ }), player.addListener('statusChange', event => {
546
+ if (event.status === 'readyToPlay') {
547
+ setDuration(player.duration || 0);
548
+ return;
549
+ }
550
+ if (event.status !== 'error') {
551
+ return;
552
+ }
553
+ if (source === uri && remote) {
554
+ markStreamingUnsupported(uri);
555
+ download();
556
+ } else {
557
+ setFailed(true);
558
+ }
559
+ })];
560
+ return () => subs.forEach(sub => sub.remove());
561
+ }, [download, onPlayback, player, remote, source, uri]);
562
+ useEffect(() => () => onPlayback(false), [onPlayback]);
563
+ const togglePlay = () => {
564
+ onActivity();
565
+ if (isPlaying) {
566
+ wantsPlay.current = false;
567
+ player.pause();
568
+ return;
569
+ }
570
+ if (ended.current) {
571
+ ended.current = false;
572
+ player.currentTime = 0;
573
+ }
574
+ wantsPlay.current = true;
575
+ player.play();
576
+ };
577
+ const latest = useRef({
578
+ trackWidth,
579
+ duration,
580
+ player,
581
+ onActivity
582
+ });
583
+ latest.current = {
584
+ trackWidth,
585
+ duration,
586
+ player,
587
+ onActivity
588
+ };
589
+ const timeAt = x => {
590
+ const l = latest.current;
591
+ if (l.trackWidth <= 0 || l.duration <= 0) {
592
+ return 0;
593
+ }
594
+ return Math.min(1, Math.max(0, x / l.trackWidth)) * l.duration;
595
+ };
596
+ const seek = seconds => {
597
+ const l = latest.current;
598
+ ended.current = false;
599
+ l.player.currentTime = seconds;
600
+ setTime(seconds);
601
+ l.onActivity();
602
+ };
603
+ const scrub = useMemo(() => {
604
+ const drag = Gesture.Pan().runOnJS(true).activeOffsetX([-2, 2]).onUpdate(e => setScrubTime(timeAt(e.x))).onEnd(e => seek(timeAt(e.x))).onFinalize(() => setScrubTime(null)).blocksExternalGesture(outerGesture);
605
+ const tap = Gesture.Tap().runOnJS(true).onEnd(e => seek(timeAt(e.x))).blocksExternalGesture(outerGesture);
606
+ return Gesture.Exclusive(drag, tap);
607
+ }, [outerGesture]);
608
+ const loading = progress !== null || source === null;
609
+ const shown = scrubTime ?? time;
610
+ const fraction = duration > 0 ? Math.min(1, shown / duration) : 0;
611
+ return /*#__PURE__*/_jsxs(_Fragment, {
612
+ children: [/*#__PURE__*/_jsx(Animated.View, {
613
+ style: [StyleSheet.absoluteFill, mediaStyle],
614
+ pointerEvents: "none",
615
+ children: !loading && !failed && /*#__PURE__*/_jsx(VideoView, {
616
+ player: player,
617
+ style: StyleSheet.absoluteFill,
618
+ contentFit: "contain",
619
+ nativeControls: false,
620
+ surfaceType: "textureView"
621
+ })
622
+ }), failed ? /*#__PURE__*/_jsxs(View, {
623
+ style: styles.center,
624
+ pointerEvents: "none",
625
+ children: [/*#__PURE__*/_jsx(Text, {
626
+ style: styles.failedTitle,
627
+ children: t('video.cantPlay')
628
+ }), /*#__PURE__*/_jsx(Text, {
629
+ style: styles.failedHint,
630
+ children: t('video.cantPlayHint')
631
+ })]
632
+ }) : loading ? /*#__PURE__*/_jsxs(View, {
633
+ style: styles.center,
634
+ pointerEvents: "none",
635
+ children: [/*#__PURE__*/_jsx(View, {
636
+ style: styles.playButton,
637
+ children: /*#__PURE__*/_jsx(ActivityIndicator, {
638
+ color: WHITE
639
+ })
640
+ }), !!progress && /*#__PURE__*/_jsxs(Text, {
641
+ style: styles.progress,
642
+ children: [Math.round(progress * 100), "%"]
643
+ })]
644
+ }) : /*#__PURE__*/_jsxs(_Fragment, {
645
+ children: [/*#__PURE__*/_jsx(Animated.View, {
646
+ pointerEvents: chromePointerEvents,
647
+ style: [styles.center, {
648
+ opacity: chromeOpacity
649
+ }],
650
+ children: /*#__PURE__*/_jsx(TouchableOpacity, {
651
+ testID: "media-viewer-play",
652
+ accessibilityRole: "button",
653
+ activeOpacity: 0.8,
654
+ style: styles.playButton,
655
+ onPress: togglePlay,
656
+ children: isPlaying ? /*#__PURE__*/_jsx(PauseIcon, {
657
+ width: 30,
658
+ height: 30,
659
+ color: WHITE
660
+ }) : /*#__PURE__*/_jsx(PlayIcon, {
661
+ width: 30,
662
+ height: 30,
663
+ color: WHITE
664
+ })
665
+ })
666
+ }), /*#__PURE__*/_jsxs(Animated.View, {
667
+ pointerEvents: chromePointerEvents,
668
+ style: [styles.scrubberRow, {
669
+ bottom: bottomOffset,
670
+ opacity: chromeOpacity
671
+ }],
672
+ children: [/*#__PURE__*/_jsx(Text, {
673
+ style: styles.time,
674
+ children: formatTime(shown)
675
+ }), /*#__PURE__*/_jsx(GestureDetector, {
676
+ gesture: scrub,
677
+ children: /*#__PURE__*/_jsxs(View, {
678
+ testID: "media-viewer-scrubber",
679
+ style: styles.track,
680
+ onLayout: e => setTrackWidth(e.nativeEvent.layout.width),
681
+ children: [/*#__PURE__*/_jsx(View, {
682
+ style: styles.trackLine,
683
+ children: /*#__PURE__*/_jsx(View, {
684
+ style: [styles.trackFill, {
685
+ width: `${fraction * 100}%`
686
+ }]
687
+ })
688
+ }), /*#__PURE__*/_jsx(View, {
689
+ style: [styles.thumb, {
690
+ left: fraction * trackWidth - 7
691
+ }]
692
+ })]
693
+ })
694
+ }), /*#__PURE__*/_jsx(Text, {
695
+ style: styles.time,
696
+ children: formatTime(duration)
697
+ })]
698
+ })]
699
+ })]
700
+ });
701
+ };
702
+ const styles = StyleSheet.create({
703
+ backdrop: {
704
+ backgroundColor: '#000000'
705
+ },
706
+ center: {
707
+ ...StyleSheet.absoluteFill,
708
+ alignItems: 'center',
709
+ justifyContent: 'center'
710
+ },
711
+ topBar: {
712
+ position: 'absolute',
713
+ top: 0,
714
+ left: 0,
715
+ right: 0,
716
+ flexDirection: 'row',
717
+ alignItems: 'center',
718
+ paddingHorizontal: 8,
719
+ paddingBottom: 10,
720
+ backgroundColor: BAR_BACKGROUND
721
+ },
722
+ bottomBar: {
723
+ position: 'absolute',
724
+ left: 0,
725
+ right: 0,
726
+ bottom: 0,
727
+ flexDirection: 'row',
728
+ alignItems: 'center',
729
+ justifyContent: 'space-between',
730
+ paddingHorizontal: 8,
731
+ paddingTop: 10,
732
+ backgroundColor: BAR_BACKGROUND
733
+ },
734
+ iconButton: {
735
+ width: 44,
736
+ height: 44,
737
+ alignItems: 'center',
738
+ justifyContent: 'center'
739
+ },
740
+ titleBlock: {
741
+ flex: 1,
742
+ alignItems: 'center',
743
+ paddingHorizontal: 8
744
+ },
745
+ title: {
746
+ color: WHITE,
747
+ fontSize: 16,
748
+ fontWeight: '600'
749
+ },
750
+ subtitle: {
751
+ color: 'rgba(255, 255, 255, 0.7)',
752
+ fontSize: 13,
753
+ marginTop: 2
754
+ },
755
+ playButton: {
756
+ width: 64,
757
+ height: 64,
758
+ borderRadius: 32,
759
+ backgroundColor: 'rgba(0, 0, 0, 0.55)',
760
+ alignItems: 'center',
761
+ justifyContent: 'center'
762
+ },
763
+ progress: {
764
+ marginTop: 10,
765
+ color: WHITE,
766
+ fontSize: 13,
767
+ fontWeight: '600'
768
+ },
769
+ failedTitle: {
770
+ color: WHITE,
771
+ fontSize: 16,
772
+ fontWeight: '600'
773
+ },
774
+ failedHint: {
775
+ marginTop: 8,
776
+ color: 'rgba(255, 255, 255, 0.7)',
777
+ textAlign: 'center',
778
+ paddingHorizontal: 32
779
+ },
780
+ scrubberRow: {
781
+ position: 'absolute',
782
+ left: 12,
783
+ right: 12,
784
+ flexDirection: 'row',
785
+ alignItems: 'center',
786
+ gap: 10
787
+ },
788
+ time: {
789
+ color: WHITE,
790
+ fontSize: 12,
791
+ fontVariant: ['tabular-nums'],
792
+ minWidth: 36,
793
+ textAlign: 'center'
794
+ },
795
+ track: {
796
+ flex: 1,
797
+ height: 32,
798
+ justifyContent: 'center'
799
+ },
800
+ trackLine: {
801
+ height: 3,
802
+ borderRadius: 1.5,
803
+ backgroundColor: 'rgba(255, 255, 255, 0.35)',
804
+ overflow: 'hidden'
805
+ },
806
+ trackFill: {
807
+ height: '100%',
808
+ backgroundColor: WHITE
809
+ },
810
+ thumb: {
811
+ position: 'absolute',
812
+ width: 14,
813
+ height: 14,
814
+ borderRadius: 7,
815
+ backgroundColor: WHITE
816
+ }
817
+ });
818
+ export default MediaViewer;
819
+ //# sourceMappingURL=MediaViewer.js.map