@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
@@ -1,25 +1,30 @@
1
- import { Fragment, memo, useCallback, useEffect, useState } from 'react';
2
1
  import {
3
- Image,
4
- Pressable,
5
- Text,
6
- TouchableOpacity,
7
- TouchableWithoutFeedback,
8
- View,
9
- } from 'react-native';
10
- import { SvgXml } from 'react-native-svg';
2
+ Fragment,
3
+ memo,
4
+ useCallback,
5
+ useEffect,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+ import { Pressable, Text, TouchableOpacity, View } from 'react-native';
11
10
  import { useStyles } from './styles';
12
11
  import useAuth from '../../../core/hooks/useAuth';
13
12
  import { IVideoPost, MediaUri } from '../legacy/Social/PostList';
14
13
  import { getPostById } from '../../../core/legacy/feed';
15
14
  import ImageView from '../legacy/react-native-image-viewing/dist';
15
+ import { SvgXml } from 'react-native-svg';
16
+ import { clearIcon } from '../../../core/assets/icons/xml';
16
17
  import { RootState, useUIKitSelector } from '../../../core/stores/store';
17
- import { playBtn } from '../../../core/assets/icons/xml';
18
+ import {
19
+ PostMediaElement,
20
+ type PostMediaControls,
21
+ } from '../../elements/PostMediaElement';
18
22
  import LivestreamContent from '../LivestreamContent';
19
23
  import { LinkPreview } from '../PreviewLink';
20
24
  import RenderTextWithMention from '../RenderTextWithMention/RenderTextWithMention';
21
25
  import { IMentionPosition } from '../../../core/types';
22
26
  import PollContent from '../PollContent';
27
+ import { MediaViewer, type MediaViewerItem } from '../MediaViewer';
23
28
 
24
29
  interface IPostContent {
25
30
  post: Amity.Post;
@@ -49,10 +54,16 @@ const PostContent: React.FC<IPostContent> = ({
49
54
  const [videoPostsFullSize, setVideoPostsFullSize] = useState<MediaUri[]>([]);
50
55
  const [visibleFullImage, setIsVisibleFullImage] = useState<boolean>(false);
51
56
  const [imageIndex, setImageIndex] = useState<number>(0);
57
+ const [videoViewerIndex, setVideoViewerIndex] = useState<number | null>(null);
58
+ const [mediaLoading, setMediaLoading] = useState<boolean>(
59
+ (childrenPosts?.length ?? 0) > 0
60
+ );
61
+ const [mediaChildPosts, setMediaChildPosts] = useState<Amity.Post[]>([]);
62
+
63
+ const mediaControlsRef = useRef<PostMediaControls | null>(null);
64
+ const viewerIndexRef = useRef<number>(0);
52
65
 
53
66
  const styles = useStyles();
54
- let imageStyle: any = styles.imageLargePost;
55
- let colStyle: any = styles.col2;
56
67
  const { currentPostdetail } = useUIKitSelector(
57
68
  (state: RootState) => state.postDetail
58
69
  );
@@ -83,11 +94,16 @@ const PostContent: React.FC<IPostContent> = ({
83
94
  }, [imagePosts, videoPosts, apiRegion]);
84
95
 
85
96
  const getPostInfo = useCallback(async () => {
97
+ setMediaLoading((childrenPosts?.length ?? 0) > 0);
86
98
  try {
87
99
  const response = await Promise.all(
88
100
  childrenPosts.map(async (id) => {
89
101
  const { data: childrenPost } = await getPostById(id);
90
- return { dataType: childrenPost?.dataType, data: childrenPost?.data };
102
+ return {
103
+ post: childrenPost,
104
+ dataType: childrenPost?.dataType,
105
+ data: childrenPost?.data,
106
+ };
91
107
  })
92
108
  );
93
109
 
@@ -95,12 +111,14 @@ const PostContent: React.FC<IPostContent> = ({
95
111
  const videos: IVideoPost[] = [];
96
112
  const polls: { pollId: string }[] = [];
97
113
  const livestreams: Amity.Room['roomId'][] = [];
114
+ const media: Amity.Post[] = [];
98
115
 
99
116
  response.forEach((item) => {
100
117
  if (item?.dataType === 'image' && item?.data?.fileId) {
101
118
  const url: string = `https://api.${apiRegion}.amity.co/api/v3/files/${item?.data.fileId}/download?size=medium`;
102
119
  if (!images.includes(url)) {
103
120
  images.push(url);
121
+ if (item.post) media.push(item.post);
104
122
  }
105
123
  } else if (
106
124
  item?.dataType === 'video' &&
@@ -112,6 +130,7 @@ const PostContent: React.FC<IPostContent> = ({
112
130
  );
113
131
  if (!isExisted) {
114
132
  videos.push(item.data);
133
+ if (item.post) media.push(item.post);
115
134
  }
116
135
  } else if (item?.dataType === 'poll') {
117
136
  if (!polls.some((poll) => poll.pollId === item.data.pollId)) {
@@ -124,12 +143,11 @@ const PostContent: React.FC<IPostContent> = ({
124
143
  }
125
144
  });
126
145
 
127
- if (images.length > 0) {
128
- setImagePosts(images);
129
- }
130
- if (videos.length > 0) {
131
- setVideoPosts(videos);
132
- }
146
+ // Set unconditionally so navigating to a post without media clears the
147
+ // previous media (see the effect below which no longer eagerly resets).
148
+ setMediaChildPosts(media);
149
+ setImagePosts(images);
150
+ setVideoPosts(videos);
133
151
  if (polls.length > 0) {
134
152
  setPollIds(polls);
135
153
  }
@@ -138,168 +156,62 @@ const PostContent: React.FC<IPostContent> = ({
138
156
  }
139
157
  } catch (error) {
140
158
  console.log('error: ', error);
159
+ } finally {
160
+ setMediaLoading(false);
141
161
  }
142
162
  }, [apiRegion, childrenPosts]);
143
163
 
144
164
  useEffect(() => {
145
- setVideoPosts([]);
146
- setImagePosts([]);
165
+ // Don't clear media here: getPostInfo resolves images/videos/media to their
166
+ // fresh values, so the existing media stays on screen until the new data
167
+ // arrives. Eagerly resetting caused a blank flash whenever the feed
168
+ // re-rendered (e.g. realtime updates), then the content popped back in.
147
169
  getPostInfo();
148
170
  }, [childrenPosts, currentPostdetail, postList, postListGlobal, getPostInfo]);
149
171
 
150
172
  function onClickImage(index: number): void {
173
+ viewerIndexRef.current = index;
151
174
  setIsVisibleFullImage(true);
152
175
  setImageIndex(index);
153
176
  }
154
177
 
155
- function renderMediaPosts() {
156
- const thumbnailFileIds: string[] =
157
- videoPosts.length > 0
158
- ? videoPosts.map((item) => {
159
- return `https://api.${apiRegion}.amity.co/api/v3/files/${item?.thumbnailFileId}/download?size=medium`;
160
- })
161
- : [];
162
- const mediaPosts =
163
- [...imagePosts].length > 0 ? [...imagePosts] : [...thumbnailFileIds];
164
- const imageElement = mediaPosts.map((item: string, index: number) => {
165
- if (mediaPosts.length === 1) {
166
- imageStyle = styles.imageLargePost;
167
- colStyle = styles.col6;
168
- } else if (mediaPosts.length === 2) {
169
- colStyle = styles.col3;
170
- if (index === 0) {
171
- imageStyle = [styles.imageLargePost, styles.imageMarginRight];
172
- } else {
173
- imageStyle = [styles.imageLargePost, styles.imageMarginLeft];
174
- }
175
- } else if (mediaPosts.length === 3) {
176
- switch (index) {
177
- case 0:
178
- colStyle = styles.col6;
179
- imageStyle = [styles.imageMediumPost, styles.imageMarginBottom];
180
- break;
181
- case 1:
182
- colStyle = styles.col3;
183
- imageStyle = [
184
- styles.imageMediumPost,
185
- styles.imageMarginTop,
186
- styles.imageMarginRight,
187
- ];
188
- break;
189
- case 2:
190
- colStyle = styles.col3;
191
- imageStyle = [
192
- styles.imageMediumPost,
193
- styles.imageMarginTop,
194
- styles.imageMarginLeft,
195
- ];
196
- break;
197
-
198
- default:
199
- break;
200
- }
201
- } else {
202
- switch (index) {
203
- case 0:
204
- colStyle = styles.col6;
205
- imageStyle = [
206
- styles.imageMediumLargePost,
207
- styles.imageMarginBottom,
208
- ];
209
- break;
210
- case 1:
211
- colStyle = styles.col2;
212
- imageStyle = [
213
- styles.imageSmallPost,
214
- styles.imageMarginTop,
215
- styles.imageMarginRight,
216
- ];
217
- break;
218
- case 2:
219
- colStyle = styles.col2;
220
- imageStyle = [
221
- styles.imageSmallPost,
222
- styles.imageMarginTop,
223
- styles.imageMarginLeft,
224
- styles.imageMarginRight,
225
- ];
226
- break;
227
- case 3:
228
- colStyle = styles.col2;
229
- imageStyle = [
230
- styles.imageSmallPost,
231
- styles.imageMarginTop,
232
- styles.imageMarginLeft,
233
- ];
234
- break;
235
- default:
236
- break;
237
- }
238
- }
178
+ function closeFullScreen(): void {
179
+ setIsVisibleFullImage(false);
180
+ mediaControlsRef.current?.slideTo(viewerIndexRef.current);
181
+ }
239
182
 
240
- return (
241
- <View style={colStyle} key={item}>
242
- <TouchableWithoutFeedback onPress={() => onClickImage(index)}>
243
- <View>
244
- {videoPosts.length > 0 && renderPlayButton()}
245
- <Image
246
- style={imageStyle}
247
- source={{
248
- uri: item,
249
- }}
250
- />
251
- {index === 3 && imagePosts.length > 4 && (
252
- <View style={styles.overlay}>
253
- <Text style={styles.overlayText}>{`+ ${
254
- imagePosts.length - 3
255
- }`}</Text>
256
- </View>
257
- )}
258
- </View>
259
- </TouchableWithoutFeedback>
260
- </View>
261
- );
262
- });
263
- if (imageElement.length < 3) {
264
- return (
265
- <View style={styles.imagesWrap}>
266
- <View style={styles.row}>{imageElement}</View>
267
- </View>
268
- );
269
- } else if (imageElement.length === 3) {
270
- return (
271
- <View style={[styles.imagesWrap]}>
272
- <View style={styles.row}>{imageElement.slice(0, 1)}</View>
273
- <View style={styles.row}>{imageElement.slice(1, 3)}</View>
274
- </View>
275
- );
276
- } else {
277
- return (
278
- <View style={styles.imagesWrap}>
279
- <View style={styles.row}>{imageElement.slice(0, 1)}</View>
280
- <View style={styles.row}>{imageElement.slice(1, 4)}</View>
281
- </View>
282
- );
283
- }
183
+ // UC2 (PDT-4309): video carousel opens the swipeable full-screen player
184
+ // (autoplay + mute-carry) instead of the single-video route.
185
+ function openVideoViewer(index: number): void {
186
+ viewerIndexRef.current = index;
187
+ setVideoViewerIndex(index);
284
188
  }
285
189
 
286
- function renderPlayButton() {
287
- return (
288
- <View style={styles.playButton}>
289
- <SvgXml xml={playBtn} width="50" height="50" />
290
- </View>
291
- );
190
+ function closeVideoViewer(lastIndex: number): void {
191
+ setVideoViewerIndex(null);
192
+ // Published post: return to the last-viewed frame.
193
+ mediaControlsRef.current?.slideTo(lastIndex);
292
194
  }
293
195
 
196
+ const videoViewerItems: MediaViewerItem[] = videoPosts.map(
197
+ (item: IVideoPost) => ({
198
+ type: 'video',
199
+ // The /files/{original}/download endpoint returns the original video
200
+ // file (mp4), not an HLS playlist — no m3u8 type hint (let the player
201
+ // detect the container).
202
+ uri: `https://api.${apiRegion}.amity.co/api/v3/files/${item?.videoFileId?.original}/download`,
203
+ })
204
+ );
205
+
294
206
  function renderImageHeader({ imageIndex: imgIndex }) {
295
207
  return (
296
208
  <View style={styles.headerContainer}>
297
209
  <View style={styles.flexWidth}>
298
210
  <TouchableOpacity
299
211
  style={styles.closebtnIcon}
300
- onPress={() => setIsVisibleFullImage(false)}
212
+ onPress={closeFullScreen}
301
213
  >
302
- <Text style={styles.closeBtn}>X</Text>
214
+ <SvgXml xml={clearIcon('white')} width={28} height={28} />
303
215
  </TouchableOpacity>
304
216
  </View>
305
217
  <View style={styles.flexWidth}>
@@ -345,7 +257,13 @@ const PostContent: React.FC<IPostContent> = ({
345
257
  roomId={livestreamId[0]}
346
258
  />
347
259
  ) : (
348
- renderMediaPosts()
260
+ <PostMediaElement
261
+ posts={mediaChildPosts}
262
+ onImageClick={onClickImage}
263
+ onVideoClick={openVideoViewer}
264
+ controlsRef={mediaControlsRef}
265
+ loading={mediaLoading}
266
+ />
349
267
  )}
350
268
  <ImageView
351
269
  images={
@@ -355,11 +273,20 @@ const PostContent: React.FC<IPostContent> = ({
355
273
  }
356
274
  imageIndex={imageIndex}
357
275
  visible={visibleFullImage}
358
- onRequestClose={() => setIsVisibleFullImage(false)}
276
+ onRequestClose={closeFullScreen}
277
+ onImageIndexChange={(idx: number) => {
278
+ viewerIndexRef.current = idx;
279
+ }}
359
280
  isVideoButton={videoPosts.length > 0 ? true : false}
360
281
  videoPosts={videoPosts}
361
282
  HeaderComponent={renderImageHeader}
362
283
  />
284
+ <MediaViewer
285
+ visible={videoViewerIndex !== null}
286
+ items={videoViewerItems}
287
+ initialIndex={videoViewerIndex ?? 0}
288
+ onClose={closeVideoViewer}
289
+ />
363
290
  </Fragment>
364
291
  );
365
292
  };
@@ -0,0 +1,236 @@
1
+ import { Fragment, useEffect, useRef, useState } from 'react';
2
+ import {
3
+ Image,
4
+ LayoutChangeEvent,
5
+ Pressable,
6
+ ScrollView,
7
+ View,
8
+ } from 'react-native';
9
+ import LoadingImage from '../LoadingImage';
10
+ import LoadingVideo from '../LoadingVideo';
11
+ import { MediaViewer, type MediaViewerItem } from '../MediaViewer';
12
+ import { IDisplayImage } from '../../../core/types';
13
+ import {
14
+ getFrameRatio,
15
+ getVideoDisplayDims,
16
+ FRAME_RATIO_VALUE,
17
+ DEFAULT_FRAME_RATIO,
18
+ type FrameRatio,
19
+ } from '../../utils/getFrameRatio';
20
+ import { useStyles } from './styles';
21
+
22
+ // Peek scroll: next slide peeks by (PEEK_INSET − SPACE_BETWEEN). The composer
23
+ // body (PostComposer `inputWrapper`) already insets everything 12px
24
+ // horizontally, so this component adds NO side padding of its own — it only
25
+ // sizes the slide so the peek lands at the spec's 31px (SelectedMediaComponent
26
+ // REQ-003a). slideWidth = trackWidth − PEEK_INSET; peek = PEEK_INSET − 8 = 31.
27
+ const PEEK_INSET = 39;
28
+ const SPACE_BETWEEN = 8;
29
+
30
+ export type SelectedMediaComponentProps = {
31
+ mediaType: 'image' | 'video';
32
+ media: IDisplayImage[];
33
+ onClose?: (originalPath: string, fileId?: string, postId?: string) => void;
34
+ onLoadFinish?: (
35
+ fileId: string,
36
+ fileUrl: string,
37
+ fileName: string,
38
+ index: number,
39
+ originalPath: string,
40
+ thumbnail?: string
41
+ ) => void;
42
+ onUploadError?: (hasError: boolean, source: string) => void;
43
+ isEditMode?: boolean;
44
+ setIsUploading?: (arg: boolean) => void;
45
+ };
46
+
47
+ export function SelectedMediaComponent({
48
+ mediaType,
49
+ media,
50
+ onClose,
51
+ onLoadFinish,
52
+ onUploadError,
53
+ isEditMode = false,
54
+ setIsUploading,
55
+ }: SelectedMediaComponentProps) {
56
+ const styles = useStyles();
57
+ const scrollRef = useRef<ScrollView>(null);
58
+ const [trackWidth, setTrackWidth] = useState(0);
59
+ const [ratio, setRatio] = useState<FrameRatio>(DEFAULT_FRAME_RATIO);
60
+ // Index of the frame the full-screen viewer was opened from (null = closed).
61
+ const [viewerIndex, setViewerIndex] = useState<number | null>(null);
62
+
63
+ const total = media.length;
64
+ const isCarousel = total > 1;
65
+ const aspectRatio = FRAME_RATIO_VALUE[ratio];
66
+ const slideWidth = trackWidth > 0 ? trackWidth - PEEK_INSET : 0;
67
+
68
+ // Frames follow the aspect ratio classified from the item currently in first
69
+ // position (REQ-003d/003e); recalculated when the first item changes.
70
+ const first = media[0];
71
+ const firstUrl = first?.url;
72
+ const firstWidth = first?.width;
73
+ const firstHeight = first?.height;
74
+ const firstRotation = first?.rotation;
75
+ useEffect(() => {
76
+ if (!firstUrl) {
77
+ setRatio(DEFAULT_FRAME_RATIO);
78
+ return undefined;
79
+ }
80
+ // Video: classify from the record/extractor dims, applying rotation
81
+ // (REQ-003d1 — MUST NOT silently fall back to 1:1 for video). Shares the
82
+ // one rotation derivation with the published carousel (REQ-SDK-002b).
83
+ if (mediaType === 'video') {
84
+ if (firstWidth && firstHeight) {
85
+ const { width, height } = getVideoDisplayDims({
86
+ width: firstWidth,
87
+ height: firstHeight,
88
+ rotation: firstRotation,
89
+ });
90
+ setRatio(getFrameRatio(width, height));
91
+ } else {
92
+ // Log the fallback so a square frame is never silently ambiguous
93
+ // (REQ-003d2).
94
+ console.log(
95
+ '[SelectedMediaComponent] video has no dimensions; defaulting ratio'
96
+ );
97
+ setRatio(DEFAULT_FRAME_RATIO);
98
+ }
99
+ return undefined;
100
+ }
101
+ // Image: prefer carried dims, else measure the source.
102
+ if (firstWidth && firstHeight) {
103
+ setRatio(getFrameRatio(firstWidth, firstHeight));
104
+ return undefined;
105
+ }
106
+ let active = true;
107
+ Image.getSize(
108
+ firstUrl,
109
+ (width, height) => {
110
+ if (active) setRatio(getFrameRatio(width, height));
111
+ },
112
+ () => {
113
+ if (active) setRatio(DEFAULT_FRAME_RATIO);
114
+ }
115
+ );
116
+ return () => {
117
+ active = false;
118
+ };
119
+ }, [firstUrl, firstWidth, firstHeight, firstRotation, mediaType]);
120
+
121
+ const onLayout = (event: LayoutChangeEvent) => {
122
+ const width = event.nativeEvent.layout.width;
123
+ if (width && width !== trackWidth) setTrackWidth(width);
124
+ };
125
+
126
+ const viewerItems: MediaViewerItem[] = media.map((m) => ({
127
+ type: mediaType,
128
+ uri: m.url,
129
+ }));
130
+
131
+ // PDT-4310/4312: on close, return to the frame originally tapped from — not
132
+ // the last frame viewed in full screen.
133
+ const handleCloseViewer = () => {
134
+ const tapped = viewerIndex ?? 0;
135
+ setViewerIndex(null);
136
+ if (isCarousel && slideWidth > 0) {
137
+ scrollRef.current?.scrollTo({
138
+ x: tapped * (slideWidth + SPACE_BETWEEN),
139
+ animated: false,
140
+ });
141
+ }
142
+ };
143
+
144
+ const renderFrame = (item: IDisplayImage, index: number) => (
145
+ <View style={[styles.selectedMedia__frame, { aspectRatio }]}>
146
+ {mediaType === 'video' ? (
147
+ <LoadingVideo
148
+ carousel
149
+ source={item.url}
150
+ onClose={onClose}
151
+ index={index}
152
+ onLoadFinish={onLoadFinish}
153
+ onUploadError={onUploadError}
154
+ isUploaded={item.isUploaded}
155
+ fileId={item.fileId}
156
+ thumbNail={item.thumbNail as string}
157
+ fileCount={total}
158
+ isEditMode={isEditMode}
159
+ postId={item.postId}
160
+ setIsUploading={setIsUploading}
161
+ onPlay={() => setViewerIndex(index)}
162
+ />
163
+ ) : (
164
+ <Pressable
165
+ style={styles.selectedMedia__mediaButton}
166
+ onPress={() => setViewerIndex(index)}
167
+ >
168
+ <LoadingImage
169
+ carousel
170
+ source={item.url}
171
+ onClose={onClose}
172
+ index={index}
173
+ onLoadFinish={onLoadFinish}
174
+ onUploadError={onUploadError}
175
+ isUploaded={item.isUploaded}
176
+ fileId={item.fileId}
177
+ fileCount={total}
178
+ isEditMode={isEditMode}
179
+ postId={item.postId}
180
+ setIsUploading={setIsUploading}
181
+ />
182
+ </Pressable>
183
+ )}
184
+ </View>
185
+ );
186
+
187
+ if (total === 0) return null;
188
+
189
+ return (
190
+ <Fragment>
191
+ {!isCarousel ? (
192
+ <View style={styles.selectedMedia}>
193
+ <View style={styles.selectedMedia__track} onLayout={onLayout}>
194
+ {renderFrame(media[0], 0)}
195
+ </View>
196
+ </View>
197
+ ) : (
198
+ <View style={styles.selectedMedia}>
199
+ <View style={styles.selectedMedia__track} onLayout={onLayout}>
200
+ {slideWidth > 0 && (
201
+ <ScrollView
202
+ ref={scrollRef}
203
+ horizontal
204
+ showsHorizontalScrollIndicator={false}
205
+ decelerationRate="fast"
206
+ snapToInterval={slideWidth + SPACE_BETWEEN}
207
+ snapToAlignment="start"
208
+ >
209
+ {media.map((item, index) => (
210
+ <View
211
+ key={`${item.url}-${index}`}
212
+ style={[
213
+ styles.selectedMedia__slide,
214
+ {
215
+ width: slideWidth,
216
+ marginRight: index < total - 1 ? SPACE_BETWEEN : 0,
217
+ },
218
+ ]}
219
+ >
220
+ {renderFrame(item, index)}
221
+ </View>
222
+ ))}
223
+ </ScrollView>
224
+ )}
225
+ </View>
226
+ </View>
227
+ )}
228
+ <MediaViewer
229
+ visible={viewerIndex !== null}
230
+ items={viewerItems}
231
+ initialIndex={viewerIndex ?? 0}
232
+ onClose={handleCloseViewer}
233
+ />
234
+ </Fragment>
235
+ );
236
+ }
@@ -0,0 +1,2 @@
1
+ export { SelectedMediaComponent } from './SelectedMediaComponent';
2
+ export type { SelectedMediaComponentProps } from './SelectedMediaComponent';
@@ -0,0 +1,38 @@
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 SelectedMediaComponent.module.css (1rem = 16dp).
6
+ // Web composer preview: peek scroll row (slidesPerView=auto, spaceBetween 8,
7
+ // slide width = calc(100% - 3.4375rem) = track - 55). NO counter, NO dots.
8
+ export const useStyles = () => {
9
+ const theme = useTheme<MyMD3Theme>();
10
+
11
+ return StyleSheet.create({
12
+ selectedMedia: {
13
+ width: '100%',
14
+ paddingVertical: 12, // web: padding 0.75rem 0
15
+ },
16
+ selectedMedia__track: {
17
+ position: 'relative',
18
+ width: '100%',
19
+ },
20
+ selectedMedia__slide: {
21
+ // width + marginRight (spaceBetween 8) set inline from measured track width
22
+ },
23
+ selectedMedia__frame: {
24
+ width: '100%',
25
+ overflow: 'hidden',
26
+ // web: no border-radius; background var(--asc-color-base-shade4)
27
+ backgroundColor: theme.colors.baseShade4,
28
+ },
29
+ selectedMedia__media: {
30
+ width: '100%',
31
+ height: '100%',
32
+ },
33
+ selectedMedia__mediaButton: {
34
+ width: '100%',
35
+ height: '100%',
36
+ },
37
+ });
38
+ };
@@ -0,0 +1,4 @@
1
+ // Carousel media post (PDT-3424)
2
+ export const MAX_INDICATOR_DOTS = 6;
3
+ export const MEDIA_ATTACHMENT_CAP = 10;
4
+ export const SWIPE_COMMIT_PX = 40;