@amityco/react-native-social-uikit 4.1.0 → 4.1.1-21d3e899.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 (167) hide show
  1. package/lib/commonjs/core/assets/icons/xml.js +43 -1
  2. package/lib/commonjs/core/assets/icons/xml.js.map +1 -1
  3. package/lib/commonjs/social/components/LoadingImage/index.js +4 -3
  4. package/lib/commonjs/social/components/LoadingImage/index.js.map +1 -1
  5. package/lib/commonjs/social/components/LoadingImage/styles.js +4 -0
  6. package/lib/commonjs/social/components/LoadingImage/styles.js.map +1 -1
  7. package/lib/commonjs/social/components/LoadingVideo/index.js +12 -6
  8. package/lib/commonjs/social/components/LoadingVideo/index.js.map +1 -1
  9. package/lib/commonjs/social/components/LoadingVideo/styles.js +4 -0
  10. package/lib/commonjs/social/components/LoadingVideo/styles.js.map +1 -1
  11. package/lib/commonjs/social/components/MediaSelector/MediaSelector.js +164 -0
  12. package/lib/commonjs/social/components/MediaSelector/MediaSelector.js.map +1 -0
  13. package/lib/commonjs/social/components/MediaSelector/index.js +13 -0
  14. package/lib/commonjs/social/components/MediaSelector/index.js.map +1 -0
  15. package/lib/commonjs/social/components/MediaSelector/styles.js +130 -0
  16. package/lib/commonjs/social/components/MediaSelector/styles.js.map +1 -0
  17. package/lib/commonjs/social/components/MediaViewer/MediaViewer.js +134 -0
  18. package/lib/commonjs/social/components/MediaViewer/MediaViewer.js.map +1 -0
  19. package/lib/commonjs/social/components/MediaViewer/index.js +13 -0
  20. package/lib/commonjs/social/components/MediaViewer/index.js.map +1 -0
  21. package/lib/commonjs/social/components/MediaViewer/styles.js +60 -0
  22. package/lib/commonjs/social/components/MediaViewer/styles.js.map +1 -0
  23. package/lib/commonjs/social/components/PostContent/index.js +75 -120
  24. package/lib/commonjs/social/components/PostContent/index.js.map +1 -1
  25. package/lib/commonjs/social/components/SelectedMediaComponent/SelectedMediaComponent.js +147 -0
  26. package/lib/commonjs/social/components/SelectedMediaComponent/SelectedMediaComponent.js.map +1 -0
  27. package/lib/commonjs/social/components/SelectedMediaComponent/index.js +13 -0
  28. package/lib/commonjs/social/components/SelectedMediaComponent/index.js.map +1 -0
  29. package/lib/commonjs/social/components/SelectedMediaComponent/styles.js +43 -0
  30. package/lib/commonjs/social/components/SelectedMediaComponent/styles.js.map +1 -0
  31. package/lib/commonjs/social/constants/index.js +11 -0
  32. package/lib/commonjs/social/constants/index.js.map +1 -0
  33. package/lib/commonjs/social/elements/PostMediaElement/PostMediaElement.js +244 -0
  34. package/lib/commonjs/social/elements/PostMediaElement/PostMediaElement.js.map +1 -0
  35. package/lib/commonjs/social/elements/PostMediaElement/index.js +13 -0
  36. package/lib/commonjs/social/elements/PostMediaElement/index.js.map +1 -0
  37. package/lib/commonjs/social/elements/PostMediaElement/styles.js +117 -0
  38. package/lib/commonjs/social/elements/PostMediaElement/styles.js.map +1 -0
  39. package/lib/commonjs/social/features/post/Composer/PostComposer.js +57 -91
  40. package/lib/commonjs/social/features/post/Composer/PostComposer.js.map +1 -1
  41. package/lib/commonjs/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.js +17 -7
  42. package/lib/commonjs/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.js.map +1 -1
  43. package/lib/commonjs/social/features/post/components/MediaAttachment/MediaAttachment.js +17 -7
  44. package/lib/commonjs/social/features/post/components/MediaAttachment/MediaAttachment.js.map +1 -1
  45. package/lib/commonjs/social/utils/getFrameRatio.js +52 -0
  46. package/lib/commonjs/social/utils/getFrameRatio.js.map +1 -0
  47. package/lib/commonjs/social/utils/getIndicatorConfig.js +59 -0
  48. package/lib/commonjs/social/utils/getIndicatorConfig.js.map +1 -0
  49. package/lib/module/core/assets/icons/xml.js +38 -0
  50. package/lib/module/core/assets/icons/xml.js.map +1 -1
  51. package/lib/module/social/components/LoadingImage/index.js +4 -3
  52. package/lib/module/social/components/LoadingImage/index.js.map +1 -1
  53. package/lib/module/social/components/LoadingImage/styles.js +4 -0
  54. package/lib/module/social/components/LoadingImage/styles.js.map +1 -1
  55. package/lib/module/social/components/LoadingVideo/index.js +13 -7
  56. package/lib/module/social/components/LoadingVideo/index.js.map +1 -1
  57. package/lib/module/social/components/LoadingVideo/styles.js +4 -0
  58. package/lib/module/social/components/LoadingVideo/styles.js.map +1 -1
  59. package/lib/module/social/components/MediaSelector/MediaSelector.js +158 -0
  60. package/lib/module/social/components/MediaSelector/MediaSelector.js.map +1 -0
  61. package/lib/module/social/components/MediaSelector/index.js +2 -0
  62. package/lib/module/social/components/MediaSelector/index.js.map +1 -0
  63. package/lib/module/social/components/MediaSelector/styles.js +123 -0
  64. package/lib/module/social/components/MediaSelector/styles.js.map +1 -0
  65. package/lib/module/social/components/MediaViewer/MediaViewer.js +127 -0
  66. package/lib/module/social/components/MediaViewer/MediaViewer.js.map +1 -0
  67. package/lib/module/social/components/MediaViewer/index.js +2 -0
  68. package/lib/module/social/components/MediaViewer/index.js.map +1 -0
  69. package/lib/module/social/components/MediaViewer/styles.js +53 -0
  70. package/lib/module/social/components/MediaViewer/styles.js.map +1 -0
  71. package/lib/module/social/components/PostContent/index.js +77 -122
  72. package/lib/module/social/components/PostContent/index.js.map +1 -1
  73. package/lib/module/social/components/SelectedMediaComponent/SelectedMediaComponent.js +141 -0
  74. package/lib/module/social/components/SelectedMediaComponent/SelectedMediaComponent.js.map +1 -0
  75. package/lib/module/social/components/SelectedMediaComponent/index.js +2 -0
  76. package/lib/module/social/components/SelectedMediaComponent/index.js.map +1 -0
  77. package/lib/module/social/components/SelectedMediaComponent/styles.js +36 -0
  78. package/lib/module/social/components/SelectedMediaComponent/styles.js.map +1 -0
  79. package/lib/module/social/constants/index.js +5 -0
  80. package/lib/module/social/constants/index.js.map +1 -0
  81. package/lib/module/social/elements/PostMediaElement/PostMediaElement.js +237 -0
  82. package/lib/module/social/elements/PostMediaElement/PostMediaElement.js.map +1 -0
  83. package/lib/module/social/elements/PostMediaElement/index.js +2 -0
  84. package/lib/module/social/elements/PostMediaElement/index.js.map +1 -0
  85. package/lib/module/social/elements/PostMediaElement/styles.js +110 -0
  86. package/lib/module/social/elements/PostMediaElement/styles.js.map +1 -0
  87. package/lib/module/social/features/post/Composer/PostComposer.js +59 -93
  88. package/lib/module/social/features/post/Composer/PostComposer.js.map +1 -1
  89. package/lib/module/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.js +17 -7
  90. package/lib/module/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.js.map +1 -1
  91. package/lib/module/social/features/post/components/MediaAttachment/MediaAttachment.js +17 -7
  92. package/lib/module/social/features/post/components/MediaAttachment/MediaAttachment.js.map +1 -1
  93. package/lib/module/social/utils/getFrameRatio.js +44 -0
  94. package/lib/module/social/utils/getFrameRatio.js.map +1 -0
  95. package/lib/module/social/utils/getIndicatorConfig.js +53 -0
  96. package/lib/module/social/utils/getIndicatorConfig.js.map +1 -0
  97. package/lib/typescript/core/assets/icons/xml.d.ts +5 -0
  98. package/lib/typescript/core/assets/icons/xml.d.ts.map +1 -1
  99. package/lib/typescript/social/components/LoadingImage/index.d.ts +2 -1
  100. package/lib/typescript/social/components/LoadingImage/index.d.ts.map +1 -1
  101. package/lib/typescript/social/components/LoadingImage/styles.d.ts +4 -0
  102. package/lib/typescript/social/components/LoadingImage/styles.d.ts.map +1 -1
  103. package/lib/typescript/social/components/LoadingVideo/index.d.ts +2 -1
  104. package/lib/typescript/social/components/LoadingVideo/index.d.ts.map +1 -1
  105. package/lib/typescript/social/components/LoadingVideo/styles.d.ts +4 -0
  106. package/lib/typescript/social/components/LoadingVideo/styles.d.ts.map +1 -1
  107. package/lib/typescript/social/components/MediaSelector/MediaSelector.d.ts +14 -0
  108. package/lib/typescript/social/components/MediaSelector/MediaSelector.d.ts.map +1 -0
  109. package/lib/typescript/social/components/MediaSelector/index.d.ts +2 -0
  110. package/lib/typescript/social/components/MediaSelector/index.d.ts.map +1 -0
  111. package/lib/typescript/social/components/MediaSelector/styles.d.ts +116 -0
  112. package/lib/typescript/social/components/MediaSelector/styles.d.ts.map +1 -0
  113. package/lib/typescript/social/components/MediaViewer/MediaViewer.d.ts +17 -0
  114. package/lib/typescript/social/components/MediaViewer/MediaViewer.d.ts.map +1 -0
  115. package/lib/typescript/social/components/MediaViewer/index.d.ts +3 -0
  116. package/lib/typescript/social/components/MediaViewer/index.d.ts.map +1 -0
  117. package/lib/typescript/social/components/MediaViewer/styles.d.ts +48 -0
  118. package/lib/typescript/social/components/MediaViewer/styles.d.ts.map +1 -0
  119. package/lib/typescript/social/components/PostContent/index.d.ts.map +1 -1
  120. package/lib/typescript/social/components/SelectedMediaComponent/SelectedMediaComponent.d.ts +12 -0
  121. package/lib/typescript/social/components/SelectedMediaComponent/SelectedMediaComponent.d.ts.map +1 -0
  122. package/lib/typescript/social/components/SelectedMediaComponent/index.d.ts +3 -0
  123. package/lib/typescript/social/components/SelectedMediaComponent/index.d.ts.map +1 -0
  124. package/lib/typescript/social/components/SelectedMediaComponent/styles.d.ts +25 -0
  125. package/lib/typescript/social/components/SelectedMediaComponent/styles.d.ts.map +1 -0
  126. package/lib/typescript/social/constants/index.d.ts +4 -0
  127. package/lib/typescript/social/constants/index.d.ts.map +1 -0
  128. package/lib/typescript/social/elements/PostMediaElement/PostMediaElement.d.ts +15 -0
  129. package/lib/typescript/social/elements/PostMediaElement/PostMediaElement.d.ts.map +1 -0
  130. package/lib/typescript/social/elements/PostMediaElement/index.d.ts +3 -0
  131. package/lib/typescript/social/elements/PostMediaElement/index.d.ts.map +1 -0
  132. package/lib/typescript/social/elements/PostMediaElement/styles.d.ts +88 -0
  133. package/lib/typescript/social/elements/PostMediaElement/styles.d.ts.map +1 -0
  134. package/lib/typescript/social/features/post/Composer/PostComposer.d.ts.map +1 -1
  135. package/lib/typescript/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.d.ts +3 -0
  136. package/lib/typescript/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.d.ts.map +1 -1
  137. package/lib/typescript/social/features/post/components/MediaAttachment/MediaAttachment.d.ts +3 -0
  138. package/lib/typescript/social/features/post/components/MediaAttachment/MediaAttachment.d.ts.map +1 -1
  139. package/lib/typescript/social/utils/getFrameRatio.d.ts +21 -0
  140. package/lib/typescript/social/utils/getFrameRatio.d.ts.map +1 -0
  141. package/lib/typescript/social/utils/getIndicatorConfig.d.ts +11 -0
  142. package/lib/typescript/social/utils/getIndicatorConfig.d.ts.map +1 -0
  143. package/package.json +5 -3
  144. package/src/core/assets/icons/xml.ts +46 -0
  145. package/src/social/components/LoadingImage/index.tsx +12 -2
  146. package/src/social/components/LoadingImage/styles.ts +4 -0
  147. package/src/social/components/LoadingVideo/index.tsx +28 -4
  148. package/src/social/components/LoadingVideo/styles.ts +4 -0
  149. package/src/social/components/MediaSelector/MediaSelector.tsx +231 -0
  150. package/src/social/components/MediaSelector/index.tsx +1 -0
  151. package/src/social/components/MediaSelector/styles.ts +124 -0
  152. package/src/social/components/MediaViewer/MediaViewer.tsx +168 -0
  153. package/src/social/components/MediaViewer/index.tsx +2 -0
  154. package/src/social/components/MediaViewer/styles.ts +53 -0
  155. package/src/social/components/PostContent/index.tsx +86 -159
  156. package/src/social/components/SelectedMediaComponent/SelectedMediaComponent.tsx +202 -0
  157. package/src/social/components/SelectedMediaComponent/index.ts +2 -0
  158. package/src/social/components/SelectedMediaComponent/styles.ts +38 -0
  159. package/src/social/constants/index.ts +4 -0
  160. package/src/social/elements/PostMediaElement/PostMediaElement.tsx +318 -0
  161. package/src/social/elements/PostMediaElement/index.tsx +5 -0
  162. package/src/social/elements/PostMediaElement/styles.ts +99 -0
  163. package/src/social/features/post/Composer/PostComposer.tsx +70 -96
  164. package/src/social/features/post/components/DetailedMediaAttachment/DetailedMediaAttachment.tsx +10 -3
  165. package/src/social/features/post/components/MediaAttachment/MediaAttachment.tsx +14 -3
  166. package/src/social/utils/getFrameRatio.ts +42 -0
  167. package/src/social/utils/getIndicatorConfig.ts +62 -0
@@ -9,7 +9,12 @@ import {
9
9
  import * as Progress from 'react-native-progress';
10
10
  import { SvgXml } from 'react-native-svg';
11
11
  import { deleteAmityFile, uploadVideoFile } from '../../../core/legacy/file';
12
- import { closeIcon, playBtn, toastIcon } from '../../../core/assets/icons/xml';
12
+ import {
13
+ closeIcon,
14
+ playBtn,
15
+ toastIcon,
16
+ videoControlIcon,
17
+ } from '../../../core/assets/icons/xml';
13
18
  import { useStyles } from './styles';
14
19
  import Video from 'react-native-video';
15
20
  import { useNavigation } from '@react-navigation/native';
@@ -39,6 +44,7 @@ interface OverlayImageProps {
39
44
  fileCount?: number;
40
45
  postId?: string;
41
46
  setIsUploading?: (arg: boolean) => void;
47
+ carousel?: boolean;
42
48
  }
43
49
  const LoadingVideo = ({
44
50
  source,
@@ -54,6 +60,7 @@ const LoadingVideo = ({
54
60
  fileCount,
55
61
  postId,
56
62
  setIsUploading,
63
+ carousel = false,
57
64
  }: OverlayImageProps) => {
58
65
  const dispatch = useUIKitDispatch();
59
66
  const { showToastMessage } = uiSlice.actions;
@@ -157,8 +164,13 @@ const LoadingVideo = ({
157
164
 
158
165
  const handleOnPlay = () => {
159
166
  setIsPause(!isPause);
167
+ // In carousel mode the parent (onPlay) opens the full-screen media viewer,
168
+ // so skip the default single-video route navigation.
169
+ if (onPlay) {
170
+ onPlay(source);
171
+ return;
172
+ }
160
173
  playVideoFullScreen(source);
161
- onPlay && onPlay(source);
162
174
  };
163
175
 
164
176
  const onRetryUpload = () => {
@@ -166,10 +178,22 @@ const LoadingVideo = ({
166
178
  };
167
179
 
168
180
  return (
169
- <View style={fileCount >= 3 ? styles.image3XContainer : styles.container}>
181
+ <View
182
+ style={
183
+ carousel
184
+ ? styles.carouselContainer
185
+ : fileCount >= 3
186
+ ? styles.image3XContainer
187
+ : styles.container
188
+ }
189
+ >
170
190
  {!loading && !isUploadError && isPause && (
171
191
  <TouchableOpacity style={styles.playButton} onPress={handleOnPlay}>
172
- <SvgXml xml={playBtn} width="50" height="50" />
192
+ <SvgXml
193
+ xml={carousel ? videoControlIcon : playBtn}
194
+ width={carousel ? 40 : 50}
195
+ height={carousel ? 40 : 50}
196
+ />
173
197
  </TouchableOpacity>
174
198
  )}
175
199
  {playingUri && !isPause ? (
@@ -13,6 +13,10 @@ export const useStyles = () => {
13
13
  height: width / 3 - 16,
14
14
  margin: 3,
15
15
  },
16
+ carouselContainer: {
17
+ width: '100%',
18
+ height: '100%',
19
+ },
16
20
  image: {
17
21
  width: '100%',
18
22
  height: '100%',
@@ -0,0 +1,231 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import {
3
+ FlatList,
4
+ Image,
5
+ Modal,
6
+ PermissionsAndroid,
7
+ Platform,
8
+ Text,
9
+ TouchableOpacity,
10
+ TouchableWithoutFeedback,
11
+ View,
12
+ } from 'react-native';
13
+ import {
14
+ CameraRoll,
15
+ type PhotoIdentifier,
16
+ } from '@react-native-camera-roll/camera-roll';
17
+ import { useStyles } from './styles';
18
+
19
+ type MediaSelectorProps = {
20
+ visible: boolean;
21
+ mediaType: 'photo' | 'video';
22
+ /** URIs already added to the composer — used for silent duplicate detection. */
23
+ existing: string[];
24
+ /** How many more items may be added (10 - current count). */
25
+ remaining: number;
26
+ onClose: () => void;
27
+ /** New, de-duplicated URIs in the order they were selected. */
28
+ onConfirm: (uris: string[]) => void;
29
+ };
30
+
31
+ const PAGE_SIZE = 90;
32
+
33
+ async function ensureLibraryPermission(
34
+ mediaType: 'photo' | 'video'
35
+ ): Promise<boolean> {
36
+ if (Platform.OS !== 'android') return true;
37
+ const version =
38
+ typeof Platform.Version === 'number'
39
+ ? Platform.Version
40
+ : parseInt(Platform.Version, 10);
41
+ const perms =
42
+ version >= 33
43
+ ? [
44
+ mediaType === 'photo'
45
+ ? PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES
46
+ : PermissionsAndroid.PERMISSIONS.READ_MEDIA_VIDEO,
47
+ ]
48
+ : [PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE];
49
+ try {
50
+ const result = await PermissionsAndroid.requestMultiple(perms);
51
+ return Object.values(result).every(
52
+ (status) => status === PermissionsAndroid.RESULTS.GRANTED
53
+ );
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ function formatDuration(seconds?: number): string {
60
+ if (!seconds || seconds <= 0) return '';
61
+ const total = Math.round(seconds);
62
+ const mins = Math.floor(total / 60);
63
+ const secs = total % 60;
64
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
65
+ }
66
+
67
+ export function MediaSelector({
68
+ visible,
69
+ mediaType,
70
+ existing,
71
+ remaining,
72
+ onClose,
73
+ onConfirm,
74
+ }: MediaSelectorProps) {
75
+ const { styles } = useStyles();
76
+ const [items, setItems] = useState<PhotoIdentifier[]>([]);
77
+ const [selected, setSelected] = useState<string[]>([]);
78
+ const [cursor, setCursor] = useState<string | undefined>(undefined);
79
+ const [hasNextPage, setHasNextPage] = useState(false);
80
+ const [loading, setLoading] = useState(false);
81
+
82
+ const loadPage = useCallback(
83
+ async (after?: string) => {
84
+ if (loading) return;
85
+ setLoading(true);
86
+ try {
87
+ const res = await CameraRoll.getPhotos({
88
+ first: PAGE_SIZE,
89
+ after,
90
+ assetType: mediaType === 'photo' ? 'Photos' : 'Videos',
91
+ include: ['filename', 'playableDuration'],
92
+ });
93
+ setItems((prev) => (after ? [...prev, ...res.edges] : res.edges));
94
+ setCursor(res.page_info.end_cursor);
95
+ setHasNextPage(res.page_info.has_next_page);
96
+ } catch {
97
+ // permission denied / empty library — leave the list empty
98
+ } finally {
99
+ setLoading(false);
100
+ }
101
+ },
102
+ [loading, mediaType]
103
+ );
104
+
105
+ // Reset and (re)load whenever the selector opens. A fresh open starts with an
106
+ // empty selection; previously-added items are handled by duplicate detection.
107
+ useEffect(() => {
108
+ if (!visible) return;
109
+ setSelected([]);
110
+ setItems([]);
111
+ setCursor(undefined);
112
+ setHasNextPage(false);
113
+ (async () => {
114
+ const granted = await ensureLibraryPermission(mediaType);
115
+ if (granted) loadPage();
116
+ })();
117
+ }, [visible, mediaType]);
118
+
119
+ const toggle = useCallback(
120
+ (uri: string) => {
121
+ setSelected((prev) => {
122
+ const index = prev.indexOf(uri);
123
+ if (index !== -1) {
124
+ // Deselect: removing from the array renumbers the rest automatically.
125
+ return prev.filter((u) => u !== uri);
126
+ }
127
+ if (prev.length >= remaining) return prev; // respect the cap silently
128
+ return [...prev, uri];
129
+ });
130
+ },
131
+ [remaining]
132
+ );
133
+
134
+ const handleConfirm = useCallback(() => {
135
+ const deduped = selected.filter((uri) => !existing.includes(uri));
136
+ onConfirm(deduped);
137
+ }, [selected, existing, onConfirm]);
138
+
139
+ const onEndReached = useCallback(() => {
140
+ if (hasNextPage && !loading) loadPage(cursor);
141
+ }, [hasNextPage, loading, cursor, loadPage]);
142
+
143
+ const renderItem = useCallback(
144
+ ({ item }: { item: PhotoIdentifier }) => {
145
+ const uri = item.node.image.uri;
146
+ const order = selected.indexOf(uri);
147
+ const isSelected = order !== -1;
148
+ const duration =
149
+ mediaType === 'video'
150
+ ? formatDuration(item.node.image.playableDuration)
151
+ : '';
152
+ return (
153
+ <TouchableWithoutFeedback onPress={() => toggle(uri)}>
154
+ <View style={styles.cell}>
155
+ <Image style={styles.cell__image} source={{ uri }} />
156
+ {!!duration && (
157
+ <Text style={styles.cell__videoDuration}>{duration}</Text>
158
+ )}
159
+ {isSelected ? (
160
+ <>
161
+ <View style={styles.cell__selectedOverlay} />
162
+ <View style={styles.cell__badge}>
163
+ <Text style={styles.cell__badgeLabel}>{order + 1}</Text>
164
+ </View>
165
+ </>
166
+ ) : (
167
+ <View style={styles.cell__badgeEmpty} />
168
+ )}
169
+ </View>
170
+ </TouchableWithoutFeedback>
171
+ );
172
+ },
173
+ [selected, mediaType, toggle, styles]
174
+ );
175
+
176
+ return (
177
+ <Modal
178
+ visible={visible}
179
+ animationType="slide"
180
+ onRequestClose={onClose}
181
+ presentationStyle="fullScreen"
182
+ >
183
+ <View style={styles.container}>
184
+ <View style={styles.header}>
185
+ <TouchableOpacity style={styles.headerButton} onPress={onClose}>
186
+ <Text style={styles.headerClose}>Cancel</Text>
187
+ </TouchableOpacity>
188
+ <Text style={styles.headerTitle}>
189
+ {mediaType === 'photo' ? 'Photos' : 'Videos'}
190
+ </Text>
191
+ <TouchableOpacity
192
+ style={styles.headerButton__right}
193
+ disabled={selected.length === 0}
194
+ onPress={handleConfirm}
195
+ >
196
+ <Text
197
+ style={
198
+ selected.length === 0
199
+ ? styles.headerAdd__disabled
200
+ : styles.headerAdd
201
+ }
202
+ >
203
+ {selected.length > 0 ? `Add (${selected.length})` : 'Add'}
204
+ </Text>
205
+ </TouchableOpacity>
206
+ </View>
207
+ <FlatList
208
+ data={items}
209
+ numColumns={3}
210
+ keyExtractor={(item) => item.node.image.uri}
211
+ renderItem={renderItem}
212
+ contentContainerStyle={styles.grid}
213
+ onEndReached={onEndReached}
214
+ onEndReachedThreshold={0.6}
215
+ initialNumToRender={30}
216
+ windowSize={5}
217
+ ListEmptyComponent={
218
+ !loading ? (
219
+ <View style={styles.empty}>
220
+ <Text style={styles.empty__text}>
221
+ No {mediaType === 'photo' ? 'photos' : 'videos'} found on this
222
+ device.
223
+ </Text>
224
+ </View>
225
+ ) : null
226
+ }
227
+ />
228
+ </View>
229
+ </Modal>
230
+ );
231
+ }
@@ -0,0 +1 @@
1
+ export { MediaSelector } from './MediaSelector';
@@ -0,0 +1,124 @@
1
+ import { StyleSheet, useWindowDimensions } from 'react-native';
2
+ import { useTheme } from 'react-native-paper';
3
+ import type { MyMD3Theme } from '../../../core/providers/AmityUIKitProvider';
4
+
5
+ const NUM_COLUMNS = 3;
6
+ const CELL_GAP = 2;
7
+
8
+ export const useStyles = () => {
9
+ const theme = useTheme<MyMD3Theme>();
10
+ const { width } = useWindowDimensions();
11
+ const cellSize = Math.floor(
12
+ (width - CELL_GAP * (NUM_COLUMNS - 1)) / NUM_COLUMNS
13
+ );
14
+
15
+ const styles = StyleSheet.create({
16
+ container: {
17
+ flex: 1,
18
+ backgroundColor: theme.colors.background,
19
+ },
20
+ header: {
21
+ flexDirection: 'row',
22
+ alignItems: 'center',
23
+ justifyContent: 'space-between',
24
+ paddingHorizontal: 16,
25
+ height: 56,
26
+ },
27
+ headerButton: {
28
+ minWidth: 64,
29
+ },
30
+ headerButton__right: {
31
+ minWidth: 64,
32
+ alignItems: 'flex-end',
33
+ },
34
+ headerTitle: {
35
+ color: theme.colors.base,
36
+ fontSize: 16,
37
+ fontWeight: '600',
38
+ },
39
+ headerClose: {
40
+ color: theme.colors.base,
41
+ fontSize: 16,
42
+ },
43
+ headerAdd: {
44
+ color: theme.colors.primary,
45
+ fontSize: 16,
46
+ fontWeight: '600',
47
+ },
48
+ headerAdd__disabled: {
49
+ color: theme.colors.baseShade3,
50
+ fontSize: 16,
51
+ fontWeight: '600',
52
+ },
53
+ grid: {
54
+ paddingBottom: 24,
55
+ },
56
+ cell: {
57
+ width: cellSize,
58
+ height: cellSize,
59
+ marginRight: CELL_GAP,
60
+ marginBottom: CELL_GAP,
61
+ backgroundColor: theme.colors.baseShade4,
62
+ },
63
+ cell__image: {
64
+ width: '100%',
65
+ height: '100%',
66
+ },
67
+ cell__selectedOverlay: {
68
+ ...StyleSheet.absoluteFillObject,
69
+ borderWidth: 2,
70
+ borderColor: theme.colors.primary,
71
+ },
72
+ cell__badge: {
73
+ position: 'absolute',
74
+ top: 6,
75
+ right: 6,
76
+ minWidth: 22,
77
+ height: 22,
78
+ borderRadius: 11,
79
+ paddingHorizontal: 5,
80
+ alignItems: 'center',
81
+ justifyContent: 'center',
82
+ backgroundColor: theme.colors.primary,
83
+ },
84
+ cell__badgeEmpty: {
85
+ position: 'absolute',
86
+ top: 6,
87
+ right: 6,
88
+ width: 22,
89
+ height: 22,
90
+ borderRadius: 11,
91
+ borderWidth: 1.5,
92
+ borderColor: '#FFFFFF',
93
+ backgroundColor: 'rgba(0,0,0,0.2)',
94
+ },
95
+ cell__badgeLabel: {
96
+ color: '#FFFFFF',
97
+ fontSize: 12,
98
+ fontWeight: '700',
99
+ },
100
+ cell__videoDuration: {
101
+ position: 'absolute',
102
+ bottom: 6,
103
+ right: 6,
104
+ color: '#FFFFFF',
105
+ fontSize: 11,
106
+ fontWeight: '600',
107
+ textShadowColor: 'rgba(0,0,0,0.6)',
108
+ textShadowRadius: 2,
109
+ },
110
+ empty: {
111
+ flex: 1,
112
+ alignItems: 'center',
113
+ justifyContent: 'center',
114
+ padding: 32,
115
+ },
116
+ empty__text: {
117
+ color: theme.colors.baseShade1,
118
+ fontSize: 14,
119
+ textAlign: 'center',
120
+ },
121
+ });
122
+
123
+ return { styles, theme };
124
+ };
@@ -0,0 +1,168 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import {
3
+ FlatList,
4
+ Image,
5
+ ImageStyle,
6
+ Modal,
7
+ NativeScrollEvent,
8
+ NativeSyntheticEvent,
9
+ StyleProp,
10
+ Text,
11
+ TouchableOpacity,
12
+ useWindowDimensions,
13
+ View,
14
+ } from 'react-native';
15
+ import Video from 'react-native-video';
16
+ import { SvgXml } from 'react-native-svg';
17
+ import {
18
+ clearIcon,
19
+ soundOnIcon,
20
+ soundOffIcon,
21
+ } from '../../../core/assets/icons/xml';
22
+ import { useStyles } from './styles';
23
+
24
+ export type MediaViewerItem = {
25
+ type: 'image' | 'video';
26
+ uri: string;
27
+ /** e.g. 'm3u8' for HLS (published) videos; omit for local files. */
28
+ videoType?: string;
29
+ };
30
+
31
+ type MediaViewerProps = {
32
+ visible: boolean;
33
+ items: MediaViewerItem[];
34
+ initialIndex: number;
35
+ /** Called with the last-viewed index so the caller can decide where to
36
+ * return (published: last-viewed; composer: originally-tapped). */
37
+ onClose: (lastIndex: number) => void;
38
+ };
39
+
40
+ export function MediaViewer({
41
+ visible,
42
+ items,
43
+ initialIndex,
44
+ onClose,
45
+ }: MediaViewerProps) {
46
+ const { styles } = useStyles();
47
+ const { width } = useWindowDimensions();
48
+ const listRef = useRef<FlatList<MediaViewerItem>>(null);
49
+ const [current, setCurrent] = useState(initialIndex);
50
+ // Mute-carry (PDT-4309 / PDT-4312): the first video plays unmuted; each swipe
51
+ // defaults the next video to muted — until the user unmutes, after which
52
+ // subsequent videos stay unmuted.
53
+ const [muted, setMuted] = useState(false);
54
+ const keepUnmutedRef = useRef(false);
55
+
56
+ useEffect(() => {
57
+ if (!visible) return;
58
+ setCurrent(initialIndex);
59
+ setMuted(false);
60
+ keepUnmutedRef.current = false;
61
+ }, [visible, initialIndex]);
62
+
63
+ const onMomentumScrollEnd = useCallback(
64
+ (e: NativeSyntheticEvent<NativeScrollEvent>) => {
65
+ if (!width) return;
66
+ const idx = Math.round(e.nativeEvent.contentOffset.x / width);
67
+ if (idx !== current) {
68
+ setCurrent(idx);
69
+ setMuted(!keepUnmutedRef.current);
70
+ }
71
+ },
72
+ [width, current]
73
+ );
74
+
75
+ const toggleMute = useCallback(() => {
76
+ setMuted((prev) => {
77
+ const next = !prev;
78
+ keepUnmutedRef.current = !next; // user unmuted -> keep unmuted afterwards
79
+ return next;
80
+ });
81
+ }, []);
82
+
83
+ const renderItem = useCallback(
84
+ ({ item, index }: { item: MediaViewerItem; index: number }) => (
85
+ <View style={[styles.page, { width }]}>
86
+ {item.type === 'video' ? (
87
+ // pointerEvents none so horizontal swipes reach the pager instead of
88
+ // being captured by the native video view.
89
+ <View style={styles.media} pointerEvents="none">
90
+ <Video
91
+ source={
92
+ item.videoType
93
+ ? { uri: item.uri, type: item.videoType }
94
+ : { uri: item.uri }
95
+ }
96
+ style={styles.media}
97
+ resizeMode="contain"
98
+ paused={index !== current || !visible}
99
+ muted={muted && index === current}
100
+ repeat
101
+ playInBackground={false}
102
+ playWhenInactive={false}
103
+ />
104
+ </View>
105
+ ) : (
106
+ <Image
107
+ source={{ uri: item.uri }}
108
+ style={styles.media as StyleProp<ImageStyle>}
109
+ resizeMode="contain"
110
+ />
111
+ )}
112
+ </View>
113
+ ),
114
+ [styles, width, current, muted, visible]
115
+ );
116
+
117
+ const currentIsVideo = items[current]?.type === 'video';
118
+
119
+ return (
120
+ <Modal
121
+ visible={visible}
122
+ transparent
123
+ animationType="fade"
124
+ onRequestClose={() => onClose(current)}
125
+ >
126
+ <View style={styles.container}>
127
+ <View style={styles.header}>
128
+ <TouchableOpacity
129
+ style={styles.headerBtn}
130
+ onPress={() => onClose(current)}
131
+ >
132
+ <SvgXml xml={clearIcon('white')} width={28} height={28} />
133
+ </TouchableOpacity>
134
+ <Text style={styles.counter}>
135
+ {current + 1}/{items.length}
136
+ </Text>
137
+ <View style={styles.headerBtn__right}>
138
+ {currentIsVideo && (
139
+ <TouchableOpacity onPress={toggleMute}>
140
+ <SvgXml
141
+ xml={muted ? soundOffIcon('#FFFFFF') : soundOnIcon('#FFFFFF')}
142
+ width={24}
143
+ height={24}
144
+ />
145
+ </TouchableOpacity>
146
+ )}
147
+ </View>
148
+ </View>
149
+ <FlatList
150
+ ref={listRef}
151
+ data={items}
152
+ horizontal
153
+ pagingEnabled
154
+ showsHorizontalScrollIndicator={false}
155
+ keyExtractor={(it, i) => `${it.uri}-${i}`}
156
+ renderItem={renderItem}
157
+ getItemLayout={(_, index) => ({
158
+ length: width,
159
+ offset: width * index,
160
+ index,
161
+ })}
162
+ initialScrollIndex={initialIndex}
163
+ onMomentumScrollEnd={onMomentumScrollEnd}
164
+ />
165
+ </View>
166
+ </Modal>
167
+ );
168
+ }
@@ -0,0 +1,2 @@
1
+ export { MediaViewer } from './MediaViewer';
2
+ export type { MediaViewerItem } from './MediaViewer';
@@ -0,0 +1,53 @@
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
+ });
51
+
52
+ return { styles, theme };
53
+ };