@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.
- package/README.md +7 -0
- package/lib/commonjs/core/assets/icons/post.js +1 -1
- package/lib/commonjs/core/assets/icons/post.js.map +1 -1
- package/lib/commonjs/core/assets/icons/story.js +1 -1
- package/lib/commonjs/core/assets/icons/story.js.map +1 -1
- package/lib/commonjs/core/assets/icons/xml.js +16 -2
- package/lib/commonjs/core/assets/icons/xml.js.map +1 -1
- package/lib/commonjs/core/legacy/file.js +145 -62
- package/lib/commonjs/core/legacy/file.js.map +1 -1
- package/lib/commonjs/core/stores/slices/feedSlice.js +3 -0
- package/lib/commonjs/core/stores/slices/feedSlice.js.map +1 -1
- package/lib/commonjs/core/stores/slices/globalfeedSlice.js +5 -0
- package/lib/commonjs/core/stores/slices/globalfeedSlice.js.map +1 -1
- package/lib/commonjs/core/stores/slices/localVideoThumbnailSlice.js +46 -0
- package/lib/commonjs/core/stores/slices/localVideoThumbnailSlice.js.map +1 -0
- package/lib/commonjs/core/stores/slices/uiSlice.js +5 -1
- package/lib/commonjs/core/stores/slices/uiSlice.js.map +1 -1
- package/lib/commonjs/core/stores/store/index.js +3 -1
- package/lib/commonjs/core/stores/store/index.js.map +1 -1
- package/lib/commonjs/social/components/LoadingImage/index.js +73 -12
- package/lib/commonjs/social/components/LoadingImage/index.js.map +1 -1
- package/lib/commonjs/social/components/LoadingImage/styles.js +8 -3
- package/lib/commonjs/social/components/LoadingImage/styles.js.map +1 -1
- package/lib/commonjs/social/components/LoadingVideo/index.js +90 -16
- package/lib/commonjs/social/components/LoadingVideo/index.js.map +1 -1
- package/lib/commonjs/social/components/LoadingVideo/styles.js +14 -5
- package/lib/commonjs/social/components/LoadingVideo/styles.js.map +1 -1
- package/lib/commonjs/social/components/MediaViewer/MediaViewer.js +13 -13
- package/lib/commonjs/social/components/MediaViewer/MediaViewer.js.map +1 -1
- package/lib/commonjs/social/components/MediaViewer/styles.js +11 -1
- package/lib/commonjs/social/components/MediaViewer/styles.js.map +1 -1
- package/lib/commonjs/social/components/PostContent/index.js +8 -29
- package/lib/commonjs/social/components/PostContent/index.js.map +1 -1
- package/lib/commonjs/social/components/SelectedMediaComponent/SelectedMediaComponent.js +109 -17
- package/lib/commonjs/social/components/SelectedMediaComponent/SelectedMediaComponent.js.map +1 -1
- package/lib/commonjs/social/components/legacy/LoadingVideo/index.js +17 -6
- package/lib/commonjs/social/components/legacy/LoadingVideo/index.js.map +1 -1
- package/lib/commonjs/social/elements/PostMediaElement/PostMediaElement.js +121 -23
- package/lib/commonjs/social/elements/PostMediaElement/PostMediaElement.js.map +1 -1
- package/lib/commonjs/social/elements/PostMediaElement/styles.js +7 -0
- package/lib/commonjs/social/elements/PostMediaElement/styles.js.map +1 -1
- package/lib/commonjs/social/features/community/PendingRequest/components/PendingPostList/PendingPostList.js +11 -5
- package/lib/commonjs/social/features/community/PendingRequest/components/PendingPostList/PendingPostList.js.map +1 -1
- package/lib/commonjs/social/features/feed/components/NewsFeed/NewsFeed.js +14 -0
- package/lib/commonjs/social/features/feed/components/NewsFeed/NewsFeed.js.map +1 -1
- package/lib/commonjs/social/features/post/Composer/PostComposer.js +234 -86
- package/lib/commonjs/social/features/post/Composer/PostComposer.js.map +1 -1
- package/lib/commonjs/social/hooks/index.js +11 -0
- package/lib/commonjs/social/hooks/index.js.map +1 -1
- package/lib/commonjs/social/hooks/useCommunities.js +5 -3
- package/lib/commonjs/social/hooks/useCommunities.js.map +1 -1
- package/lib/commonjs/social/hooks/useCommunityPermission.js +43 -0
- package/lib/commonjs/social/hooks/useCommunityPermission.js.map +1 -0
- package/lib/commonjs/social/hooks/useCustomRankingGlobalFeed.js +10 -1
- package/lib/commonjs/social/hooks/useCustomRankingGlobalFeed.js.map +1 -1
- package/lib/commonjs/social/hooks/useFile.js +11 -0
- package/lib/commonjs/social/hooks/useFile.js.map +1 -1
- package/lib/commonjs/social/screens/CommunityProfile/components/Header/Header.js +18 -7
- package/lib/commonjs/social/screens/CommunityProfile/components/Header/Header.js.map +1 -1
- package/lib/commonjs/social/utils/mediaAttachments.js +150 -0
- package/lib/commonjs/social/utils/mediaAttachments.js.map +1 -0
- package/lib/commonjs/social/utils/postChildren.js +72 -0
- package/lib/commonjs/social/utils/postChildren.js.map +1 -0
- package/lib/module/core/assets/icons/post.js +1 -1
- package/lib/module/core/assets/icons/post.js.map +1 -1
- package/lib/module/core/assets/icons/story.js +1 -1
- package/lib/module/core/assets/icons/story.js.map +1 -1
- package/lib/module/core/assets/icons/xml.js +13 -0
- package/lib/module/core/assets/icons/xml.js.map +1 -1
- package/lib/module/core/legacy/file.js +145 -62
- package/lib/module/core/legacy/file.js.map +1 -1
- package/lib/module/core/stores/slices/feedSlice.js +3 -0
- package/lib/module/core/stores/slices/feedSlice.js.map +1 -1
- package/lib/module/core/stores/slices/globalfeedSlice.js +5 -0
- package/lib/module/core/stores/slices/globalfeedSlice.js.map +1 -1
- package/lib/module/core/stores/slices/localVideoThumbnailSlice.js +41 -0
- package/lib/module/core/stores/slices/localVideoThumbnailSlice.js.map +1 -0
- package/lib/module/core/stores/slices/uiSlice.js +5 -1
- package/lib/module/core/stores/slices/uiSlice.js.map +1 -1
- package/lib/module/core/stores/store/index.js +3 -1
- package/lib/module/core/stores/store/index.js.map +1 -1
- package/lib/module/social/components/LoadingImage/index.js +74 -14
- package/lib/module/social/components/LoadingImage/index.js.map +1 -1
- package/lib/module/social/components/LoadingImage/styles.js +8 -3
- package/lib/module/social/components/LoadingImage/styles.js.map +1 -1
- package/lib/module/social/components/LoadingVideo/index.js +91 -17
- package/lib/module/social/components/LoadingVideo/index.js.map +1 -1
- package/lib/module/social/components/LoadingVideo/styles.js +14 -5
- package/lib/module/social/components/LoadingVideo/styles.js.map +1 -1
- package/lib/module/social/components/MediaViewer/MediaViewer.js +13 -13
- package/lib/module/social/components/MediaViewer/MediaViewer.js.map +1 -1
- package/lib/module/social/components/MediaViewer/styles.js +11 -1
- package/lib/module/social/components/MediaViewer/styles.js.map +1 -1
- package/lib/module/social/components/PostContent/index.js +8 -29
- package/lib/module/social/components/PostContent/index.js.map +1 -1
- package/lib/module/social/components/SelectedMediaComponent/SelectedMediaComponent.js +110 -18
- package/lib/module/social/components/SelectedMediaComponent/SelectedMediaComponent.js.map +1 -1
- package/lib/module/social/components/legacy/LoadingVideo/index.js +17 -6
- package/lib/module/social/components/legacy/LoadingVideo/index.js.map +1 -1
- package/lib/module/social/elements/PostMediaElement/PostMediaElement.js +123 -25
- package/lib/module/social/elements/PostMediaElement/PostMediaElement.js.map +1 -1
- package/lib/module/social/elements/PostMediaElement/styles.js +7 -0
- package/lib/module/social/elements/PostMediaElement/styles.js.map +1 -1
- package/lib/module/social/features/community/PendingRequest/components/PendingPostList/PendingPostList.js +12 -6
- package/lib/module/social/features/community/PendingRequest/components/PendingPostList/PendingPostList.js.map +1 -1
- package/lib/module/social/features/feed/components/NewsFeed/NewsFeed.js +15 -1
- package/lib/module/social/features/feed/components/NewsFeed/NewsFeed.js.map +1 -1
- package/lib/module/social/features/post/Composer/PostComposer.js +236 -89
- package/lib/module/social/features/post/Composer/PostComposer.js.map +1 -1
- package/lib/module/social/hooks/index.js +1 -0
- package/lib/module/social/hooks/index.js.map +1 -1
- package/lib/module/social/hooks/useCommunities.js +5 -3
- package/lib/module/social/hooks/useCommunities.js.map +1 -1
- package/lib/module/social/hooks/useCommunityPermission.js +37 -0
- package/lib/module/social/hooks/useCommunityPermission.js.map +1 -0
- package/lib/module/social/hooks/useCustomRankingGlobalFeed.js +10 -1
- package/lib/module/social/hooks/useCustomRankingGlobalFeed.js.map +1 -1
- package/lib/module/social/hooks/useFile.js +11 -0
- package/lib/module/social/hooks/useFile.js.map +1 -1
- package/lib/module/social/screens/CommunityProfile/components/Header/Header.js +19 -8
- package/lib/module/social/screens/CommunityProfile/components/Header/Header.js.map +1 -1
- package/lib/module/social/utils/mediaAttachments.js +139 -0
- package/lib/module/social/utils/mediaAttachments.js.map +1 -0
- package/lib/module/social/utils/postChildren.js +66 -0
- package/lib/module/social/utils/postChildren.js.map +1 -0
- package/lib/typescript/core/assets/icons/xml.d.ts +1 -0
- package/lib/typescript/core/assets/icons/xml.d.ts.map +1 -1
- package/lib/typescript/core/legacy/file.d.ts +32 -0
- package/lib/typescript/core/legacy/file.d.ts.map +1 -1
- package/lib/typescript/core/stores/slices/feedSlice.d.ts.map +1 -1
- package/lib/typescript/core/stores/slices/globalfeedSlice.d.ts.map +1 -1
- package/lib/typescript/core/stores/slices/localVideoThumbnailSlice.d.ts +35 -0
- package/lib/typescript/core/stores/slices/localVideoThumbnailSlice.d.ts.map +1 -0
- package/lib/typescript/core/stores/slices/uiSlice.d.ts +5 -0
- package/lib/typescript/core/stores/slices/uiSlice.d.ts.map +1 -1
- package/lib/typescript/core/stores/store/index.d.ts.map +1 -1
- package/lib/typescript/core/types/mention.d.ts +34 -0
- package/lib/typescript/core/types/mention.d.ts.map +1 -1
- package/lib/typescript/social/components/LoadingImage/index.d.ts +8 -2
- package/lib/typescript/social/components/LoadingImage/index.d.ts.map +1 -1
- package/lib/typescript/social/components/LoadingImage/styles.d.ts.map +1 -1
- package/lib/typescript/social/components/LoadingVideo/index.d.ts +9 -2
- package/lib/typescript/social/components/LoadingVideo/index.d.ts.map +1 -1
- package/lib/typescript/social/components/LoadingVideo/styles.d.ts +1 -0
- package/lib/typescript/social/components/LoadingVideo/styles.d.ts.map +1 -1
- package/lib/typescript/social/components/MediaViewer/MediaViewer.d.ts.map +1 -1
- package/lib/typescript/social/components/MediaViewer/styles.d.ts +1 -0
- package/lib/typescript/social/components/MediaViewer/styles.d.ts.map +1 -1
- package/lib/typescript/social/components/PostContent/index.d.ts.map +1 -1
- package/lib/typescript/social/components/SelectedMediaComponent/SelectedMediaComponent.d.ts +2 -2
- package/lib/typescript/social/components/SelectedMediaComponent/SelectedMediaComponent.d.ts.map +1 -1
- package/lib/typescript/social/components/legacy/LoadingVideo/index.d.ts.map +1 -1
- package/lib/typescript/social/elements/PostMediaElement/PostMediaElement.d.ts.map +1 -1
- package/lib/typescript/social/elements/PostMediaElement/styles.d.ts +4 -0
- package/lib/typescript/social/elements/PostMediaElement/styles.d.ts.map +1 -1
- package/lib/typescript/social/features/community/PendingRequest/components/PendingPostList/PendingPostList.d.ts.map +1 -1
- package/lib/typescript/social/features/feed/components/NewsFeed/NewsFeed.d.ts.map +1 -1
- package/lib/typescript/social/features/post/Composer/PostComposer.d.ts.map +1 -1
- package/lib/typescript/social/hooks/index.d.ts +1 -0
- package/lib/typescript/social/hooks/index.d.ts.map +1 -1
- package/lib/typescript/social/hooks/useCommunities.d.ts +2 -1
- package/lib/typescript/social/hooks/useCommunities.d.ts.map +1 -1
- package/lib/typescript/social/hooks/useCommunityPermission.d.ts +14 -0
- package/lib/typescript/social/hooks/useCommunityPermission.d.ts.map +1 -0
- package/lib/typescript/social/hooks/useCustomRankingGlobalFeed.d.ts.map +1 -1
- package/lib/typescript/social/hooks/useFile.d.ts +11 -0
- package/lib/typescript/social/hooks/useFile.d.ts.map +1 -1
- package/lib/typescript/social/screens/CommunityProfile/components/Header/Header.d.ts.map +1 -1
- package/lib/typescript/social/utils/mediaAttachments.d.ts +91 -0
- package/lib/typescript/social/utils/mediaAttachments.d.ts.map +1 -0
- package/lib/typescript/social/utils/postChildren.d.ts +38 -0
- package/lib/typescript/social/utils/postChildren.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/core/assets/icons/post.tsx +1 -1
- package/src/core/assets/icons/story.tsx +1 -1
- package/src/core/assets/icons/xml.ts +15 -0
- package/src/core/legacy/file.tsx +169 -84
- package/src/core/stores/slices/feedSlice.ts +3 -0
- package/src/core/stores/slices/globalfeedSlice.ts +5 -0
- package/src/core/stores/slices/localVideoThumbnailSlice.ts +58 -0
- package/src/core/stores/slices/uiSlice.ts +8 -0
- package/src/core/stores/store/index.ts +2 -0
- package/src/core/types/mention.ts +34 -0
- package/src/social/components/LoadingImage/index.tsx +81 -12
- package/src/social/components/LoadingImage/styles.ts +8 -3
- package/src/social/components/LoadingVideo/index.tsx +111 -21
- package/src/social/components/LoadingVideo/styles.ts +13 -4
- package/src/social/components/MediaViewer/MediaViewer.tsx +11 -12
- package/src/social/components/MediaViewer/styles.ts +9 -1
- package/src/social/components/PostContent/index.tsx +3 -35
- package/src/social/components/SelectedMediaComponent/SelectedMediaComponent.tsx +103 -13
- package/src/social/components/legacy/LoadingVideo/index.tsx +26 -15
- package/src/social/elements/PostMediaElement/PostMediaElement.tsx +139 -29
- package/src/social/elements/PostMediaElement/styles.ts +7 -0
- package/src/social/features/community/PendingRequest/components/PendingPostList/PendingPostList.tsx +11 -6
- package/src/social/features/feed/components/NewsFeed/NewsFeed.tsx +16 -1
- package/src/social/features/post/Composer/PostComposer.tsx +304 -91
- package/src/social/hooks/index.ts +1 -0
- package/src/social/hooks/useCommunities.ts +4 -2
- package/src/social/hooks/useCommunityPermission.ts +40 -0
- package/src/social/hooks/useCustomRankingGlobalFeed.ts +8 -1
- package/src/social/hooks/useFile.ts +11 -0
- package/src/social/screens/CommunityProfile/components/Header/Header.tsx +19 -10
- package/src/social/utils/mediaAttachments.ts +158 -0
- 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
|
-
|
|
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
|
|
68
|
-
|
|
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
|
-
{(
|
|
178
|
-
pendingPosts?.some(
|
|
179
|
-
(post) => post.creator?.userId === client.userId
|
|
180
|
-
)) &&
|
|
189
|
+
{(canReviewCommunityPosts || isPendingPostOwner) &&
|
|
181
190
|
pendingPosts?.length > 0 &&
|
|
182
191
|
community.isJoined &&
|
|
183
|
-
|
|
192
|
+
requiresPostApproval && (
|
|
184
193
|
<CommunityPendingPost
|
|
185
194
|
number={pendingPosts.length}
|
|
186
195
|
pageId={pageId}
|
|
187
196
|
componentId={componentId}
|
|
188
|
-
isModerator={
|
|
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
|
+
}
|