@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
@@ -5,10 +5,12 @@ export const useCommunities = ({
5
5
  categoryId,
6
6
  membership = 'member',
7
7
  limit = 20,
8
+ sortBy = 'displayName',
8
9
  }: {
9
10
  membership?: 'all' | 'member' | 'notMember';
10
11
  categoryId?: string;
11
12
  limit?: number;
13
+ sortBy?: Amity.CommunitySortBy;
12
14
  } = {}) => {
13
15
  const [communities, setCommunities] = useState<Amity.Community[]>();
14
16
  const [loading, setLoading] = useState(true);
@@ -16,7 +18,7 @@ export const useCommunities = ({
16
18
  useState<() => void | null>(null);
17
19
  useEffect(() => {
18
20
  const unsubscribe = CommunityRepository.getCommunities(
19
- { membership, limit, categoryId },
21
+ { membership, limit, categoryId, sortBy },
20
22
  ({ error, loading, data, hasNextPage, onNextPage }) => {
21
23
  if (error) return;
22
24
  if (!loading) {
@@ -31,6 +33,6 @@ export const useCommunities = ({
31
33
  }
32
34
  );
33
35
  return unsubscribe;
34
- }, [categoryId, membership, limit]);
36
+ }, [categoryId, membership, limit, sortBy]);
35
37
  return { communities, onNextCommunityPage, loading };
36
38
  };
@@ -0,0 +1,40 @@
1
+ import { Client } from '@amityco/ts-sdk-react-native';
2
+ import { MemberRoles } from '../../core/constants';
3
+ import { useCommunityMemberCollection } from './collections/community/useCommunityMemberCollection';
4
+ import { useUser } from './useUser';
5
+ import { isAdmin } from '../utils/permissions';
6
+
7
+ /**
8
+ * Who may review a community's pending posts, resolved the way web resolves it
9
+ * (useCommunityPermission + useCommunityModeratorsCollection).
10
+ *
11
+ * Moderating a community is a property of the membership, not of the user: a
12
+ * moderator of one community carries no moderator role on their own user
13
+ * object. Reading the user's roles therefore misses every moderator who is not
14
+ * also a network-wide one.
15
+ */
16
+ export const useCommunityPermission = (
17
+ communityId?: Amity.Community['communityId']
18
+ ) => {
19
+ const myId = Client.getActiveClient()?.userId;
20
+ const currentUser = useUser(myId ?? '');
21
+
22
+ const { members: moderators } = useCommunityMemberCollection({
23
+ enabled: !!communityId,
24
+ params: {
25
+ communityId: communityId as string,
26
+ roles: [MemberRoles.COMMUNITY_MODERATOR],
27
+ limit: 20,
28
+ },
29
+ });
30
+
31
+ const isCommunityModerator = !!moderators?.some(
32
+ (moderator) => moderator.userId === myId
33
+ );
34
+ const isGlobalAdmin = isAdmin(currentUser?.roles);
35
+
36
+ return {
37
+ isCommunityModerator,
38
+ canReviewCommunityPosts: isGlobalAdmin || isCommunityModerator,
39
+ };
40
+ };
@@ -4,6 +4,7 @@ import {
4
4
  } from '@amityco/ts-sdk-react-native';
5
5
  import { useCallback, useEffect, useRef, useState } from 'react';
6
6
  import globalFeedSlice from '../../core/stores/slices/globalfeedSlice';
7
+ import uiSlice from '../../core/stores/slices/uiSlice';
7
8
  import { globalFeedPageLimit } from '../features/feed/components/GlobalFeed/GlobalFeed';
8
9
  import { InteractionManager } from 'react-native';
9
10
  import {
@@ -35,6 +36,7 @@ export const useCustomRankingGlobalFeed = ({
35
36
  const interactionHandleRef = useRef<{ cancel: () => void } | null>(null);
36
37
 
37
38
  const { setNewGlobalFeed } = globalFeedSlice.actions;
39
+ const { markFeedReloaded } = uiSlice.actions;
38
40
 
39
41
  const [fetching, setFetching] = useState(false);
40
42
  const [error, setError] = useState(false);
@@ -112,8 +114,13 @@ export const useCustomRankingGlobalFeed = ({
112
114
  onNextPageRef.current = null;
113
115
  hasInitialDataRef.current = false;
114
116
 
117
+ // The list keys posts by id, so a reload hands the same component
118
+ // instances back their old state instead of remounting them. Announce the
119
+ // reload so anything holding a position can start over.
120
+ dispatch(markFeedReloaded());
121
+
115
122
  unsubscribeRef.current = fetchCustomRanking();
116
- }, [fetchCustomRanking]);
123
+ }, [dispatch, fetchCustomRanking, markFeedReloaded]);
117
124
 
118
125
  return {
119
126
  loading: fetching,
@@ -12,6 +12,17 @@ interface useFileProps {
12
12
  type?: 'user' | 'community';
13
13
  }
14
14
 
15
+ /**
16
+ * Resolves an **avatar** file to a display url.
17
+ *
18
+ * A miss — no id, no file, no url — answers with the default avatar (or
19
+ * community avatar) image, which is the right placeholder for a profile
20
+ * picture and the wrong one for anything else. Post media must not be resolved
21
+ * through here: handing an avatar back for a video whose thumbnail is still
22
+ * transcoding renders a person icon in the carousel as though it were the
23
+ * frame. Resolve media with FileRepository directly and let an unresolved file
24
+ * stay undefined.
25
+ */
15
26
  export const useFile = () => {
16
27
  const getImage = useCallback(
17
28
  async ({
@@ -4,8 +4,7 @@ import { ComponentID, PageID } from '../../../../enums';
4
4
  import {
5
5
  useAmityComponent,
6
6
  useCommunity,
7
- isModerator,
8
- useUser,
7
+ useCommunityPermission,
9
8
  useGlobalBehavior,
10
9
  } from '../../../../hooks';
11
10
  import { useStyles } from './styles';
@@ -64,8 +63,21 @@ const AmityCommunityHeaderComponent: FC<AmityCommunityHeaderComponentProps> = ({
64
63
  componentId,
65
64
  });
66
65
 
67
- const currentUser = useUser(client?.userId || '');
68
- const isCommunityModerator = isModerator(currentUser?.roles);
66
+ const { canReviewCommunityPosts } = useCommunityPermission(communityId);
67
+
68
+ // A community may still be on the legacy flag when the SDK has not mapped it
69
+ // to `postSetting`, and reading `postSetting` alone hid the banner for those
70
+ // communities. Same fallback web applies.
71
+ const requiresPostApproval = community?.postSetting
72
+ ? community.postSetting === 'ADMIN_REVIEW_POST_REQUIRED'
73
+ : !!(community as Record<string, any>)?.needApprovalOnPostCreation;
74
+
75
+ // `postedUserId` is the field web reads; `creator` is what this screen read
76
+ // before. Keep both — the author of a pending post must not lose the banner
77
+ // if one of the two is absent from a payload.
78
+ const isPendingPostOwner = !!pendingPosts?.some(
79
+ (post) => (post.postedUserId ?? post.creator?.userId) === client?.userId
80
+ );
69
81
 
70
82
  const styles = useStyles(themeStyles);
71
83
 
@@ -174,18 +186,15 @@ const AmityCommunityHeaderComponent: FC<AmityCommunityHeaderComponentProps> = ({
174
186
  />
175
187
  </View>
176
188
  )}
177
- {(isCommunityModerator ||
178
- pendingPosts?.some(
179
- (post) => post.creator?.userId === client.userId
180
- )) &&
189
+ {(canReviewCommunityPosts || isPendingPostOwner) &&
181
190
  pendingPosts?.length > 0 &&
182
191
  community.isJoined &&
183
- community?.postSetting === 'ADMIN_REVIEW_POST_REQUIRED' && (
192
+ requiresPostApproval && (
184
193
  <CommunityPendingPost
185
194
  number={pendingPosts.length}
186
195
  pageId={pageId}
187
196
  componentId={componentId}
188
- isModerator={isCommunityModerator}
197
+ isModerator={canReviewCommunityPosts}
189
198
  style={styles.pendingPostWrap}
190
199
  onPress={() => {
191
200
  navigation.navigate('CommunityPendingRequest', {
@@ -0,0 +1,158 @@
1
+ import type { IDisplayImage } from '../../core/types';
2
+
3
+ /**
4
+ * The composer caps a post at 10 attachments of a single media type.
5
+ */
6
+ export const MAX_MEDIA_ATTACHMENTS = 10;
7
+
8
+ /**
9
+ * The identity fields a staged attachment must carry for the helpers below to
10
+ * recognise it across its own upload. See `IDisplayImage.localId`.
11
+ */
12
+ export type MediaPickCandidate = IDisplayImage & {
13
+ localId: string;
14
+ };
15
+
16
+ /**
17
+ * Drop the candidates that are already staged, and any repeated inside the
18
+ * batch itself, preserving the order of the ones that survive.
19
+ *
20
+ * De-duplication runs against the LOCAL identity, never `fileName`: the moment
21
+ * an item finishes uploading the composer rewrites its `fileName` to the
22
+ * server's `attributes.name`, and edit-mode entries are hydrated with the
23
+ * fileId, so a set built from `fileName` compares server-side names against
24
+ * picker names and never matches. `localId` / `localFileName` are assigned once
25
+ * at pick time and survive the upload, so they still line up here.
26
+ *
27
+ * `localFileName` is a second key next to `localId` because the picker's
28
+ * `asset.id` is not populated by every configuration — the name is what catches
29
+ * a re-pick when no id came back. Camera captures deliberately carry no
30
+ * `localFileName`, so two shots never de-duplicate against each other.
31
+ *
32
+ * The drop is silent by design (no toast, no alert) and leaves the already
33
+ * staged copy exactly where it is, which is what keeps a re-pick of A followed
34
+ * by B and C rendering as A, B, C.
35
+ */
36
+ export function dedupeMediaPicks<T extends MediaPickCandidate>(
37
+ staged: IDisplayImage[],
38
+ candidates: T[]
39
+ ): T[] {
40
+ const seenIds = new Set(
41
+ staged.map((item) => item.localId).filter(Boolean) as string[]
42
+ );
43
+ const seenNames = new Set(
44
+ staged.map((item) => item.localFileName).filter(Boolean) as string[]
45
+ );
46
+
47
+ const additions: T[] = [];
48
+ for (const candidate of candidates) {
49
+ const { localId, localFileName } = candidate;
50
+ if (seenIds.has(localId)) continue;
51
+ if (localFileName && seenNames.has(localFileName)) continue;
52
+ seenIds.add(localId);
53
+ if (localFileName) seenNames.add(localFileName);
54
+ additions.push(candidate);
55
+ }
56
+ return additions;
57
+ }
58
+
59
+ /**
60
+ * Append `additions` to `staged`, holding the 10-attachment cap.
61
+ */
62
+ export function appendWithinCap(
63
+ staged: IDisplayImage[],
64
+ additions: IDisplayImage[]
65
+ ): IDisplayImage[] {
66
+ const updated = [...staged, ...additions];
67
+ return updated.length > MAX_MEDIA_ATTACHMENTS
68
+ ? updated.slice(0, MAX_MEDIA_ATTACHMENTS)
69
+ : updated;
70
+ }
71
+
72
+ /**
73
+ * Route a finished upload back to the entry it actually belongs to, matched by
74
+ * the local path the child was mounted with.
75
+ *
76
+ * The child reports both a positional `index` and the `originalPath`. The index
77
+ * is the array position the frame was RENDERED at when the upload started —
78
+ * LoadingVideo goes further and memoises its uploader on `[source]` alone, so
79
+ * it freezes the index of the render where the source last changed. Writing
80
+ * `data[index]` therefore lands on whatever now sits at that position: a frame
81
+ * removed, an upload retried after the array grew, or a slow upload finishing
82
+ * after its neighbours moved all put one file's remote url onto another file's
83
+ * entry — which is what QA saw as an already-loaded image repainting with a
84
+ * different picture once the network came back. Matching on `originalPath` is
85
+ * exact: a not-yet-uploaded entry's `url` IS the local path its child was
86
+ * handed, and pick de-duplication guarantees those are unique across the array.
87
+ *
88
+ * The patch is MERGED rather than replacing the entry: `localId` /
89
+ * `localFileName` are what the next pick de-duplicates against, and
90
+ * what the carousel keys its frames by, and the picker's width/height still
91
+ * classify the frame ratio — a wholesale replacement dropped all of them.
92
+ *
93
+ * No match means the entry is gone (closed mid-upload) or already finished, so
94
+ * the array is returned untouched rather than resurrecting or double-writing.
95
+ */
96
+ export function applyFinishedUpload(
97
+ staged: IDisplayImage[],
98
+ originalPath: string,
99
+ patch: Partial<IDisplayImage>
100
+ ): IDisplayImage[] {
101
+ const targetIndex = staged.findIndex((item) => item.url === originalPath);
102
+ if (targetIndex === -1) return staged;
103
+ const updated = [...staged];
104
+ updated[targetIndex] = { ...updated[targetIndex], ...patch };
105
+ return updated;
106
+ }
107
+
108
+ /**
109
+ * Add or remove a media item's `source` from the set of in-flight uploads.
110
+ *
111
+ * The composer gates Post/Save on this set being empty. It replaced a single
112
+ * shared boolean that whichever upload finished FIRST flipped back to false on
113
+ * behalf of every other frame, unlocking Post while the rest were still
114
+ * uploading.
115
+ *
116
+ * Returns the same set instance when nothing changes, so a redundant report —
117
+ * a child clearing on unmount right after `handleLoadEnd` already cleared —
118
+ * does not trigger a re-render.
119
+ */
120
+ export function toggleUploadingSource(
121
+ uploading: Set<string>,
122
+ source: string,
123
+ isUploading: boolean
124
+ ): Set<string> {
125
+ if (isUploading === uploading.has(source)) return uploading;
126
+ const updated = new Set(uploading);
127
+ if (isUploading) {
128
+ updated.add(source);
129
+ } else {
130
+ updated.delete(source);
131
+ }
132
+ return updated;
133
+ }
134
+
135
+ /**
136
+ * The entries the feed's local-thumbnail bridge should hold for a just-created
137
+ * video post.
138
+ *
139
+ * A video's server thumbnail does not exist until transcoding finishes, so the
140
+ * feed falls back to the frame the composer decoded at pick time, matched on
141
+ * the uploaded video's fileId. Entries without both halves are dropped: a row
142
+ * with an empty url still satisfies the feed's lookup, which suppresses its own
143
+ * pending state and leaves the frame blank instead of showing the play button
144
+ * over a plain background.
145
+ */
146
+ export function toLocalVideoThumbnails(
147
+ videos: Pick<IDisplayImage, 'fileId' | 'thumbNail'>[]
148
+ ): { fileId: string; thumbnailUrl: string }[] {
149
+ return videos.reduce<{ fileId: string; thumbnailUrl: string }[]>(
150
+ (acc, { fileId, thumbNail }) => {
151
+ if (!fileId || !thumbNail) return acc;
152
+
153
+ acc.push({ fileId, thumbnailUrl: thumbNail });
154
+ return acc;
155
+ },
156
+ []
157
+ );
158
+ }
@@ -0,0 +1,85 @@
1
+ import type { IVideoPost } from '../components/legacy/Social/PostList';
2
+
3
+ /** A post child as the feed resolves it: the child post plus its own payload. */
4
+ export type ResolvedPostChild = {
5
+ post?: Amity.Post;
6
+ dataType?: string;
7
+ data?: Record<string, any>;
8
+ };
9
+
10
+ export type ClassifiedPostChildren = {
11
+ /** Medium-size download urls, de-duplicated. */
12
+ images: string[];
13
+ videos: IVideoPost[];
14
+ polls: { pollId: string }[];
15
+ livestreams: Amity.Room['roomId'][];
16
+ /**
17
+ * The media children the carousel renders, in the same order — and for
18
+ * videos, at the same indices — as `videos`. The full-screen player is built
19
+ * from `videos` but opened with an index taken from this array, so a child
20
+ * that lands in one and not the other opens the wrong video.
21
+ */
22
+ media: Amity.Post[];
23
+ };
24
+
25
+ /**
26
+ * Sort a post's resolved children into what each surface needs.
27
+ *
28
+ * Every hop into a child's payload is guarded, because the cost of being wrong
29
+ * is out of proportion to the saving: an ErrorBoundary sits around the whole
30
+ * UIKit without an onError, so one TypeError in here replaces the entire app
31
+ * with a generic error page and takes its own stack down with it.
32
+ *
33
+ * `ContentDataVideo` types `videoFileId` as always present but every size
34
+ * inside it as optional, so `original` is the hop that can legitimately be
35
+ * missing. The keys above it are guarded on the same principle rather than on
36
+ * evidence that they go missing.
37
+ */
38
+ export function classifyPostChildren(
39
+ children: ResolvedPostChild[],
40
+ apiRegion: string
41
+ ): ClassifiedPostChildren {
42
+ const images: string[] = [];
43
+ const videos: IVideoPost[] = [];
44
+ const polls: { pollId: string }[] = [];
45
+ const livestreams: Amity.Room['roomId'][] = [];
46
+ const media: Amity.Post[] = [];
47
+
48
+ children.forEach((item) => {
49
+ if (item?.dataType === 'image' && item?.data?.fileId) {
50
+ const url = `https://api.${apiRegion}.amity.co/api/v3/files/${item.data.fileId}/download?size=medium`;
51
+ if (!images.includes(url)) {
52
+ images.push(url);
53
+ if (item.post) media.push(item.post);
54
+ }
55
+ return;
56
+ }
57
+
58
+ if (item?.dataType === 'video' && item?.data?.videoFileId?.original) {
59
+ const isExisted = videos.some(
60
+ (video) =>
61
+ video.videoFileId?.original === item.data?.videoFileId?.original
62
+ );
63
+ if (!isExisted) {
64
+ videos.push(item.data as IVideoPost);
65
+ if (item.post) media.push(item.post);
66
+ }
67
+ return;
68
+ }
69
+
70
+ if (item?.dataType === 'poll' && item?.data?.pollId) {
71
+ if (!polls.some((poll) => poll.pollId === item.data?.pollId)) {
72
+ polls.push(item.data as { pollId: string });
73
+ }
74
+ return;
75
+ }
76
+
77
+ if (item?.dataType === 'room' && item?.data?.roomId) {
78
+ if (!livestreams.includes(item.data.roomId)) {
79
+ livestreams.push(item.data.roomId);
80
+ }
81
+ }
82
+ });
83
+
84
+ return { images, videos, polls, livestreams, media };
85
+ }