@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
@@ -0,0 +1,318 @@
1
+ import { useMemo, useRef, useState, type MutableRefObject } from 'react';
2
+ import {
3
+ Image,
4
+ ImageStyle,
5
+ LayoutChangeEvent,
6
+ NativeScrollEvent,
7
+ NativeSyntheticEvent,
8
+ Pressable,
9
+ ScrollView,
10
+ StyleProp,
11
+ useWindowDimensions,
12
+ View,
13
+ } from 'react-native';
14
+ import { SvgXml } from 'react-native-svg';
15
+ import { Typography } from '../../../core/components/Typography/Typography';
16
+ import useAuth from '../../../core/hooks/useAuth';
17
+ import {
18
+ videoControlIcon,
19
+ brokenImageIcon,
20
+ } from '../../../core/assets/icons/xml';
21
+ import {
22
+ getFrameRatio,
23
+ getVideoDisplayDims,
24
+ FRAME_RATIO_VALUE,
25
+ DEFAULT_FRAME_RATIO,
26
+ type FrameRatio,
27
+ } from '../../utils/getFrameRatio';
28
+ import { getIndicatorConfig } from '../../utils/getIndicatorConfig';
29
+ import { useStyles } from './styles';
30
+
31
+ type MediaPost = Amity.Post<'image'> | Amity.Post<'video'>;
32
+
33
+ function isMediaPost(post: Amity.Post): post is MediaPost {
34
+ return post.dataType === 'image' || post.dataType === 'video';
35
+ }
36
+
37
+ function getAttachmentRatio(post?: MediaPost): FrameRatio {
38
+ if (!post) return DEFAULT_FRAME_RATIO;
39
+
40
+ if (post.dataType === 'image') {
41
+ const image = post.getImageInfo();
42
+ return getFrameRatio(image?.getWidth(), image?.getHeight());
43
+ }
44
+
45
+ const video = post.getVideoInfo();
46
+ const { width, height } = getVideoDisplayDims({
47
+ width: video?.getWidth(),
48
+ height: video?.getHeight(),
49
+ rotation: video?.getRotation(),
50
+ });
51
+ return getFrameRatio(width, height);
52
+ }
53
+
54
+ export type PostMediaControls = {
55
+ slideTo: (index: number) => void;
56
+ };
57
+
58
+ export type PostMediaElementProps = {
59
+ posts: Amity.Post[];
60
+ onImageClick: (index: number) => void;
61
+ onVideoClick: (index: number) => void;
62
+ controlsRef?: MutableRefObject<PostMediaControls | null>;
63
+ /** While the post's media children are still being fetched, show a
64
+ * placeholder frame instead of nothing (avoids a blank → pop). */
65
+ loading?: boolean;
66
+ };
67
+
68
+ export function PostMediaElement({
69
+ posts,
70
+ onImageClick,
71
+ onVideoClick,
72
+ controlsRef,
73
+ loading = false,
74
+ }: PostMediaElementProps) {
75
+ const styles = useStyles();
76
+ const { width: screenWidth } = useWindowDimensions();
77
+ const [current, setCurrent] = useState(0);
78
+ const [trackWidth, setTrackWidth] = useState(0);
79
+ const scrollRef = useRef<ScrollView>(null);
80
+
81
+ const mediaPosts = useMemo(() => (posts ?? []).filter(isMediaPost), [posts]);
82
+ const ratio = useMemo(() => getAttachmentRatio(mediaPosts[0]), [mediaPosts]);
83
+
84
+ if (mediaPosts.length === 0) {
85
+ if (!loading) return null;
86
+ // Loading placeholder: a full-bleed frame in the base-shade4 colour, so the
87
+ // media area doesn't flash blank before the child posts resolve.
88
+ return (
89
+ <View style={styles.postMedia}>
90
+ <View style={styles.postMedia__track}>
91
+ <View
92
+ style={[
93
+ styles.postMedia__frame,
94
+ { aspectRatio: FRAME_RATIO_VALUE[DEFAULT_FRAME_RATIO] },
95
+ ]}
96
+ />
97
+ </View>
98
+ </View>
99
+ );
100
+ }
101
+
102
+ const total = mediaPosts.length;
103
+ const isCarousel = total > 1;
104
+ const aspectRatio = FRAME_RATIO_VALUE[ratio];
105
+ // The carousel is full-bleed (screen width); use that immediately so the
106
+ // ScrollView renders without waiting for onLayout — otherwise there is an
107
+ // empty block between the placeholder and the first frame.
108
+ const slideWidth = trackWidth || screenWidth;
109
+
110
+ const handleFrameClick = (index: number) => {
111
+ const post = mediaPosts[index];
112
+ if (post.dataType === 'image') onImageClick(index);
113
+ else onVideoClick(index);
114
+ };
115
+
116
+ const onLayout = (event: LayoutChangeEvent) => {
117
+ const width = event.nativeEvent.layout.width;
118
+ if (width && width !== trackWidth) setTrackWidth(width);
119
+ };
120
+
121
+ const onMomentumScrollEnd = (
122
+ event: NativeSyntheticEvent<NativeScrollEvent>
123
+ ) => {
124
+ if (!slideWidth) return;
125
+ const nextIndex = Math.round(
126
+ event.nativeEvent.contentOffset.x / slideWidth
127
+ );
128
+ if (nextIndex !== current) setCurrent(nextIndex);
129
+ };
130
+
131
+ if (controlsRef) {
132
+ controlsRef.current = {
133
+ slideTo: (index: number) => {
134
+ scrollRef.current?.scrollTo({
135
+ x: index * slideWidth,
136
+ animated: false,
137
+ });
138
+ setCurrent(index);
139
+ },
140
+ };
141
+ }
142
+
143
+ if (!isCarousel) {
144
+ return (
145
+ <View style={styles.postMedia}>
146
+ <View style={styles.postMedia__track} onLayout={onLayout}>
147
+ <MediaFrame
148
+ post={mediaPosts[0]}
149
+ aspectRatio={aspectRatio}
150
+ onPress={() => handleFrameClick(0)}
151
+ />
152
+ </View>
153
+ </View>
154
+ );
155
+ }
156
+
157
+ return (
158
+ <View style={styles.postMedia}>
159
+ <View style={styles.postMedia__track} onLayout={onLayout}>
160
+ <ScrollView
161
+ ref={scrollRef}
162
+ horizontal
163
+ pagingEnabled
164
+ showsHorizontalScrollIndicator={false}
165
+ onMomentumScrollEnd={onMomentumScrollEnd}
166
+ scrollEventThrottle={16}
167
+ decelerationRate="fast"
168
+ >
169
+ {mediaPosts.map((post, index) => (
170
+ <View
171
+ key={post.postId}
172
+ style={[styles.postMedia__slide, { width: slideWidth }]}
173
+ >
174
+ <MediaFrame
175
+ post={post}
176
+ aspectRatio={aspectRatio}
177
+ onPress={() => handleFrameClick(index)}
178
+ />
179
+ </View>
180
+ ))}
181
+ </ScrollView>
182
+ <PositionCounter current={current} total={total} />
183
+ </View>
184
+ <PaginationIndicator current={current} total={total} />
185
+ </View>
186
+ );
187
+ }
188
+
189
+ type PaginationIndicatorProps = {
190
+ current: number;
191
+ total: number;
192
+ };
193
+
194
+ function PaginationIndicator({ current, total }: PaginationIndicatorProps) {
195
+ const styles = useStyles();
196
+ const { dots } = getIndicatorConfig(current, total);
197
+
198
+ return (
199
+ <View style={styles.postMedia__indicator}>
200
+ {dots.map((dot) => (
201
+ <View key={dot.key} style={styles.postMedia__dotSlot}>
202
+ <View
203
+ style={[
204
+ styles.postMedia__dot,
205
+ dot.state === 'active'
206
+ ? styles.postMedia__dotActive
207
+ : dot.state === 'edge'
208
+ ? styles.postMedia__dotEdge
209
+ : styles.postMedia__dotInactive,
210
+ ]}
211
+ />
212
+ </View>
213
+ ))}
214
+ </View>
215
+ );
216
+ }
217
+
218
+ type PositionCounterProps = {
219
+ current: number;
220
+ total: number;
221
+ };
222
+
223
+ function PositionCounter({ current, total }: PositionCounterProps) {
224
+ const styles = useStyles();
225
+
226
+ return (
227
+ <View style={styles.postMedia__counter}>
228
+ <Typography.Body style={styles.postMedia__counterLabel}>
229
+ {current + 1}/{total}
230
+ </Typography.Body>
231
+ </View>
232
+ );
233
+ }
234
+
235
+ type MediaFrameProps = {
236
+ post: MediaPost;
237
+ aspectRatio: number;
238
+ onPress: () => void;
239
+ };
240
+
241
+ function MediaFrame({ post, aspectRatio, onPress }: MediaFrameProps) {
242
+ const styles = useStyles();
243
+
244
+ return (
245
+ <Pressable
246
+ onPress={onPress}
247
+ style={[styles.postMedia__frame, { aspectRatio }]}
248
+ >
249
+ {post.dataType === 'image' ? (
250
+ <ImageMedia post={post as Amity.Post<'image'>} />
251
+ ) : (
252
+ <VideoMedia post={post as Amity.Post<'video'>} />
253
+ )}
254
+ </Pressable>
255
+ );
256
+ }
257
+
258
+ type ImageMediaProps = { post: Amity.Post<'image'> };
259
+
260
+ function ImageMedia({ post }: ImageMediaProps) {
261
+ const styles = useStyles();
262
+ const { apiRegion } = useAuth();
263
+ const [isBroken, setIsBroken] = useState(false);
264
+ const fileId = post.getImageInfo()?.fileId;
265
+ const url = fileId
266
+ ? `https://api.${apiRegion}.amity.co/api/v3/files/${fileId}/download?size=large`
267
+ : undefined;
268
+
269
+ if (!url || isBroken) return <BrokenFrame />;
270
+
271
+ return (
272
+ <Image
273
+ style={styles.postMedia__media as StyleProp<ImageStyle>}
274
+ source={{ uri: url }}
275
+ resizeMode="cover"
276
+ onError={() => setIsBroken(true)}
277
+ />
278
+ );
279
+ }
280
+
281
+ type VideoMediaProps = { post: Amity.Post<'video'> };
282
+
283
+ function VideoMedia({ post }: VideoMediaProps) {
284
+ const styles = useStyles();
285
+ const { apiRegion } = useAuth();
286
+ const [isBroken, setIsBroken] = useState(false);
287
+
288
+ const data = post.data as Amity.ContentDataVideo | undefined;
289
+ const thumbnailFileId = data?.thumbnailFileId;
290
+ const url = thumbnailFileId
291
+ ? `https://api.${apiRegion}.amity.co/api/v3/files/${thumbnailFileId}/download?size=large`
292
+ : undefined;
293
+
294
+ if (!url || isBroken) return <BrokenFrame />;
295
+
296
+ return (
297
+ <>
298
+ <Image
299
+ style={styles.postMedia__media as StyleProp<ImageStyle>}
300
+ source={{ uri: url }}
301
+ resizeMode="cover"
302
+ onError={() => setIsBroken(true)}
303
+ />
304
+ <View style={styles.postMedia__playButton}>
305
+ <SvgXml xml={videoControlIcon} width="40" height="40" />
306
+ </View>
307
+ </>
308
+ );
309
+ }
310
+
311
+ function BrokenFrame() {
312
+ const styles = useStyles();
313
+ return (
314
+ <View style={styles.postMedia__broken}>
315
+ <SvgXml xml={brokenImageIcon()} width={32} height={32} />
316
+ </View>
317
+ );
318
+ }
@@ -0,0 +1,5 @@
1
+ export { PostMediaElement } from './PostMediaElement';
2
+ export type {
3
+ PostMediaElementProps,
4
+ PostMediaControls,
5
+ } from './PostMediaElement';
@@ -0,0 +1,99 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import { useTheme } from 'react-native-paper';
3
+ import type { MyMD3Theme } from '../../../core/providers/AmityUIKitProvider';
4
+
5
+ // Values map 1:1 from web PR #1830 PostMediaElement.module.css (1rem = 16dp).
6
+ // Colors resolve through theme tokens (web CSS variables), never hardcoded.
7
+ export const useStyles = () => {
8
+ const theme = useTheme<MyMD3Theme>();
9
+
10
+ const styles = StyleSheet.create({
11
+ postMedia: {
12
+ // Full-bleed: cancel the post card's 16px horizontal padding so the media
13
+ // spans the full card width (web PR #1830/#1834: width calc(100% + 2rem),
14
+ // margin-left/right -1rem).
15
+ marginHorizontal: -16,
16
+ paddingVertical: 8, // web: padding 0.5rem 0
17
+ },
18
+ postMedia__track: {
19
+ position: 'relative',
20
+ width: '100%',
21
+ },
22
+ postMedia__slide: {
23
+ // width is set dynamically to the measured track width so paging aligns
24
+ },
25
+ postMedia__frame: {
26
+ width: '100%',
27
+ overflow: 'hidden',
28
+ // web: no border-radius; background var(--asc-color-base-shade4)
29
+ backgroundColor: theme.colors.baseShade4,
30
+ },
31
+ postMedia__media: {
32
+ width: '100%',
33
+ height: '100%',
34
+ },
35
+ postMedia__playButton: {
36
+ position: 'absolute',
37
+ top: '50%',
38
+ left: '50%',
39
+ // web: 2.5rem (40dp), centred via translate(-50%, -50%)
40
+ width: 40,
41
+ height: 40,
42
+ transform: [{ translateX: -20 }, { translateY: -20 }],
43
+ zIndex: 1,
44
+ },
45
+ postMedia__broken: {
46
+ flex: 1,
47
+ alignItems: 'center',
48
+ justifyContent: 'center',
49
+ backgroundColor: theme.colors.baseShade4,
50
+ },
51
+ postMedia__counter: {
52
+ position: 'absolute',
53
+ top: 8, // web: 0.5rem
54
+ right: 8, // web: 0.5rem
55
+ alignItems: 'center',
56
+ justifyContent: 'center',
57
+ paddingVertical: 6, // web: 0.375rem
58
+ paddingHorizontal: 12, // web: 0.75rem
59
+ borderRadius: 999, // web: --asc-border-radius-full
60
+ backgroundColor: 'rgba(0,0,0,0.5)', // web: --asc-color-transparent-black (#00000080)
61
+ },
62
+ postMedia__counterLabel: {
63
+ color: '#FFFFFF', // web: var(--asc-color-white)
64
+ },
65
+ postMedia__indicator: {
66
+ flexDirection: 'row',
67
+ alignItems: 'center',
68
+ justifyContent: 'center',
69
+ gap: 4, // web: gap 0.25rem
70
+ height: 6, // web: height 0.375rem
71
+ marginTop: 12, // web: parent .postMedia column gap 0.75rem
72
+ },
73
+ postMedia__dotSlot: {
74
+ alignItems: 'center',
75
+ justifyContent: 'center',
76
+ width: 6, // web: 0.375rem
77
+ height: 6, // web: 0.375rem
78
+ },
79
+ postMedia__dot: {
80
+ borderRadius: 999, // web: border-radius 50%
81
+ backgroundColor: theme.colors.baseShade4, // inactive/edge: base-shade4
82
+ },
83
+ postMedia__dotActive: {
84
+ width: 6,
85
+ height: 6,
86
+ backgroundColor: theme.colors.base, // web: var(--asc-color-base-default)
87
+ },
88
+ postMedia__dotInactive: {
89
+ width: 6,
90
+ height: 6,
91
+ },
92
+ postMedia__dotEdge: {
93
+ width: 3, // web: 0.1875rem
94
+ height: 3,
95
+ },
96
+ });
97
+
98
+ return styles;
99
+ };
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  Alert,
3
- FlatList,
4
3
  Keyboard,
5
4
  KeyboardAvoidingView,
6
5
  Platform,
@@ -48,12 +47,11 @@ import AmityMediaAttachmentComponent from '../components/MediaAttachment';
48
47
  import AmityDetailedMediaAttachmentComponent from '../components/DetailedMediaAttachment';
49
48
  import { useKeyboardStatus } from '../../../hooks';
50
49
  import ImagePicker, {
51
- launchImageLibrary,
52
50
  type Asset,
53
51
  launchCamera,
54
52
  } from 'react-native-image-picker';
55
- import LoadingImage from '../../../components/LoadingImage';
56
- import LoadingVideo from '../../../components/LoadingVideo';
53
+ import { SelectedMediaComponent } from '../../../components/SelectedMediaComponent';
54
+ import { MediaSelector } from '../../../components/MediaSelector';
57
55
  import { NativeStackNavigationProp } from '@react-navigation/native-stack';
58
56
  import { RootStackParamList } from '../../../../core/routes/RouteParamList';
59
57
  import {
@@ -117,6 +115,9 @@ const AmityPostComposerPage: FC<AmityPostComposerPageType> = ({
117
115
  const [isSwipeup, setIsSwipeup] = useState(true);
118
116
  const [deletedPostIds, setDeletedPostIds] = useState<string[]>([]);
119
117
  const [isUploading, setIsUploading] = useState(false);
118
+ const [mediaSelectorType, setMediaSelectorType] = useState<
119
+ 'photo' | 'video' | null
120
+ >(null);
120
121
  const [hasChangedAttachment, setHasChangedAttachment] = useState(false);
121
122
  const [imageErrors, setImageErrors] = useState<Set<string>>(new Set());
122
123
  const [videoErrors, setVideoErrors] = useState<Set<string>>(new Set());
@@ -584,64 +585,42 @@ const AmityPostComposerPage: FC<AmityPostComposerPageType> = ({
584
585
  }, [displayImages.length, displayVideos.length, pickCamera]);
585
586
 
586
587
  const onPressImage = useCallback(async () => {
587
- if (displayImages.length === 10)
588
+ if (displayImages.length >= 10)
588
589
  return Alert.alert(
589
590
  'Maximum upload limit reached',
590
591
  "You've reached the upload limit of 10 images. Any additional images will not be saved."
591
592
  );
592
- const result: ImagePicker.ImagePickerResponse = await launchImageLibrary({
593
- mediaType: 'photo',
594
- quality: 1,
595
- selectionLimit: 10 - displayImages.length,
596
- });
597
- if (!result.didCancel && result.assets && result.assets.length > 0) {
598
- const imageUriArr: string[] = result.assets.map(
599
- (item: Asset) => item.uri
600
- ) as string[];
601
- const mediaOj = processMedia(imageUriArr);
602
- setDisplayImages((prev) => {
603
- const updatedArray = [...prev, ...mediaOj];
604
- if (updatedArray.length > 10) {
605
- Alert.alert(
606
- 'Maximum number of images exceeded',
607
- 'Maximum number of images that can be uploaded is 10. The rest images will be discarded'
608
- );
609
- return updatedArray.slice(0, 10);
610
- }
611
- return updatedArray;
612
- });
613
- }
614
- }, [displayImages.length, processMedia]);
593
+ setMediaSelectorType('photo');
594
+ }, [displayImages.length]);
615
595
 
616
596
  const onPressVideo = useCallback(async () => {
617
- if (displayVideos.length === 10)
597
+ if (displayVideos.length >= 10)
618
598
  return Alert.alert(
619
599
  'Maximum upload limit reached',
620
600
  "You've reached the upload limit of 10 videos. Any additional videos will not be saved."
621
601
  );
622
- const result: ImagePicker.ImagePickerResponse = await launchImageLibrary({
623
- mediaType: 'video',
624
- quality: 1,
625
- selectionLimit: 10 - displayVideos.length,
626
- });
627
- if (!result.didCancel && result.assets && result.assets.length > 0) {
628
- const videoUriArr: string[] = result.assets.map(
629
- (item: Asset) => item.uri
630
- ) as string[];
631
- const mediaOj = processMedia(videoUriArr);
632
- setDisplayVideos((prev) => {
633
- const updatedArray = [...prev, ...mediaOj];
634
- if (updatedArray.length > 10) {
635
- Alert.alert(
636
- 'Maximum number of videos exceeded',
637
- 'Maximum number of videos that can be uploaded is 10. The rest videos will be discarded'
638
- );
639
- return updatedArray.slice(0, 10);
640
- }
641
- return updatedArray;
602
+ setMediaSelectorType('video');
603
+ }, [displayVideos.length]);
604
+
605
+ // Receives new URIs from the custom media selector. Silently drops duplicates
606
+ // (already-selected files) and caps the total at 10, per PDT-4314.
607
+ const handleSelectedMedia = useCallback(
608
+ (uris: string[]) => {
609
+ const type = mediaSelectorType;
610
+ setMediaSelectorType(null);
611
+ if (!type || uris.length === 0) return;
612
+ const mediaOj = processMedia(uris);
613
+ if (!mediaOj) return;
614
+ const setter = type === 'photo' ? setDisplayImages : setDisplayVideos;
615
+ setter((prev) => {
616
+ const existingUrls = new Set(prev.map((m) => m.url));
617
+ const additions = mediaOj.filter((m) => !existingUrls.has(m.url));
618
+ const updated = [...prev, ...additions];
619
+ return updated.length > 10 ? updated.slice(0, 10) : updated;
642
620
  });
643
- }
644
- }, [displayVideos.length, processMedia]);
621
+ },
622
+ [mediaSelectorType, processMedia]
623
+ );
645
624
 
646
625
  const handleImageUploadError = useCallback(
647
626
  (hasError: boolean, source: string) => {
@@ -752,6 +731,11 @@ const AmityPostComposerPage: FC<AmityPostComposerPageType> = ({
752
731
  []
753
732
  );
754
733
 
734
+ // PDT-4310 / PDT-4312: at the 10-attachment cap the camera + gallery icons
735
+ // are disabled until the user removes a frame.
736
+ const isMediaCapReached =
737
+ displayImages.length >= 10 || displayVideos.length >= 10;
738
+
755
739
  const renderDetailedAttachment = useCallback(() => {
756
740
  if (isEditMode) return null;
757
741
  if (shouldShowDetailAttachment) {
@@ -762,6 +746,7 @@ const AmityPostComposerPage: FC<AmityPostComposerPageType> = ({
762
746
  onPressVideo={onPressVideo}
763
747
  chosenMediaType={chosenMediaType}
764
748
  onHeightChange={setAttachmentBarHeight}
749
+ disabled={isMediaCapReached}
765
750
  />
766
751
  );
767
752
  }
@@ -772,11 +757,13 @@ const AmityPostComposerPage: FC<AmityPostComposerPageType> = ({
772
757
  onPressVideo={onPressVideo}
773
758
  chosenMediaType={chosenMediaType}
774
759
  onHeightChange={setAttachmentBarHeight}
760
+ disabled={isMediaCapReached}
775
761
  />
776
762
  );
777
763
  }, [
778
764
  chosenMediaType,
779
765
  isEditMode,
766
+ isMediaCapReached,
780
767
  onPressCamera,
781
768
  onPressImage,
782
769
  onPressVideo,
@@ -835,54 +822,25 @@ const AmityPostComposerPage: FC<AmityPostComposerPageType> = ({
835
822
  })}
836
823
  <View style={styles.imageContainer}>
837
824
  {displayImages.length > 0 && (
838
- <FlatList
839
- nestedScrollEnabled={true}
840
- scrollEnabled={false}
841
- data={displayImages}
842
- renderItem={({ item, index }) => {
843
- if (!item) return null;
844
- return (
845
- <LoadingImage
846
- source={item.url}
847
- onClose={handleOnCloseImage}
848
- index={index} //TODO: Fix this without index
849
- onLoadFinish={handleOnFinishImage}
850
- onUploadError={handleImageUploadError}
851
- isUploaded={item.isUploaded}
852
- fileId={item.fileId}
853
- fileCount={displayImages.length}
854
- isEditMode={isEditMode}
855
- postId={item.postId}
856
- setIsUploading={setIsUploading}
857
- />
858
- );
859
- }}
860
- numColumns={3}
825
+ <SelectedMediaComponent
826
+ mediaType="image"
827
+ media={displayImages}
828
+ onClose={handleOnCloseImage}
829
+ onLoadFinish={handleOnFinishImage}
830
+ onUploadError={handleImageUploadError}
831
+ isEditMode={isEditMode}
832
+ setIsUploading={setIsUploading}
861
833
  />
862
834
  )}
863
835
  {displayVideos.length > 0 && (
864
- <FlatList
865
- data={displayVideos}
866
- renderItem={({ item, index }) => {
867
- if (!item) return null;
868
- return (
869
- <LoadingVideo
870
- source={item.url}
871
- onClose={handleOnCloseVideo}
872
- index={index} //TODO: Fix this without index
873
- onLoadFinish={handleOnFinishVideo}
874
- onUploadError={handleVideoUploadError}
875
- isUploaded={item.isUploaded}
876
- fileId={item.fileId}
877
- thumbNail={item.thumbNail as string}
878
- fileCount={displayVideos.length}
879
- isEditMode={isEditMode}
880
- postId={item.postId}
881
- setIsUploading={setIsUploading}
882
- />
883
- );
884
- }}
885
- numColumns={3}
836
+ <SelectedMediaComponent
837
+ mediaType="video"
838
+ media={displayVideos}
839
+ onClose={handleOnCloseVideo}
840
+ onLoadFinish={handleOnFinishVideo}
841
+ onUploadError={handleVideoUploadError}
842
+ isEditMode={isEditMode}
843
+ setIsUploading={setIsUploading}
886
844
  />
887
845
  )}
888
846
  </View>
@@ -902,6 +860,22 @@ const AmityPostComposerPage: FC<AmityPostComposerPageType> = ({
902
860
  </View>
903
861
  </KeyboardAvoidingView>
904
862
  <StatusBar backgroundColor={themeStyles.colors.background} />
863
+ <MediaSelector
864
+ visible={mediaSelectorType !== null}
865
+ mediaType={mediaSelectorType ?? 'photo'}
866
+ existing={(mediaSelectorType === 'video'
867
+ ? displayVideos
868
+ : displayImages
869
+ ).map((m) => m.url)}
870
+ remaining={
871
+ 10 -
872
+ (mediaSelectorType === 'video'
873
+ ? displayVideos.length
874
+ : displayImages.length)
875
+ }
876
+ onClose={() => setMediaSelectorType(null)}
877
+ onConfirm={handleSelectedMedia}
878
+ />
905
879
  </SafeAreaView>
906
880
  );
907
881
  };