@amityco/react-native-social-uikit 4.1.0 → 4.1.1-11cdc854.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 (156) hide show
  1. package/lib/commonjs/core/assets/icons/xml.js +55 -1
  2. package/lib/commonjs/core/assets/icons/xml.js.map +1 -1
  3. package/lib/commonjs/core/hooks/usePostByIds.js +38 -0
  4. package/lib/commonjs/core/hooks/usePostByIds.js.map +1 -0
  5. package/lib/commonjs/social/components/LoadingImage/index.js +4 -3
  6. package/lib/commonjs/social/components/LoadingImage/index.js.map +1 -1
  7. package/lib/commonjs/social/components/LoadingImage/styles.js +4 -0
  8. package/lib/commonjs/social/components/LoadingImage/styles.js.map +1 -1
  9. package/lib/commonjs/social/components/LoadingVideo/index.js +12 -6
  10. package/lib/commonjs/social/components/LoadingVideo/index.js.map +1 -1
  11. package/lib/commonjs/social/components/LoadingVideo/styles.js +4 -0
  12. package/lib/commonjs/social/components/LoadingVideo/styles.js.map +1 -1
  13. package/lib/commonjs/social/components/MediaViewer/MediaViewer.js +266 -0
  14. package/lib/commonjs/social/components/MediaViewer/MediaViewer.js.map +1 -0
  15. package/lib/commonjs/social/components/MediaViewer/index.js +13 -0
  16. package/lib/commonjs/social/components/MediaViewer/index.js.map +1 -0
  17. package/lib/commonjs/social/components/MediaViewer/styles.js +116 -0
  18. package/lib/commonjs/social/components/MediaViewer/styles.js.map +1 -0
  19. package/lib/commonjs/social/components/PostContent/index.js +75 -120
  20. package/lib/commonjs/social/components/PostContent/index.js.map +1 -1
  21. package/lib/commonjs/social/components/SelectedMediaComponent/SelectedMediaComponent.js +180 -0
  22. package/lib/commonjs/social/components/SelectedMediaComponent/SelectedMediaComponent.js.map +1 -0
  23. package/lib/commonjs/social/components/SelectedMediaComponent/index.js +13 -0
  24. package/lib/commonjs/social/components/SelectedMediaComponent/index.js.map +1 -0
  25. package/lib/commonjs/social/components/SelectedMediaComponent/styles.js +43 -0
  26. package/lib/commonjs/social/components/SelectedMediaComponent/styles.js.map +1 -0
  27. package/lib/commonjs/social/constants/index.js +11 -0
  28. package/lib/commonjs/social/constants/index.js.map +1 -0
  29. package/lib/commonjs/social/elements/PostMediaElement/PostMediaElement.js +280 -0
  30. package/lib/commonjs/social/elements/PostMediaElement/PostMediaElement.js.map +1 -0
  31. package/lib/commonjs/social/elements/PostMediaElement/index.js +13 -0
  32. package/lib/commonjs/social/elements/PostMediaElement/index.js.map +1 -0
  33. package/lib/commonjs/social/elements/PostMediaElement/styles.js +117 -0
  34. package/lib/commonjs/social/elements/PostMediaElement/styles.js.map +1 -0
  35. package/lib/commonjs/social/features/post/Composer/PostComposer.js +91 -106
  36. package/lib/commonjs/social/features/post/Composer/PostComposer.js.map +1 -1
  37. package/lib/commonjs/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.js +17 -7
  38. package/lib/commonjs/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.js.map +1 -1
  39. package/lib/commonjs/social/features/post/components/MediaAttachment/MediaAttachment.js +17 -7
  40. package/lib/commonjs/social/features/post/components/MediaAttachment/MediaAttachment.js.map +1 -1
  41. package/lib/commonjs/social/utils/getFrameRatio.js +52 -0
  42. package/lib/commonjs/social/utils/getFrameRatio.js.map +1 -0
  43. package/lib/commonjs/social/utils/getIndicatorConfig.js +59 -0
  44. package/lib/commonjs/social/utils/getIndicatorConfig.js.map +1 -0
  45. package/lib/module/core/assets/icons/xml.js +48 -0
  46. package/lib/module/core/assets/icons/xml.js.map +1 -1
  47. package/lib/module/core/hooks/usePostByIds.js +32 -0
  48. package/lib/module/core/hooks/usePostByIds.js.map +1 -0
  49. package/lib/module/social/components/LoadingImage/index.js +4 -3
  50. package/lib/module/social/components/LoadingImage/index.js.map +1 -1
  51. package/lib/module/social/components/LoadingImage/styles.js +4 -0
  52. package/lib/module/social/components/LoadingImage/styles.js.map +1 -1
  53. package/lib/module/social/components/LoadingVideo/index.js +13 -7
  54. package/lib/module/social/components/LoadingVideo/index.js.map +1 -1
  55. package/lib/module/social/components/LoadingVideo/styles.js +4 -0
  56. package/lib/module/social/components/LoadingVideo/styles.js.map +1 -1
  57. package/lib/module/social/components/MediaViewer/MediaViewer.js +259 -0
  58. package/lib/module/social/components/MediaViewer/MediaViewer.js.map +1 -0
  59. package/lib/module/social/components/MediaViewer/index.js +2 -0
  60. package/lib/module/social/components/MediaViewer/index.js.map +1 -0
  61. package/lib/module/social/components/MediaViewer/styles.js +109 -0
  62. package/lib/module/social/components/MediaViewer/styles.js.map +1 -0
  63. package/lib/module/social/components/PostContent/index.js +77 -122
  64. package/lib/module/social/components/PostContent/index.js.map +1 -1
  65. package/lib/module/social/components/SelectedMediaComponent/SelectedMediaComponent.js +174 -0
  66. package/lib/module/social/components/SelectedMediaComponent/SelectedMediaComponent.js.map +1 -0
  67. package/lib/module/social/components/SelectedMediaComponent/index.js +2 -0
  68. package/lib/module/social/components/SelectedMediaComponent/index.js.map +1 -0
  69. package/lib/module/social/components/SelectedMediaComponent/styles.js +36 -0
  70. package/lib/module/social/components/SelectedMediaComponent/styles.js.map +1 -0
  71. package/lib/module/social/constants/index.js +5 -0
  72. package/lib/module/social/constants/index.js.map +1 -0
  73. package/lib/module/social/elements/PostMediaElement/PostMediaElement.js +273 -0
  74. package/lib/module/social/elements/PostMediaElement/PostMediaElement.js.map +1 -0
  75. package/lib/module/social/elements/PostMediaElement/index.js +2 -0
  76. package/lib/module/social/elements/PostMediaElement/index.js.map +1 -0
  77. package/lib/module/social/elements/PostMediaElement/styles.js +110 -0
  78. package/lib/module/social/elements/PostMediaElement/styles.js.map +1 -0
  79. package/lib/module/social/features/post/Composer/PostComposer.js +94 -109
  80. package/lib/module/social/features/post/Composer/PostComposer.js.map +1 -1
  81. package/lib/module/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.js +17 -7
  82. package/lib/module/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.js.map +1 -1
  83. package/lib/module/social/features/post/components/MediaAttachment/MediaAttachment.js +17 -7
  84. package/lib/module/social/features/post/components/MediaAttachment/MediaAttachment.js.map +1 -1
  85. package/lib/module/social/utils/getFrameRatio.js +44 -0
  86. package/lib/module/social/utils/getFrameRatio.js.map +1 -0
  87. package/lib/module/social/utils/getIndicatorConfig.js +53 -0
  88. package/lib/module/social/utils/getIndicatorConfig.js.map +1 -0
  89. package/lib/typescript/core/assets/icons/xml.d.ts +7 -0
  90. package/lib/typescript/core/assets/icons/xml.d.ts.map +1 -1
  91. package/lib/typescript/core/hooks/usePostByIds.d.ts +8 -0
  92. package/lib/typescript/core/hooks/usePostByIds.d.ts.map +1 -0
  93. package/lib/typescript/core/types/mention.d.ts +6 -0
  94. package/lib/typescript/core/types/mention.d.ts.map +1 -1
  95. package/lib/typescript/social/components/LoadingImage/index.d.ts +2 -1
  96. package/lib/typescript/social/components/LoadingImage/index.d.ts.map +1 -1
  97. package/lib/typescript/social/components/LoadingImage/styles.d.ts +4 -0
  98. package/lib/typescript/social/components/LoadingImage/styles.d.ts.map +1 -1
  99. package/lib/typescript/social/components/LoadingVideo/index.d.ts +2 -1
  100. package/lib/typescript/social/components/LoadingVideo/index.d.ts.map +1 -1
  101. package/lib/typescript/social/components/LoadingVideo/styles.d.ts +4 -0
  102. package/lib/typescript/social/components/LoadingVideo/styles.d.ts.map +1 -1
  103. package/lib/typescript/social/components/MediaViewer/MediaViewer.d.ts +17 -0
  104. package/lib/typescript/social/components/MediaViewer/MediaViewer.d.ts.map +1 -0
  105. package/lib/typescript/social/components/MediaViewer/index.d.ts +3 -0
  106. package/lib/typescript/social/components/MediaViewer/index.d.ts.map +1 -0
  107. package/lib/typescript/social/components/MediaViewer/styles.d.ts +99 -0
  108. package/lib/typescript/social/components/MediaViewer/styles.d.ts.map +1 -0
  109. package/lib/typescript/social/components/PostContent/index.d.ts.map +1 -1
  110. package/lib/typescript/social/components/SelectedMediaComponent/SelectedMediaComponent.d.ts +12 -0
  111. package/lib/typescript/social/components/SelectedMediaComponent/SelectedMediaComponent.d.ts.map +1 -0
  112. package/lib/typescript/social/components/SelectedMediaComponent/index.d.ts +3 -0
  113. package/lib/typescript/social/components/SelectedMediaComponent/index.d.ts.map +1 -0
  114. package/lib/typescript/social/components/SelectedMediaComponent/styles.d.ts +25 -0
  115. package/lib/typescript/social/components/SelectedMediaComponent/styles.d.ts.map +1 -0
  116. package/lib/typescript/social/constants/index.d.ts +4 -0
  117. package/lib/typescript/social/constants/index.d.ts.map +1 -0
  118. package/lib/typescript/social/elements/PostMediaElement/PostMediaElement.d.ts +15 -0
  119. package/lib/typescript/social/elements/PostMediaElement/PostMediaElement.d.ts.map +1 -0
  120. package/lib/typescript/social/elements/PostMediaElement/index.d.ts +3 -0
  121. package/lib/typescript/social/elements/PostMediaElement/index.d.ts.map +1 -0
  122. package/lib/typescript/social/elements/PostMediaElement/styles.d.ts +88 -0
  123. package/lib/typescript/social/elements/PostMediaElement/styles.d.ts.map +1 -0
  124. package/lib/typescript/social/features/post/Composer/PostComposer.d.ts.map +1 -1
  125. package/lib/typescript/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.d.ts +3 -0
  126. package/lib/typescript/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.d.ts.map +1 -1
  127. package/lib/typescript/social/features/post/components/MediaAttachment/MediaAttachment.d.ts +3 -0
  128. package/lib/typescript/social/features/post/components/MediaAttachment/MediaAttachment.d.ts.map +1 -1
  129. package/lib/typescript/social/utils/getFrameRatio.d.ts +21 -0
  130. package/lib/typescript/social/utils/getFrameRatio.d.ts.map +1 -0
  131. package/lib/typescript/social/utils/getIndicatorConfig.d.ts +11 -0
  132. package/lib/typescript/social/utils/getIndicatorConfig.d.ts.map +1 -0
  133. package/package.json +3 -3
  134. package/src/core/assets/icons/xml.ts +60 -0
  135. package/src/core/hooks/usePostByIds.ts +33 -0
  136. package/src/core/types/mention.ts +6 -0
  137. package/src/social/components/LoadingImage/index.tsx +12 -2
  138. package/src/social/components/LoadingImage/styles.ts +4 -0
  139. package/src/social/components/LoadingVideo/index.tsx +28 -4
  140. package/src/social/components/LoadingVideo/styles.ts +4 -0
  141. package/src/social/components/MediaViewer/MediaViewer.tsx +384 -0
  142. package/src/social/components/MediaViewer/index.tsx +2 -0
  143. package/src/social/components/MediaViewer/styles.ts +109 -0
  144. package/src/social/components/PostContent/index.tsx +86 -159
  145. package/src/social/components/SelectedMediaComponent/SelectedMediaComponent.tsx +236 -0
  146. package/src/social/components/SelectedMediaComponent/index.ts +2 -0
  147. package/src/social/components/SelectedMediaComponent/styles.ts +38 -0
  148. package/src/social/constants/index.ts +4 -0
  149. package/src/social/elements/PostMediaElement/PostMediaElement.tsx +362 -0
  150. package/src/social/elements/PostMediaElement/index.tsx +5 -0
  151. package/src/social/elements/PostMediaElement/styles.ts +99 -0
  152. package/src/social/features/post/Composer/PostComposer.tsx +107 -123
  153. package/src/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.tsx +10 -3
  154. package/src/social/features/post/components/MediaAttachment/MediaAttachment.tsx +14 -3
  155. package/src/social/utils/getFrameRatio.ts +42 -0
  156. package/src/social/utils/getIndicatorConfig.ts +62 -0
@@ -0,0 +1,384 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ FlatList,
4
+ GestureResponderEvent,
5
+ Image,
6
+ ImageStyle,
7
+ LayoutChangeEvent,
8
+ Modal,
9
+ NativeScrollEvent,
10
+ NativeSyntheticEvent,
11
+ PanResponder,
12
+ Pressable,
13
+ StyleProp,
14
+ StyleSheet,
15
+ Text,
16
+ TouchableOpacity,
17
+ useWindowDimensions,
18
+ View,
19
+ } from 'react-native';
20
+ import Video, {
21
+ type OnLoadData,
22
+ type OnProgressData,
23
+ type VideoRef,
24
+ } from 'react-native-video';
25
+ import { SvgXml } from 'react-native-svg';
26
+ import {
27
+ clearIcon,
28
+ pauseControlIcon,
29
+ playControlIcon,
30
+ soundOnIcon,
31
+ soundOffIcon,
32
+ } from '../../../core/assets/icons/xml';
33
+ import { useStyles } from './styles';
34
+
35
+ export type MediaViewerItem = {
36
+ type: 'image' | 'video';
37
+ uri: string;
38
+ /** e.g. 'm3u8' for HLS (published) videos; omit for local files. */
39
+ videoType?: string;
40
+ };
41
+
42
+ type MediaViewerProps = {
43
+ visible: boolean;
44
+ items: MediaViewerItem[];
45
+ initialIndex: number;
46
+ /** Called with the last-viewed index so the caller can decide where to
47
+ * return (published: last-viewed; composer: originally-tapped). */
48
+ onClose: (lastIndex: number) => void;
49
+ };
50
+
51
+ const CONTROLS_AUTO_HIDE_MS = 3000;
52
+
53
+ function formatTime(seconds: number): string {
54
+ if (!Number.isFinite(seconds) || seconds < 0) return '0:00';
55
+ const total = Math.floor(seconds);
56
+ const mins = Math.floor(total / 60);
57
+ const secs = total % 60;
58
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
59
+ }
60
+
61
+ export function MediaViewer({
62
+ visible,
63
+ items,
64
+ initialIndex,
65
+ onClose,
66
+ }: MediaViewerProps) {
67
+ const { styles } = useStyles();
68
+ const { width } = useWindowDimensions();
69
+ const listRef = useRef<FlatList<MediaViewerItem>>(null);
70
+ const videoRef = useRef<VideoRef | null>(null);
71
+ const [current, setCurrent] = useState(initialIndex);
72
+ // Mute-carry (PDT-4309 / PDT-4312): the first video plays unmuted; each swipe
73
+ // defaults the next video to muted — until the user unmutes, after which
74
+ // subsequent videos stay unmuted.
75
+ const [muted, setMuted] = useState(false);
76
+ const keepUnmutedRef = useRef(false);
77
+
78
+ // --- playback controls (ported from web VideoPlayerControls) ---
79
+ const [userPaused, setUserPaused] = useState(false);
80
+ const [progress, setProgress] = useState(0);
81
+ const [duration, setDuration] = useState(0);
82
+ const [scrubbing, setScrubbing] = useState(false);
83
+ const [controlsVisible, setControlsVisible] = useState(true);
84
+ const [trackWidth, setTrackWidth] = useState(0);
85
+ const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
86
+
87
+ const clearHideTimer = useCallback(() => {
88
+ if (hideTimer.current) {
89
+ clearTimeout(hideTimer.current);
90
+ hideTimer.current = null;
91
+ }
92
+ }, []);
93
+
94
+ const scheduleHide = useCallback(() => {
95
+ clearHideTimer();
96
+ hideTimer.current = setTimeout(
97
+ () => setControlsVisible(false),
98
+ CONTROLS_AUTO_HIDE_MS
99
+ );
100
+ }, [clearHideTimer]);
101
+
102
+ const revealControls = useCallback(() => {
103
+ setControlsVisible(true);
104
+ scheduleHide();
105
+ }, [scheduleHide]);
106
+
107
+ const resetPlayback = useCallback(() => {
108
+ setUserPaused(false);
109
+ setProgress(0);
110
+ setDuration(0);
111
+ setScrubbing(false);
112
+ }, []);
113
+
114
+ useEffect(() => {
115
+ if (!visible) {
116
+ clearHideTimer();
117
+ return;
118
+ }
119
+ setCurrent(initialIndex);
120
+ setMuted(false);
121
+ keepUnmutedRef.current = false;
122
+ resetPlayback();
123
+ revealControls();
124
+ }, [visible, initialIndex, resetPlayback, revealControls, clearHideTimer]);
125
+
126
+ const onMomentumScrollEnd = useCallback(
127
+ (e: NativeSyntheticEvent<NativeScrollEvent>) => {
128
+ if (!width) return;
129
+ const idx = Math.round(e.nativeEvent.contentOffset.x / width);
130
+ if (idx !== current) {
131
+ setCurrent(idx);
132
+ setMuted(!keepUnmutedRef.current);
133
+ resetPlayback();
134
+ revealControls();
135
+ }
136
+ },
137
+ [width, current, resetPlayback, revealControls]
138
+ );
139
+
140
+ const toggleMute = useCallback(() => {
141
+ setMuted((prev) => {
142
+ const next = !prev;
143
+ keepUnmutedRef.current = !next; // user unmuted -> keep unmuted afterwards
144
+ return next;
145
+ });
146
+ revealControls();
147
+ }, [revealControls]);
148
+
149
+ const togglePlay = useCallback(() => {
150
+ setUserPaused((p) => !p);
151
+ revealControls();
152
+ }, [revealControls]);
153
+
154
+ const onLoad = useCallback((data: OnLoadData) => {
155
+ setDuration(data.duration ?? 0);
156
+ }, []);
157
+
158
+ const onProgress = useCallback(
159
+ (data: OnProgressData) => {
160
+ if (!scrubbing) setProgress(data.currentTime);
161
+ },
162
+ [scrubbing]
163
+ );
164
+
165
+ const seekToX = useCallback(
166
+ (locationX: number) => {
167
+ if (trackWidth <= 0 || duration <= 0) return;
168
+ const ratio = Math.max(0, Math.min(1, locationX / trackWidth));
169
+ setProgress(ratio * duration);
170
+ },
171
+ [trackWidth, duration]
172
+ );
173
+
174
+ // Scrubber: a horizontal drag inside a horizontal pager. We claim the gesture
175
+ // and disable FlatList scroll while scrubbing so the two don't fight.
176
+ const panResponder = useMemo(
177
+ () =>
178
+ PanResponder.create({
179
+ onStartShouldSetPanResponder: () => true,
180
+ onMoveShouldSetPanResponder: () => true,
181
+ onPanResponderGrant: (e: GestureResponderEvent) => {
182
+ setScrubbing(true);
183
+ clearHideTimer();
184
+ setControlsVisible(true);
185
+ seekToX(e.nativeEvent.locationX);
186
+ },
187
+ onPanResponderMove: (e: GestureResponderEvent) => {
188
+ seekToX(e.nativeEvent.locationX);
189
+ },
190
+ onPanResponderRelease: (e: GestureResponderEvent) => {
191
+ const ratio =
192
+ trackWidth > 0
193
+ ? Math.max(0, Math.min(1, e.nativeEvent.locationX / trackWidth))
194
+ : 0;
195
+ const target = ratio * (duration || 0);
196
+ videoRef.current?.seek(target);
197
+ setProgress(target);
198
+ setScrubbing(false);
199
+ scheduleHide();
200
+ },
201
+ onPanResponderTerminate: () => {
202
+ setScrubbing(false);
203
+ scheduleHide();
204
+ },
205
+ }),
206
+ [seekToX, clearHideTimer, scheduleHide, trackWidth, duration]
207
+ );
208
+
209
+ const onTrackLayout = useCallback((e: LayoutChangeEvent) => {
210
+ setTrackWidth(e.nativeEvent.layout.width);
211
+ }, []);
212
+
213
+ // The control overlay must live INSIDE the page (a descendant of the pager),
214
+ // so horizontal drags on the tap-target are reclaimed by the FlatList's
215
+ // native scroll — a container-level sibling overlay would swallow swipes.
216
+ const renderControls = useCallback(() => {
217
+ const progressPct =
218
+ duration > 0 ? Math.max(0, Math.min(1, progress / duration)) : 0;
219
+ return (
220
+ <View style={StyleSheet.absoluteFill} pointerEvents="box-none">
221
+ {/* full-surface tap target: taps toggle controls; horizontal moves
222
+ fall through to the pager (Pressable cancels on move). */}
223
+ <Pressable
224
+ style={StyleSheet.absoluteFill}
225
+ onPress={() =>
226
+ controlsVisible ? setControlsVisible(false) : revealControls()
227
+ }
228
+ />
229
+ {controlsVisible && (
230
+ <>
231
+ {/* Centered play/pause */}
232
+ <View style={styles.centerControls} pointerEvents="box-none">
233
+ <TouchableOpacity style={styles.centerBtn} onPress={togglePlay}>
234
+ <SvgXml
235
+ xml={
236
+ userPaused
237
+ ? playControlIcon('#FFFFFF')
238
+ : pauseControlIcon('#FFFFFF')
239
+ }
240
+ width={24}
241
+ height={24}
242
+ />
243
+ </TouchableOpacity>
244
+ </View>
245
+
246
+ {/* Bottom bar: time + scrubbable progress */}
247
+ <View style={styles.controlBar} pointerEvents="box-none">
248
+ <Text style={styles.controlTime}>
249
+ {formatTime(progress)} / {formatTime(duration)}
250
+ </Text>
251
+ <View
252
+ style={styles.progressTrack}
253
+ onLayout={onTrackLayout}
254
+ {...panResponder.panHandlers}
255
+ >
256
+ <View style={styles.progressBg} />
257
+ <View
258
+ style={[
259
+ styles.progressFill,
260
+ { width: `${progressPct * 100}%` },
261
+ ]}
262
+ />
263
+ </View>
264
+ </View>
265
+ </>
266
+ )}
267
+ </View>
268
+ );
269
+ }, [
270
+ styles,
271
+ controlsVisible,
272
+ setControlsVisible,
273
+ revealControls,
274
+ togglePlay,
275
+ userPaused,
276
+ progress,
277
+ duration,
278
+ onTrackLayout,
279
+ panResponder,
280
+ ]);
281
+
282
+ const renderItem = useCallback(
283
+ ({ item, index }: { item: MediaViewerItem; index: number }) => (
284
+ <View style={[styles.page, { width }]}>
285
+ {item.type === 'video' ? (
286
+ <>
287
+ {/* pointerEvents none so horizontal swipes reach the pager instead
288
+ of being captured by the native video view. */}
289
+ <View style={styles.media} pointerEvents="none">
290
+ <Video
291
+ ref={index === current ? videoRef : undefined}
292
+ source={
293
+ item.videoType
294
+ ? { uri: item.uri, type: item.videoType }
295
+ : { uri: item.uri }
296
+ }
297
+ style={styles.media}
298
+ resizeMode="contain"
299
+ paused={index !== current || !visible || userPaused}
300
+ muted={muted && index === current}
301
+ repeat
302
+ playInBackground={false}
303
+ playWhenInactive={false}
304
+ onLoad={index === current ? onLoad : undefined}
305
+ onProgress={index === current ? onProgress : undefined}
306
+ />
307
+ </View>
308
+ {index === current && renderControls()}
309
+ </>
310
+ ) : (
311
+ <Image
312
+ source={{ uri: item.uri }}
313
+ style={styles.media as StyleProp<ImageStyle>}
314
+ resizeMode="contain"
315
+ />
316
+ )}
317
+ </View>
318
+ ),
319
+ [
320
+ styles,
321
+ width,
322
+ current,
323
+ muted,
324
+ visible,
325
+ userPaused,
326
+ onLoad,
327
+ onProgress,
328
+ renderControls,
329
+ ]
330
+ );
331
+
332
+ const currentIsVideo = items[current]?.type === 'video';
333
+
334
+ return (
335
+ <Modal
336
+ visible={visible}
337
+ transparent
338
+ animationType="fade"
339
+ onRequestClose={() => onClose(current)}
340
+ >
341
+ <View style={styles.container}>
342
+ <View style={styles.header}>
343
+ <TouchableOpacity
344
+ style={styles.headerBtn}
345
+ onPress={() => onClose(current)}
346
+ >
347
+ <SvgXml xml={clearIcon('white')} width={28} height={28} />
348
+ </TouchableOpacity>
349
+ <Text style={styles.counter}>
350
+ {current + 1}/{items.length}
351
+ </Text>
352
+ <View style={styles.headerBtn__right}>
353
+ {currentIsVideo && (
354
+ <TouchableOpacity onPress={toggleMute}>
355
+ <SvgXml
356
+ xml={muted ? soundOffIcon('#FFFFFF') : soundOnIcon('#FFFFFF')}
357
+ width={24}
358
+ height={24}
359
+ />
360
+ </TouchableOpacity>
361
+ )}
362
+ </View>
363
+ </View>
364
+ <FlatList
365
+ ref={listRef}
366
+ data={items}
367
+ horizontal
368
+ pagingEnabled
369
+ scrollEnabled={!scrubbing}
370
+ showsHorizontalScrollIndicator={false}
371
+ keyExtractor={(it, i) => `${it.uri}-${i}`}
372
+ renderItem={renderItem}
373
+ getItemLayout={(_, index) => ({
374
+ length: width,
375
+ offset: width * index,
376
+ index,
377
+ })}
378
+ initialScrollIndex={initialIndex}
379
+ onMomentumScrollEnd={onMomentumScrollEnd}
380
+ />
381
+ </View>
382
+ </Modal>
383
+ );
384
+ }
@@ -0,0 +1,2 @@
1
+ export { MediaViewer } from './MediaViewer';
2
+ export type { MediaViewerItem } from './MediaViewer';
@@ -0,0 +1,109 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import { useTheme } from 'react-native-paper';
3
+ import type { MyMD3Theme } from '../../../core/providers/AmityUIKitProvider';
4
+
5
+ export const useStyles = () => {
6
+ const theme = useTheme<MyMD3Theme>();
7
+
8
+ const styles = StyleSheet.create({
9
+ container: {
10
+ flex: 1,
11
+ backgroundColor: '#000000',
12
+ },
13
+ header: {
14
+ flexDirection: 'row',
15
+ alignItems: 'center',
16
+ justifyContent: 'space-between',
17
+ paddingHorizontal: 16,
18
+ height: 56,
19
+ zIndex: 2,
20
+ },
21
+ headerBtn: {
22
+ minWidth: 44,
23
+ height: 44,
24
+ alignItems: 'flex-start',
25
+ justifyContent: 'center',
26
+ },
27
+ headerBtn__right: {
28
+ minWidth: 44,
29
+ height: 44,
30
+ alignItems: 'flex-end',
31
+ justifyContent: 'center',
32
+ },
33
+ counter: {
34
+ color: '#FFFFFF',
35
+ fontSize: 14,
36
+ fontWeight: '600',
37
+ },
38
+ muteGlyph: {
39
+ fontSize: 20,
40
+ },
41
+ page: {
42
+ flex: 1,
43
+ alignItems: 'center',
44
+ justifyContent: 'center',
45
+ },
46
+ media: {
47
+ width: '100%',
48
+ height: '100%',
49
+ },
50
+ // --- video control bar (ported from web VideoPlayerControls) ---
51
+ controlBar: {
52
+ position: 'absolute',
53
+ left: 0,
54
+ right: 0,
55
+ bottom: 0,
56
+ paddingHorizontal: 16,
57
+ paddingBottom: 24,
58
+ paddingTop: 12,
59
+ zIndex: 3,
60
+ },
61
+ controlTime: {
62
+ color: '#FFFFFF',
63
+ fontSize: 12,
64
+ fontWeight: '600',
65
+ marginBottom: 8,
66
+ },
67
+ progressTrack: {
68
+ height: 24,
69
+ justifyContent: 'center',
70
+ },
71
+ progressBg: {
72
+ height: 4,
73
+ borderRadius: 2,
74
+ backgroundColor: 'rgba(255, 255, 255, 0.3)',
75
+ },
76
+ progressFill: {
77
+ position: 'absolute',
78
+ left: 0,
79
+ height: 4,
80
+ borderRadius: 2,
81
+ backgroundColor: '#FFFFFF',
82
+ },
83
+ // Centered play/pause + skip row (web .mobileControlsRow: top/left 50%,
84
+ // translate -50%, gap 2rem).
85
+ centerControls: {
86
+ position: 'absolute',
87
+ top: 0,
88
+ left: 0,
89
+ right: 0,
90
+ bottom: 0,
91
+ flexDirection: 'row',
92
+ alignItems: 'center',
93
+ justifyContent: 'center',
94
+ },
95
+ // Circular translucent-black button (web .playButton/.skipButton: 2.5rem,
96
+ // border-radius 50%, transparent-black bg). 32px gap between buttons.
97
+ centerBtn: {
98
+ width: 40,
99
+ height: 40,
100
+ borderRadius: 20,
101
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
102
+ alignItems: 'center',
103
+ justifyContent: 'center',
104
+ marginHorizontal: 16,
105
+ },
106
+ });
107
+
108
+ return { styles, theme };
109
+ };