@amityco/react-native-social-uikit 4.1.1-fe2678f6.0 → 4.2.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 (205) hide show
  1. package/README.md +7 -0
  2. package/lib/commonjs/core/assets/icons/post.js +1 -1
  3. package/lib/commonjs/core/assets/icons/post.js.map +1 -1
  4. package/lib/commonjs/core/assets/icons/story.js +1 -1
  5. package/lib/commonjs/core/assets/icons/story.js.map +1 -1
  6. package/lib/commonjs/core/assets/icons/xml.js +16 -2
  7. package/lib/commonjs/core/assets/icons/xml.js.map +1 -1
  8. package/lib/commonjs/core/legacy/file.js +145 -62
  9. package/lib/commonjs/core/legacy/file.js.map +1 -1
  10. package/lib/commonjs/core/stores/slices/feedSlice.js +3 -0
  11. package/lib/commonjs/core/stores/slices/feedSlice.js.map +1 -1
  12. package/lib/commonjs/core/stores/slices/globalfeedSlice.js +5 -0
  13. package/lib/commonjs/core/stores/slices/globalfeedSlice.js.map +1 -1
  14. package/lib/commonjs/core/stores/slices/localVideoThumbnailSlice.js +46 -0
  15. package/lib/commonjs/core/stores/slices/localVideoThumbnailSlice.js.map +1 -0
  16. package/lib/commonjs/core/stores/slices/uiSlice.js +5 -1
  17. package/lib/commonjs/core/stores/slices/uiSlice.js.map +1 -1
  18. package/lib/commonjs/core/stores/store/index.js +3 -1
  19. package/lib/commonjs/core/stores/store/index.js.map +1 -1
  20. package/lib/commonjs/social/components/LoadingImage/index.js +73 -12
  21. package/lib/commonjs/social/components/LoadingImage/index.js.map +1 -1
  22. package/lib/commonjs/social/components/LoadingImage/styles.js +8 -3
  23. package/lib/commonjs/social/components/LoadingImage/styles.js.map +1 -1
  24. package/lib/commonjs/social/components/LoadingVideo/index.js +90 -16
  25. package/lib/commonjs/social/components/LoadingVideo/index.js.map +1 -1
  26. package/lib/commonjs/social/components/LoadingVideo/styles.js +14 -5
  27. package/lib/commonjs/social/components/LoadingVideo/styles.js.map +1 -1
  28. package/lib/commonjs/social/components/MediaViewer/MediaViewer.js +13 -13
  29. package/lib/commonjs/social/components/MediaViewer/MediaViewer.js.map +1 -1
  30. package/lib/commonjs/social/components/MediaViewer/styles.js +11 -1
  31. package/lib/commonjs/social/components/MediaViewer/styles.js.map +1 -1
  32. package/lib/commonjs/social/components/PostContent/index.js +8 -29
  33. package/lib/commonjs/social/components/PostContent/index.js.map +1 -1
  34. package/lib/commonjs/social/components/SelectedMediaComponent/SelectedMediaComponent.js +109 -17
  35. package/lib/commonjs/social/components/SelectedMediaComponent/SelectedMediaComponent.js.map +1 -1
  36. package/lib/commonjs/social/components/legacy/LoadingVideo/index.js +17 -6
  37. package/lib/commonjs/social/components/legacy/LoadingVideo/index.js.map +1 -1
  38. package/lib/commonjs/social/elements/PostMediaElement/PostMediaElement.js +121 -23
  39. package/lib/commonjs/social/elements/PostMediaElement/PostMediaElement.js.map +1 -1
  40. package/lib/commonjs/social/elements/PostMediaElement/styles.js +7 -0
  41. package/lib/commonjs/social/elements/PostMediaElement/styles.js.map +1 -1
  42. package/lib/commonjs/social/features/community/PendingRequest/components/PendingPostList/PendingPostList.js +11 -5
  43. package/lib/commonjs/social/features/community/PendingRequest/components/PendingPostList/PendingPostList.js.map +1 -1
  44. package/lib/commonjs/social/features/feed/components/NewsFeed/NewsFeed.js +14 -0
  45. package/lib/commonjs/social/features/feed/components/NewsFeed/NewsFeed.js.map +1 -1
  46. package/lib/commonjs/social/features/post/Composer/PostComposer.js +234 -86
  47. package/lib/commonjs/social/features/post/Composer/PostComposer.js.map +1 -1
  48. package/lib/commonjs/social/hooks/index.js +11 -0
  49. package/lib/commonjs/social/hooks/index.js.map +1 -1
  50. package/lib/commonjs/social/hooks/useCommunities.js +5 -3
  51. package/lib/commonjs/social/hooks/useCommunities.js.map +1 -1
  52. package/lib/commonjs/social/hooks/useCommunityPermission.js +43 -0
  53. package/lib/commonjs/social/hooks/useCommunityPermission.js.map +1 -0
  54. package/lib/commonjs/social/hooks/useCustomRankingGlobalFeed.js +10 -1
  55. package/lib/commonjs/social/hooks/useCustomRankingGlobalFeed.js.map +1 -1
  56. package/lib/commonjs/social/hooks/useFile.js +11 -0
  57. package/lib/commonjs/social/hooks/useFile.js.map +1 -1
  58. package/lib/commonjs/social/screens/CommunityProfile/components/Header/Header.js +18 -7
  59. package/lib/commonjs/social/screens/CommunityProfile/components/Header/Header.js.map +1 -1
  60. package/lib/commonjs/social/utils/mediaAttachments.js +150 -0
  61. package/lib/commonjs/social/utils/mediaAttachments.js.map +1 -0
  62. package/lib/commonjs/social/utils/postChildren.js +72 -0
  63. package/lib/commonjs/social/utils/postChildren.js.map +1 -0
  64. package/lib/module/core/assets/icons/post.js +1 -1
  65. package/lib/module/core/assets/icons/post.js.map +1 -1
  66. package/lib/module/core/assets/icons/story.js +1 -1
  67. package/lib/module/core/assets/icons/story.js.map +1 -1
  68. package/lib/module/core/assets/icons/xml.js +13 -0
  69. package/lib/module/core/assets/icons/xml.js.map +1 -1
  70. package/lib/module/core/legacy/file.js +145 -62
  71. package/lib/module/core/legacy/file.js.map +1 -1
  72. package/lib/module/core/stores/slices/feedSlice.js +3 -0
  73. package/lib/module/core/stores/slices/feedSlice.js.map +1 -1
  74. package/lib/module/core/stores/slices/globalfeedSlice.js +5 -0
  75. package/lib/module/core/stores/slices/globalfeedSlice.js.map +1 -1
  76. package/lib/module/core/stores/slices/localVideoThumbnailSlice.js +41 -0
  77. package/lib/module/core/stores/slices/localVideoThumbnailSlice.js.map +1 -0
  78. package/lib/module/core/stores/slices/uiSlice.js +5 -1
  79. package/lib/module/core/stores/slices/uiSlice.js.map +1 -1
  80. package/lib/module/core/stores/store/index.js +3 -1
  81. package/lib/module/core/stores/store/index.js.map +1 -1
  82. package/lib/module/social/components/LoadingImage/index.js +74 -14
  83. package/lib/module/social/components/LoadingImage/index.js.map +1 -1
  84. package/lib/module/social/components/LoadingImage/styles.js +8 -3
  85. package/lib/module/social/components/LoadingImage/styles.js.map +1 -1
  86. package/lib/module/social/components/LoadingVideo/index.js +91 -17
  87. package/lib/module/social/components/LoadingVideo/index.js.map +1 -1
  88. package/lib/module/social/components/LoadingVideo/styles.js +14 -5
  89. package/lib/module/social/components/LoadingVideo/styles.js.map +1 -1
  90. package/lib/module/social/components/MediaViewer/MediaViewer.js +13 -13
  91. package/lib/module/social/components/MediaViewer/MediaViewer.js.map +1 -1
  92. package/lib/module/social/components/MediaViewer/styles.js +11 -1
  93. package/lib/module/social/components/MediaViewer/styles.js.map +1 -1
  94. package/lib/module/social/components/PostContent/index.js +8 -29
  95. package/lib/module/social/components/PostContent/index.js.map +1 -1
  96. package/lib/module/social/components/SelectedMediaComponent/SelectedMediaComponent.js +110 -18
  97. package/lib/module/social/components/SelectedMediaComponent/SelectedMediaComponent.js.map +1 -1
  98. package/lib/module/social/components/legacy/LoadingVideo/index.js +17 -6
  99. package/lib/module/social/components/legacy/LoadingVideo/index.js.map +1 -1
  100. package/lib/module/social/elements/PostMediaElement/PostMediaElement.js +123 -25
  101. package/lib/module/social/elements/PostMediaElement/PostMediaElement.js.map +1 -1
  102. package/lib/module/social/elements/PostMediaElement/styles.js +7 -0
  103. package/lib/module/social/elements/PostMediaElement/styles.js.map +1 -1
  104. package/lib/module/social/features/community/PendingRequest/components/PendingPostList/PendingPostList.js +12 -6
  105. package/lib/module/social/features/community/PendingRequest/components/PendingPostList/PendingPostList.js.map +1 -1
  106. package/lib/module/social/features/feed/components/NewsFeed/NewsFeed.js +15 -1
  107. package/lib/module/social/features/feed/components/NewsFeed/NewsFeed.js.map +1 -1
  108. package/lib/module/social/features/post/Composer/PostComposer.js +236 -89
  109. package/lib/module/social/features/post/Composer/PostComposer.js.map +1 -1
  110. package/lib/module/social/hooks/index.js +1 -0
  111. package/lib/module/social/hooks/index.js.map +1 -1
  112. package/lib/module/social/hooks/useCommunities.js +5 -3
  113. package/lib/module/social/hooks/useCommunities.js.map +1 -1
  114. package/lib/module/social/hooks/useCommunityPermission.js +37 -0
  115. package/lib/module/social/hooks/useCommunityPermission.js.map +1 -0
  116. package/lib/module/social/hooks/useCustomRankingGlobalFeed.js +10 -1
  117. package/lib/module/social/hooks/useCustomRankingGlobalFeed.js.map +1 -1
  118. package/lib/module/social/hooks/useFile.js +11 -0
  119. package/lib/module/social/hooks/useFile.js.map +1 -1
  120. package/lib/module/social/screens/CommunityProfile/components/Header/Header.js +19 -8
  121. package/lib/module/social/screens/CommunityProfile/components/Header/Header.js.map +1 -1
  122. package/lib/module/social/utils/mediaAttachments.js +139 -0
  123. package/lib/module/social/utils/mediaAttachments.js.map +1 -0
  124. package/lib/module/social/utils/postChildren.js +66 -0
  125. package/lib/module/social/utils/postChildren.js.map +1 -0
  126. package/lib/typescript/core/assets/icons/xml.d.ts +1 -0
  127. package/lib/typescript/core/assets/icons/xml.d.ts.map +1 -1
  128. package/lib/typescript/core/legacy/file.d.ts +32 -0
  129. package/lib/typescript/core/legacy/file.d.ts.map +1 -1
  130. package/lib/typescript/core/stores/slices/feedSlice.d.ts.map +1 -1
  131. package/lib/typescript/core/stores/slices/globalfeedSlice.d.ts.map +1 -1
  132. package/lib/typescript/core/stores/slices/localVideoThumbnailSlice.d.ts +35 -0
  133. package/lib/typescript/core/stores/slices/localVideoThumbnailSlice.d.ts.map +1 -0
  134. package/lib/typescript/core/stores/slices/uiSlice.d.ts +5 -0
  135. package/lib/typescript/core/stores/slices/uiSlice.d.ts.map +1 -1
  136. package/lib/typescript/core/stores/store/index.d.ts.map +1 -1
  137. package/lib/typescript/core/types/mention.d.ts +34 -0
  138. package/lib/typescript/core/types/mention.d.ts.map +1 -1
  139. package/lib/typescript/social/components/LoadingImage/index.d.ts +8 -2
  140. package/lib/typescript/social/components/LoadingImage/index.d.ts.map +1 -1
  141. package/lib/typescript/social/components/LoadingImage/styles.d.ts.map +1 -1
  142. package/lib/typescript/social/components/LoadingVideo/index.d.ts +9 -2
  143. package/lib/typescript/social/components/LoadingVideo/index.d.ts.map +1 -1
  144. package/lib/typescript/social/components/LoadingVideo/styles.d.ts +1 -0
  145. package/lib/typescript/social/components/LoadingVideo/styles.d.ts.map +1 -1
  146. package/lib/typescript/social/components/MediaViewer/MediaViewer.d.ts.map +1 -1
  147. package/lib/typescript/social/components/MediaViewer/styles.d.ts +1 -0
  148. package/lib/typescript/social/components/MediaViewer/styles.d.ts.map +1 -1
  149. package/lib/typescript/social/components/PostContent/index.d.ts.map +1 -1
  150. package/lib/typescript/social/components/SelectedMediaComponent/SelectedMediaComponent.d.ts +2 -2
  151. package/lib/typescript/social/components/SelectedMediaComponent/SelectedMediaComponent.d.ts.map +1 -1
  152. package/lib/typescript/social/components/legacy/LoadingVideo/index.d.ts.map +1 -1
  153. package/lib/typescript/social/elements/PostMediaElement/PostMediaElement.d.ts.map +1 -1
  154. package/lib/typescript/social/elements/PostMediaElement/styles.d.ts +4 -0
  155. package/lib/typescript/social/elements/PostMediaElement/styles.d.ts.map +1 -1
  156. package/lib/typescript/social/features/community/PendingRequest/components/PendingPostList/PendingPostList.d.ts.map +1 -1
  157. package/lib/typescript/social/features/feed/components/NewsFeed/NewsFeed.d.ts.map +1 -1
  158. package/lib/typescript/social/features/post/Composer/PostComposer.d.ts.map +1 -1
  159. package/lib/typescript/social/hooks/index.d.ts +1 -0
  160. package/lib/typescript/social/hooks/index.d.ts.map +1 -1
  161. package/lib/typescript/social/hooks/useCommunities.d.ts +2 -1
  162. package/lib/typescript/social/hooks/useCommunities.d.ts.map +1 -1
  163. package/lib/typescript/social/hooks/useCommunityPermission.d.ts +14 -0
  164. package/lib/typescript/social/hooks/useCommunityPermission.d.ts.map +1 -0
  165. package/lib/typescript/social/hooks/useCustomRankingGlobalFeed.d.ts.map +1 -1
  166. package/lib/typescript/social/hooks/useFile.d.ts +11 -0
  167. package/lib/typescript/social/hooks/useFile.d.ts.map +1 -1
  168. package/lib/typescript/social/screens/CommunityProfile/components/Header/Header.d.ts.map +1 -1
  169. package/lib/typescript/social/utils/mediaAttachments.d.ts +91 -0
  170. package/lib/typescript/social/utils/mediaAttachments.d.ts.map +1 -0
  171. package/lib/typescript/social/utils/postChildren.d.ts +38 -0
  172. package/lib/typescript/social/utils/postChildren.d.ts.map +1 -0
  173. package/package.json +3 -3
  174. package/src/core/assets/icons/post.tsx +1 -1
  175. package/src/core/assets/icons/story.tsx +1 -1
  176. package/src/core/assets/icons/xml.ts +15 -0
  177. package/src/core/legacy/file.tsx +169 -84
  178. package/src/core/stores/slices/feedSlice.ts +3 -0
  179. package/src/core/stores/slices/globalfeedSlice.ts +5 -0
  180. package/src/core/stores/slices/localVideoThumbnailSlice.ts +58 -0
  181. package/src/core/stores/slices/uiSlice.ts +8 -0
  182. package/src/core/stores/store/index.ts +2 -0
  183. package/src/core/types/mention.ts +34 -0
  184. package/src/social/components/LoadingImage/index.tsx +81 -12
  185. package/src/social/components/LoadingImage/styles.ts +8 -3
  186. package/src/social/components/LoadingVideo/index.tsx +111 -21
  187. package/src/social/components/LoadingVideo/styles.ts +13 -4
  188. package/src/social/components/MediaViewer/MediaViewer.tsx +11 -12
  189. package/src/social/components/MediaViewer/styles.ts +9 -1
  190. package/src/social/components/PostContent/index.tsx +3 -35
  191. package/src/social/components/SelectedMediaComponent/SelectedMediaComponent.tsx +103 -13
  192. package/src/social/components/legacy/LoadingVideo/index.tsx +26 -15
  193. package/src/social/elements/PostMediaElement/PostMediaElement.tsx +139 -29
  194. package/src/social/elements/PostMediaElement/styles.ts +7 -0
  195. package/src/social/features/community/PendingRequest/components/PendingPostList/PendingPostList.tsx +11 -6
  196. package/src/social/features/feed/components/NewsFeed/NewsFeed.tsx +16 -1
  197. package/src/social/features/post/Composer/PostComposer.tsx +304 -91
  198. package/src/social/hooks/index.ts +1 -0
  199. package/src/social/hooks/useCommunities.ts +4 -2
  200. package/src/social/hooks/useCommunityPermission.ts +40 -0
  201. package/src/social/hooks/useCustomRankingGlobalFeed.ts +8 -1
  202. package/src/social/hooks/useFile.ts +11 -0
  203. package/src/social/screens/CommunityProfile/components/Header/Header.tsx +19 -10
  204. package/src/social/utils/mediaAttachments.ts +158 -0
  205. package/src/social/utils/postChildren.ts +85 -0
@@ -6,21 +6,26 @@ import {
6
6
  ScrollView,
7
7
  View,
8
8
  } from 'react-native';
9
+ import { createVideoThumbnail } from 'react-native-compressor';
9
10
  import LoadingImage from '../LoadingImage';
10
11
  import LoadingVideo from '../LoadingVideo';
11
12
  import { MediaViewer, type MediaViewerItem } from '../MediaViewer';
12
13
  import { IDisplayImage } from '../../../core/types';
13
14
  import {
14
15
  getFrameRatio,
16
+ getVideoDisplayDims,
15
17
  FRAME_RATIO_VALUE,
16
18
  DEFAULT_FRAME_RATIO,
17
19
  type FrameRatio,
18
20
  } from '../../utils/getFrameRatio';
19
21
  import { useStyles } from './styles';
20
22
 
21
- // Web PR #1830: slide width = calc(100% - 3.4375rem) so the next slide peeks;
22
- // slides are separated by spaceBetween 8. No counter, no dots.
23
- const PEEK_INSET = 55; // 3.4375rem
23
+ // Peek scroll: next slide peeks by (PEEK_INSET − SPACE_BETWEEN). The composer
24
+ // body (PostComposer `inputWrapper`) already insets everything 12px
25
+ // horizontally, so this component adds NO side padding of its own — it only
26
+ // sizes the slide so the peek lands at the spec's 31px (SelectedMediaComponent
27
+ // REQ-003a). slideWidth = trackWidth − PEEK_INSET; peek = PEEK_INSET − 8 = 31.
28
+ const PEEK_INSET = 39;
24
29
  const SPACE_BETWEEN = 8;
25
30
 
26
31
  export type SelectedMediaComponentProps = {
@@ -37,7 +42,10 @@ export type SelectedMediaComponentProps = {
37
42
  ) => void;
38
43
  onUploadError?: (hasError: boolean, source: string) => void;
39
44
  isEditMode?: boolean;
40
- setIsUploading?: (arg: boolean) => void;
45
+ // Per-frame upload state, keyed by the item's `source` url — the parent
46
+ // aggregates it instead of holding one boolean that the first upload to
47
+ // finish would clear on behalf of every other frame.
48
+ onUploadingChange?: (isUploading: boolean, source: string) => void;
41
49
  };
42
50
 
43
51
  export function SelectedMediaComponent({
@@ -47,7 +55,7 @@ export function SelectedMediaComponent({
47
55
  onLoadFinish,
48
56
  onUploadError,
49
57
  isEditMode = false,
50
- setIsUploading,
58
+ onUploadingChange,
51
59
  }: SelectedMediaComponentProps) {
52
60
  const styles = useStyles();
53
61
  const scrollRef = useRef<ScrollView>(null);
@@ -61,14 +69,62 @@ export function SelectedMediaComponent({
61
69
  const aspectRatio = FRAME_RATIO_VALUE[ratio];
62
70
  const slideWidth = trackWidth > 0 ? trackWidth - PEEK_INSET : 0;
63
71
 
64
- // Frames follow the aspect ratio of the first media item; recalculated when
65
- // the first item changes (e.g. it was removed).
66
- const firstUrl = media[0]?.url;
72
+ // Frames follow the aspect ratio classified from the item currently in first
73
+ // position (REQ-003d/003e); recalculated when the first item changes.
74
+ const first = media[0];
75
+ const firstUrl = first?.url;
76
+ const firstWidth = first?.width;
77
+ const firstHeight = first?.height;
78
+ const firstRotation = first?.rotation;
67
79
  useEffect(() => {
68
- if (mediaType !== 'image' || !firstUrl) {
80
+ if (!firstUrl) {
69
81
  setRatio(DEFAULT_FRAME_RATIO);
70
82
  return undefined;
71
83
  }
84
+ // Video: classify from the record/extractor dims, applying rotation
85
+ // (REQ-003d1 — MUST NOT silently fall back to 1:1 for video). Shares the
86
+ // one rotation derivation with the published carousel (REQ-SDK-002b).
87
+ if (mediaType === 'video') {
88
+ if (firstWidth && firstHeight) {
89
+ const { width, height } = getVideoDisplayDims({
90
+ width: firstWidth,
91
+ height: firstHeight,
92
+ rotation: firstRotation,
93
+ });
94
+ setRatio(getFrameRatio(width, height));
95
+ return undefined;
96
+ }
97
+ // No usable dims — a locally-picked video can arrive with none (the
98
+ // Android extractor reports 0×0 when MediaMetadataRetriever fails, and
99
+ // iOS picks carry none by design, see PostComposer). Measure the
100
+ // generated thumbnail instead of defaulting to a square frame
101
+ // (REQ-003d2, PDT-4904): it is a rendered frame, so its dimensions are
102
+ // already display-oriented and need no rotation applied.
103
+ //
104
+ // LoadingVideo decodes a thumbnail for this same url too, so frame 0
105
+ // decodes twice. Kept deliberately: reporting dims back up through
106
+ // LoadingVideo would widen the composer's callback surface, and the
107
+ // decode is a cached native call on a url the child already proves
108
+ // works.
109
+ let active = true;
110
+ createVideoThumbnail(firstUrl)
111
+ .then(({ width, height }) => {
112
+ if (active) setRatio(getFrameRatio(width, height));
113
+ })
114
+ .catch(() => {
115
+ // Thumbnail decode failed too — nothing left to classify from, so
116
+ // fall back to the square frame (REQ-003d2).
117
+ if (active) setRatio(DEFAULT_FRAME_RATIO);
118
+ });
119
+ return () => {
120
+ active = false;
121
+ };
122
+ }
123
+ // Image: prefer carried dims, else measure the source.
124
+ if (firstWidth && firstHeight) {
125
+ setRatio(getFrameRatio(firstWidth, firstHeight));
126
+ return undefined;
127
+ }
72
128
  let active = true;
73
129
  Image.getSize(
74
130
  firstUrl,
@@ -82,7 +138,7 @@ export function SelectedMediaComponent({
82
138
  return () => {
83
139
  active = false;
84
140
  };
85
- }, [firstUrl, mediaType]);
141
+ }, [firstUrl, firstWidth, firstHeight, firstRotation, mediaType]);
86
142
 
87
143
  const onLayout = (event: LayoutChangeEvent) => {
88
144
  const width = event.nativeEvent.layout.width;
@@ -94,6 +150,24 @@ export function SelectedMediaComponent({
94
150
  uri: m.url,
95
151
  }));
96
152
 
153
+ // Removing a frame must not leave the strip parked past the last one. The
154
+ // ScrollView keeps its offset while the content shrinks underneath it, so
155
+ // deleting the frame that was on screen left a half-scrolled gap where it had
156
+ // been instead of settling on the frame that took its place.
157
+ const offsetRef = useRef(0);
158
+ const previousTotalRef = useRef(total);
159
+ useEffect(() => {
160
+ const previousTotal = previousTotalRef.current;
161
+ previousTotalRef.current = total;
162
+ if (total >= previousTotal || slideWidth <= 0) return;
163
+
164
+ const maxOffset = Math.max(0, (total - 1) * (slideWidth + SPACE_BETWEEN));
165
+ if (offsetRef.current <= maxOffset) return;
166
+
167
+ offsetRef.current = maxOffset;
168
+ scrollRef.current?.scrollTo({ x: maxOffset, animated: false });
169
+ }, [total, slideWidth]);
170
+
97
171
  // PDT-4310/4312: on close, return to the frame originally tapped from — not
98
172
  // the last frame viewed in full screen.
99
173
  const handleCloseViewer = () => {
@@ -113,6 +187,7 @@ export function SelectedMediaComponent({
113
187
  <LoadingVideo
114
188
  carousel
115
189
  source={item.url}
190
+ displayUri={item.localUrl}
116
191
  onClose={onClose}
117
192
  index={index}
118
193
  onLoadFinish={onLoadFinish}
@@ -123,7 +198,7 @@ export function SelectedMediaComponent({
123
198
  fileCount={total}
124
199
  isEditMode={isEditMode}
125
200
  postId={item.postId}
126
- setIsUploading={setIsUploading}
201
+ onUploadingChange={onUploadingChange}
127
202
  onPlay={() => setViewerIndex(index)}
128
203
  />
129
204
  ) : (
@@ -134,6 +209,7 @@ export function SelectedMediaComponent({
134
209
  <LoadingImage
135
210
  carousel
136
211
  source={item.url}
212
+ displayUri={item.localUrl}
137
213
  onClose={onClose}
138
214
  index={index}
139
215
  onLoadFinish={onLoadFinish}
@@ -143,7 +219,7 @@ export function SelectedMediaComponent({
143
219
  fileCount={total}
144
220
  isEditMode={isEditMode}
145
221
  postId={item.postId}
146
- setIsUploading={setIsUploading}
222
+ onUploadingChange={onUploadingChange}
147
223
  />
148
224
  </Pressable>
149
225
  )}
@@ -171,10 +247,24 @@ export function SelectedMediaComponent({
171
247
  decelerationRate="fast"
172
248
  snapToInterval={slideWidth + SPACE_BETWEEN}
173
249
  snapToAlignment="start"
250
+ scrollEventThrottle={16}
251
+ onScroll={(event) => {
252
+ offsetRef.current = event.nativeEvent.contentOffset.x;
253
+ }}
174
254
  >
175
255
  {media.map((item, index) => (
176
256
  <View
177
- key={`${item.url}-${index}`}
257
+ // Key on the item's stable local identity, not on its
258
+ // url. `url` is rewritten from the local pick path
259
+ // to the remote `?size=medium` one the instant that frame's
260
+ // upload finishes, so a url-based key tore down and
261
+ // remounted the slide — and with it the child's mount
262
+ // effect, which is what re-runs uploads and re-orders the
263
+ // in-flight completion callbacks against their siblings.
264
+ // The identity survives the swap, so a finishing upload
265
+ // now only re-renders its own frame. Falls back to the old
266
+ // key for entries built without one (legacy EditPostModal).
267
+ key={item.localId ?? `${item.url}-${index}`}
178
268
  style={[
179
269
  styles.selectedMedia__slide,
180
270
  {
@@ -89,24 +89,35 @@ const LoadingVideo = ({
89
89
  }, [progress]);
90
90
 
91
91
  const uploadFileToAmity = useCallback(async () => {
92
- const file: Amity.File<any>[] = await uploadVideoFile(
93
- source,
94
- (percent: number) => {
95
- setProgress(percent);
92
+ // `uploadVideoFile` now rejects on a failed upload instead of
93
+ // hanging on an unsettled promise, so this call needs a catch. This legacy
94
+ // component has no error UI to show (unlike its v4 counterpart, which
95
+ // renders a tappable retry overlay) — the least-bad end state is to stop
96
+ // the spinner so the frame is not stuck pretending to make progress, and
97
+ // to leave the close button reachable so the user can drop the attachment.
98
+ try {
99
+ const file: Amity.File<any>[] = await uploadVideoFile(
100
+ source,
101
+ (percent: number) => {
102
+ setProgress(percent);
103
+ }
104
+ );
105
+ if (file) {
106
+ setIsProcess(false);
107
+ handleLoadEnd();
108
+ onLoadFinish &&
109
+ onLoadFinish(
110
+ file[0]?.fileId as string,
111
+ file[0]?.fileUrl as string,
112
+ file[0]?.attributes.name as string,
113
+ index as number,
114
+ source,
115
+ thumbNail
116
+ );
96
117
  }
97
- );
98
- if (file) {
118
+ } catch (error) {
99
119
  setIsProcess(false);
100
120
  handleLoadEnd();
101
- onLoadFinish &&
102
- onLoadFinish(
103
- file[0]?.fileId as string,
104
- file[0]?.fileUrl as string,
105
- file[0]?.attributes.name as string,
106
- index as number,
107
- source,
108
- thumbNail
109
- );
110
121
  }
111
122
  }, [source]);
112
123
 
@@ -1,5 +1,12 @@
1
- import { useMemo, useRef, useState, type MutableRefObject } from 'react';
2
1
  import {
2
+ useEffect,
3
+ useMemo,
4
+ useRef,
5
+ useState,
6
+ type MutableRefObject,
7
+ } from 'react';
8
+ import {
9
+ Animated,
3
10
  Image,
4
11
  ImageStyle,
5
12
  LayoutChangeEvent,
@@ -12,6 +19,7 @@ import {
12
19
  View,
13
20
  } from 'react-native';
14
21
  import { SvgXml } from 'react-native-svg';
22
+ import { FileRepository } from '@amityco/ts-sdk-react-native';
15
23
  import { Typography } from '../../../core/components/Typography/Typography';
16
24
  import useAuth from '../../../core/hooks/useAuth';
17
25
  import {
@@ -26,6 +34,9 @@ import {
26
34
  type FrameRatio,
27
35
  } from '../../utils/getFrameRatio';
28
36
  import { getIndicatorConfig } from '../../utils/getIndicatorConfig';
37
+ import { ImageSizeState } from '../../enums/imageSizeState';
38
+ import { useUIKitSelector } from '../../../core/stores/store';
39
+ import type { RootState } from '../../../core/stores/store';
29
40
  import { useStyles } from './styles';
30
41
 
31
42
  type MediaPost = Amity.Post<'image'> | Amity.Post<'video'>;
@@ -81,6 +92,34 @@ export function PostMediaElement({
81
92
  const mediaPosts = useMemo(() => (posts ?? []).filter(isMediaPost), [posts]);
82
93
  const ratio = useMemo(() => getAttachmentRatio(mediaPosts[0]), [mediaPosts]);
83
94
 
95
+ // A reload starts the carousel over. It cannot be left to unmounting: the
96
+ // feed keys its items by post id, so a reload reuses this instance and both
97
+ // `current` and the ScrollView's own offset would otherwise survive it,
98
+ // leaving the viewer on whichever frame they had reached before.
99
+ const feedReloadToken = useUIKitSelector(
100
+ (state: RootState) => state.ui.feedReloadToken
101
+ );
102
+ useEffect(() => {
103
+ setCurrent(0);
104
+ scrollRef.current?.scrollTo({ x: 0, animated: false });
105
+ }, [feedReloadToken]);
106
+
107
+ // An edit that removes frames must not leave the carousel pointing past the
108
+ // end of what is left. The feed reuses this instance across the edit, so both
109
+ // `current` and the ScrollView's own offset outlive the frame they referred
110
+ // to: the counter read "4/3" over an empty frame, and the indicator drew no
111
+ // active dot at all because no page matched the index.
112
+ const frameCount = mediaPosts.length;
113
+ useEffect(() => {
114
+ const lastIndex = Math.max(0, frameCount - 1);
115
+ if (current <= lastIndex) return;
116
+ setCurrent(lastIndex);
117
+ scrollRef.current?.scrollTo({
118
+ x: lastIndex * (trackWidth || screenWidth),
119
+ animated: false,
120
+ });
121
+ }, [frameCount, current, trackWidth, screenWidth]);
122
+
84
123
  if (mediaPosts.length === 0) {
85
124
  if (!loading) return null;
86
125
  // Loading placeholder: a full-bleed frame in the base-shade4 colour, so the
@@ -197,20 +236,57 @@ function PaginationIndicator({ current, total }: PaginationIndicatorProps) {
197
236
 
198
237
  return (
199
238
  <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
- ))}
239
+ {dots.map((dot) =>
240
+ dot.state === 'edge' ? (
241
+ <EdgeDot key={dot.key} side={dot.side} />
242
+ ) : (
243
+ <View key={dot.key} style={styles.postMedia__dotSlot}>
244
+ <View
245
+ style={[
246
+ styles.postMedia__dot,
247
+ dot.state === 'active'
248
+ ? styles.postMedia__dotActive
249
+ : styles.postMedia__dotInactive,
250
+ ]}
251
+ />
252
+ </View>
253
+ )
254
+ )}
255
+ </View>
256
+ );
257
+ }
258
+
259
+ // The strip keeps a constant width (fixed-width slots, always 6 elements), so
260
+ // an edge dot only needs to slide in from its own side over 300ms when it
261
+ // first appears (REQ-013b).
262
+ function EdgeDot({ side }: { side?: 'left' | 'right' }) {
263
+ const styles = useStyles();
264
+ const anim = useRef(new Animated.Value(0)).current;
265
+
266
+ useEffect(() => {
267
+ const animation = Animated.timing(anim, {
268
+ toValue: 1,
269
+ duration: 300,
270
+ useNativeDriver: true,
271
+ });
272
+ animation.start();
273
+ return () => animation.stop();
274
+ }, [anim]);
275
+
276
+ const translateX = anim.interpolate({
277
+ inputRange: [0, 1],
278
+ outputRange: [side === 'left' ? -6 : 6, 0],
279
+ });
280
+
281
+ return (
282
+ <View style={styles.postMedia__dotSlot}>
283
+ <Animated.View
284
+ style={[
285
+ styles.postMedia__dot,
286
+ styles.postMedia__dotEdge,
287
+ { opacity: anim, transform: [{ translateX }] },
288
+ ]}
289
+ />
214
290
  </View>
215
291
  );
216
292
  }
@@ -260,20 +336,22 @@ type ImageMediaProps = { post: Amity.Post<'image'> };
260
336
  function ImageMedia({ post }: ImageMediaProps) {
261
337
  const styles = useStyles();
262
338
  const { apiRegion } = useAuth();
263
- const [isBroken, setIsBroken] = useState(false);
264
339
  const fileId = post.getImageInfo()?.fileId;
265
340
  const url = fileId
266
341
  ? `https://api.${apiRegion}.amity.co/api/v3/files/${fileId}/download?size=large`
267
342
  : undefined;
343
+ // Keyed to the url rather than a bare boolean, so replacing the image retries
344
+ // the load instead of staying broken for the session (see VideoMedia).
345
+ const [brokenUrl, setBrokenUrl] = useState<string>();
268
346
 
269
- if (!url || isBroken) return <BrokenFrame />;
347
+ if (!url || brokenUrl === url) return <BrokenFrame />;
270
348
 
271
349
  return (
272
350
  <Image
273
351
  style={styles.postMedia__media as StyleProp<ImageStyle>}
274
352
  source={{ uri: url }}
275
353
  resizeMode="cover"
276
- onError={() => setIsBroken(true)}
354
+ onError={() => setBrokenUrl(url)}
277
355
  />
278
356
  );
279
357
  }
@@ -282,16 +360,50 @@ type VideoMediaProps = { post: Amity.Post<'video'> };
282
360
 
283
361
  function VideoMedia({ post }: VideoMediaProps) {
284
362
  const styles = useStyles();
285
- const { apiRegion } = useAuth();
286
- const [isBroken, setIsBroken] = useState(false);
287
363
 
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`
364
+ // Resolve the thumbnail through the SDK's linked file object rather than
365
+ // building a download url from `data.thumbnailFileId`. The file behind that
366
+ // id does not exist until the server finishes transcoding, so a url built
367
+ // from the id alone is well-formed and still 404s — which is what rendered a
368
+ // broken-image icon on every just-created video post. The file object is the
369
+ // honest signal: it is absent until the thumbnail actually exists. Mirrors
370
+ // web's useImage/useFile.
371
+ const thumbnail = post.getVideoThumbnailInfo?.();
372
+ const serverUrl = thumbnail?.fileUrl
373
+ ? FileRepository.fileUrlWithSize(thumbnail.fileUrl, ImageSizeState.large)
292
374
  : undefined;
293
375
 
294
- if (!url || isBroken) return <BrokenFrame />;
376
+ // While the server has none, borrow the frame the composer decoded when this
377
+ // video was picked, matched on the original video's fileId (web parity:
378
+ // VideoContent falls back to LayoutProvider's videoThumbnail the same way).
379
+ const localThumbnails = useUIKitSelector(
380
+ (state: RootState) => state.localVideoThumbnail.videos
381
+ );
382
+ const originalFileId = (post.data as Amity.ContentDataVideo | undefined)
383
+ ?.videoFileId?.original;
384
+ const localUrl = serverUrl
385
+ ? undefined
386
+ : localThumbnails.find(({ fileId }) => fileId === originalFileId)
387
+ ?.thumbnailUrl;
388
+
389
+ const url = serverUrl ?? localUrl;
390
+
391
+ // Keyed to the url, not a bare boolean: the url changes when the real
392
+ // thumbnail lands, and a latched flag would keep the frame broken for the
393
+ // rest of the session.
394
+ const [brokenUrl, setBrokenUrl] = useState<string>();
395
+
396
+ const playButton = (
397
+ <View style={styles.postMedia__playButton}>
398
+ <SvgXml xml={videoControlIcon} width="40" height="40" />
399
+ </View>
400
+ );
401
+
402
+ // Nothing to show yet, from either source: still a normal state for a new
403
+ // post, so keep the play affordance rather than flagging it broken.
404
+ if (!url) return <View style={styles.postMedia__pending}>{playButton}</View>;
405
+
406
+ if (brokenUrl === url) return <BrokenFrame />;
295
407
 
296
408
  return (
297
409
  <>
@@ -299,11 +411,9 @@ function VideoMedia({ post }: VideoMediaProps) {
299
411
  style={styles.postMedia__media as StyleProp<ImageStyle>}
300
412
  source={{ uri: url }}
301
413
  resizeMode="cover"
302
- onError={() => setIsBroken(true)}
414
+ onError={() => setBrokenUrl(url)}
303
415
  />
304
- <View style={styles.postMedia__playButton}>
305
- <SvgXml xml={videoControlIcon} width="40" height="40" />
306
- </View>
416
+ {playButton}
307
417
  </>
308
418
  );
309
419
  }
@@ -48,6 +48,13 @@ export const useStyles = () => {
48
48
  justifyContent: 'center',
49
49
  backgroundColor: theme.colors.baseShade4,
50
50
  },
51
+ // A video whose thumbnail the server has not produced yet: same plain
52
+ // container as the broken frame, but it carries the play button instead of
53
+ // the broken-image glyph.
54
+ postMedia__pending: {
55
+ flex: 1,
56
+ backgroundColor: theme.colors.baseShade4,
57
+ },
51
58
  postMedia__counter: {
52
59
  position: 'absolute',
53
60
  top: 8, // web: 0.5rem
@@ -19,7 +19,7 @@ import { usePostCollection } from '../../../../../hooks/collections/post/usePost
19
19
  import ActionButton from '../../../../../elements/ActionButton';
20
20
  import { usePendingPostQuery } from '../../../../../hooks/usePendingPostQuery';
21
21
  import { BrandBadge } from '../../../../../elements/BrandBadge';
22
- import { isModerator, useUser } from '../../../../../hooks';
22
+ import { useCommunityPermission } from '../../../../../hooks';
23
23
 
24
24
  import { SvgXml } from 'react-native-svg';
25
25
  import { Title } from '../../../../../elements';
@@ -50,8 +50,13 @@ const usePendingPostList = ({
50
50
  },
51
51
  });
52
52
 
53
- const currentUser = useUser(client.userId || '');
54
- const isCommunityModerator = isModerator(currentUser?.roles);
53
+ // Reviewing a community's posts is a property of the membership, not of the
54
+ // user: a moderator of one community carries no moderator role on their own
55
+ // user object, so reading the user's roles hid the accept/decline buttons
56
+ // from every moderator who was not also a network-wide admin.
57
+ const { canReviewCommunityPosts } = useCommunityPermission(
58
+ community.communityId
59
+ );
55
60
 
56
61
  useEffect(() => {
57
62
  onPendingPostCountChange?.(collection.data.length);
@@ -91,7 +96,7 @@ const usePendingPostList = ({
91
96
  renderDivider,
92
97
  openBottomSheet,
93
98
  handleDeletePost,
94
- isCommunityModerator,
99
+ canReviewCommunityPosts,
95
100
  };
96
101
  };
97
102
 
@@ -110,7 +115,7 @@ const PendingPostList = ({
110
115
  renderDivider,
111
116
  openBottomSheet,
112
117
  handleDeletePost,
113
- isCommunityModerator,
118
+ canReviewCommunityPosts,
114
119
  } = usePendingPostList({ community, onPendingPostCountChange });
115
120
 
116
121
  return (
@@ -221,7 +226,7 @@ const PendingPostList = ({
221
226
  mentionPositionArr={item?.metadata?.mentioned ?? []}
222
227
  />
223
228
  </View>
224
- {isCommunityModerator && (
229
+ {canReviewCommunityPosts && (
225
230
  <>
226
231
  <View style={styles.divider} />
227
232
  <View style={styles.actionContainer}>
@@ -1,5 +1,6 @@
1
- import { FC, memo } from 'react';
1
+ import { FC, memo, useCallback } from 'react';
2
2
  import { View } from 'react-native';
3
+ import { useFocusEffect } from '@react-navigation/native';
3
4
  import { ComponentID, PageID } from '../../../../enums/enumUIKitID';
4
5
  import AmityGlobalFeedComponent from '../GlobalFeed/GlobalFeed';
5
6
  import { useStyles } from './styles';
@@ -7,6 +8,8 @@ import { useAmityComponent } from '../../../../hooks';
7
8
  import { useCustomRankingGlobalFeed } from '../../../../hooks/useCustomRankingGlobalFeed';
8
9
  import NewsFeedLoadingComponent from '../../../../components/NewsFeedLoadingComponent/NewsFeedLoadingComponent';
9
10
  import { AmityEmptyNewsFeedComponent } from '../../../..';
11
+ import uiSlice from '../../../../../core/stores/slices/uiSlice';
12
+ import { useUIKitDispatch } from '../../../../../core/stores/store';
10
13
 
11
14
  type AmityNewsFeedComponentType = {
12
15
  pageId?: PageID;
@@ -27,6 +30,18 @@ const AmityNewsFeedComponent: FC<AmityNewsFeedComponentType> = ({
27
30
  const { itemWithAds, refresh, globalFeedPosts, loading, onNextPage } =
28
31
  useCustomRankingGlobalFeed();
29
32
 
33
+ const dispatch = useUIKitDispatch();
34
+ const { markFeedReloaded } = uiSlice.actions;
35
+
36
+ // Coming back to the feed is a reload as far as the viewer is concerned, but
37
+ // the screen is never unmounted, so nothing here re-runs on its own. Say so
38
+ // explicitly, the same way pull-to-refresh does.
39
+ useFocusEffect(
40
+ useCallback(() => {
41
+ dispatch(markFeedReloaded());
42
+ }, [dispatch, markFeedReloaded])
43
+ );
44
+
30
45
  if (isExcluded) return null;
31
46
 
32
47
  if (