diff --git a/lib/actions/link-actions.js b/lib/actions/link-actions.js index d83f7bb71..a5dd9d937 100644 --- a/lib/actions/link-actions.js +++ b/lib/actions/link-actions.js @@ -1,109 +1,147 @@ // @flow import type { FetchInviteLinksResponse, InviteLinkVerificationRequest, InviteLinkVerificationResponse, CreateOrUpdatePublicLinkRequest, InviteLink, DisableInviteLinkRequest, DisableInviteLinkPayload, } from '../types/link-types.js'; +import { extractKeyserverIDFromID } from '../utils/action-utils.js'; import type { CallServerEndpoint } from '../utils/call-server-endpoint.js'; +import { useKeyserverCall } from '../utils/keyserver-call.js'; +import type { CallKeyserverEndpoint } from '../utils/keyserver-call.js'; const verifyInviteLinkActionTypes = Object.freeze({ started: 'VERIFY_INVITE_LINK_STARTED', success: 'VERIFY_INVITE_LINK_SUCCESS', failed: 'VERIFY_INVITE_LINK_FAILED', }); const verifyInviteLink = ( callServerEndpoint: CallServerEndpoint, ): (( request: InviteLinkVerificationRequest, ) => Promise) => async request => { const response = await callServerEndpoint('verify_invite_link', request); if (response.status === 'valid' || response.status === 'already_joined') { return { status: response.status, community: response.community, }; } return { status: response.status, }; }; const fetchPrimaryInviteLinkActionTypes = Object.freeze({ started: 'FETCH_PRIMARY_INVITE_LINKS_STARTED', success: 'FETCH_PRIMARY_INVITE_LINKS_SUCCESS', failed: 'FETCH_PRIMARY_INVITE_LINKS_FAILED', }); const fetchPrimaryInviteLinks = ( - callServerEndpoint: CallServerEndpoint, + callKeyserverEndpoint: CallKeyserverEndpoint, + allKeyserverIDs: $ReadOnlyArray, ): (() => Promise) => async () => { - const response = await callServerEndpoint('fetch_primary_invite_links'); + const requests = {}; + for (const keyserverID of allKeyserverIDs) { + requests[keyserverID] = undefined; + } + const responses = await callKeyserverEndpoint( + 'fetch_primary_invite_links', + requests, + ); + let links = []; + for (const keyserverID in responses) { + links = links.concat(responses[keyserverID].links); + } return { - links: response.links, + links, }; }; +function useFetchPrimaryInviteLinks(): () => Promise { + return useKeyserverCall(fetchPrimaryInviteLinks); +} + const createOrUpdatePublicLinkActionTypes = Object.freeze({ started: 'CREATE_OR_UPDATE_PUBLIC_LINK_STARTED', success: 'CREATE_OR_UPDATE_PUBLIC_LINK_SUCCESS', failed: 'CREATE_OR_UPDATE_PUBLIC_LINK_FAILED', }); const createOrUpdatePublicLink = ( - callServerEndpoint: CallServerEndpoint, - ): ((request: CreateOrUpdatePublicLinkRequest) => Promise) => - async request => { - const response = await callServerEndpoint('create_or_update_public_link', { - name: request.name, - communityID: request.communityID, - }); + callKeyserverEndpoint: CallKeyserverEndpoint, + ): ((input: CreateOrUpdatePublicLinkRequest) => Promise) => + async input => { + const keyserverID = extractKeyserverIDFromID(input.communityID); + const requests = { + [keyserverID]: { + name: input.name, + communityID: input.communityID, + }, + }; + + const responses = await callKeyserverEndpoint( + 'create_or_update_public_link', + requests, + ); + const response = responses[keyserverID]; return { name: response.name, primary: response.primary, role: response.role, communityID: response.communityID, expirationTime: response.expirationTime, limitOfUses: response.limitOfUses, numberOfUses: response.numberOfUses, }; }; +function useCreateOrUpdatePublicLink(): ( + input: CreateOrUpdatePublicLinkRequest, +) => Promise { + return useKeyserverCall(createOrUpdatePublicLink); +} + const disableInviteLinkLinkActionTypes = Object.freeze({ started: 'DISABLE_INVITE_LINK_STARTED', success: 'DISABLE_INVITE_LINK_SUCCESS', failed: 'DISABLE_INVITE_LINK_FAILED', }); const disableInviteLink = ( - callServerEndpoint: CallServerEndpoint, - ): (( - request: DisableInviteLinkRequest, - ) => Promise) => - async request => { - await callServerEndpoint('disable_invite_link', request); - return { - name: request.name, - communityID: request.communityID, - }; + callKeyserverEndpoint: CallKeyserverEndpoint, + ): ((input: DisableInviteLinkRequest) => Promise) => + async input => { + const keyserverID = extractKeyserverIDFromID(input.communityID); + const requests = { [keyserverID]: input }; + + await callKeyserverEndpoint('disable_invite_link', requests); + return input; }; +function useDisableInviteLink(): ( + input: DisableInviteLinkRequest, +) => Promise { + return useKeyserverCall(disableInviteLink); +} + export { verifyInviteLinkActionTypes, verifyInviteLink, fetchPrimaryInviteLinkActionTypes, - fetchPrimaryInviteLinks, + useFetchPrimaryInviteLinks, createOrUpdatePublicLinkActionTypes, - createOrUpdatePublicLink, + useCreateOrUpdatePublicLink, disableInviteLinkLinkActionTypes, - disableInviteLink, + useDisableInviteLink, }; diff --git a/lib/hooks/invite-links.js b/lib/hooks/invite-links.js index 10b4cf333..b07381901 100644 --- a/lib/hooks/invite-links.js +++ b/lib/hooks/invite-links.js @@ -1,105 +1,102 @@ // @flow import React from 'react'; import { - createOrUpdatePublicLink, + useCreateOrUpdatePublicLink, createOrUpdatePublicLinkActionTypes, - disableInviteLink as callDisableInviteLink, + useDisableInviteLink, disableInviteLinkLinkActionTypes, } from '../actions/link-actions.js'; import { createLoadingStatusSelector } from '../selectors/loading-selectors.js'; import type { SetState } from '../types/hook-types.js'; import type { InviteLink } from '../types/link-types.js'; -import { - useDispatchActionPromise, - useServerCall, -} from '../utils/action-utils.js'; +import { useDispatchActionPromise } from '../utils/action-utils.js'; import { useSelector } from '../utils/redux-utils.js'; const createOrUpdatePublicLinkStatusSelector = createLoadingStatusSelector( createOrUpdatePublicLinkActionTypes, ); const disableInviteLinkStatusSelector = createLoadingStatusSelector( disableInviteLinkLinkActionTypes, ); function useInviteLinksActions( communityID: string, inviteLink: ?InviteLink, ): { +error: ?string, +isLoading: boolean, +name: string, +setName: SetState, +createOrUpdateInviteLink: () => mixed, +disableInviteLink: () => mixed, } { const [name, setName] = React.useState( inviteLink?.name ?? Math.random().toString(36).slice(-9), ); const [error, setError] = React.useState(null); const dispatchActionPromise = useDispatchActionPromise(); - const callCreateOrUpdatePublicLink = useServerCall(createOrUpdatePublicLink); + const callCreateOrUpdatePublicLink = useCreateOrUpdatePublicLink(); const createCreateOrUpdateActionPromise = React.useCallback(async () => { setError(null); try { return await callCreateOrUpdatePublicLink({ name, communityID, }); } catch (e) { setError(e.message); throw e; } }, [callCreateOrUpdatePublicLink, communityID, name]); const createOrUpdateInviteLink = React.useCallback(() => { dispatchActionPromise( createOrUpdatePublicLinkActionTypes, createCreateOrUpdateActionPromise(), ); }, [createCreateOrUpdateActionPromise, dispatchActionPromise]); - const disableInviteLinkServerCall = useServerCall(callDisableInviteLink); + const disableInviteLinkServerCall = useDisableInviteLink(); const createDisableLinkActionPromise = React.useCallback(async () => { setError(null); try { return await disableInviteLinkServerCall({ name, communityID, }); } catch (e) { setError(e.message); throw e; } }, [disableInviteLinkServerCall, communityID, name]); const disableInviteLink = React.useCallback(() => { dispatchActionPromise( disableInviteLinkLinkActionTypes, createDisableLinkActionPromise(), ); }, [createDisableLinkActionPromise, dispatchActionPromise]); const disableInviteLinkStatus = useSelector(disableInviteLinkStatusSelector); const createOrUpdatePublicLinkStatus = useSelector( createOrUpdatePublicLinkStatusSelector, ); const isLoading = createOrUpdatePublicLinkStatus === 'loading' || disableInviteLinkStatus === 'loading'; return React.useMemo( () => ({ error, isLoading, name, setName, createOrUpdateInviteLink, disableInviteLink, }), [createOrUpdateInviteLink, disableInviteLink, error, isLoading, name], ); } export { useInviteLinksActions }; diff --git a/native/navigation/community-drawer-content.react.js b/native/navigation/community-drawer-content.react.js index 1d7e996d0..1d255b52b 100644 --- a/native/navigation/community-drawer-content.react.js +++ b/native/navigation/community-drawer-content.react.js @@ -1,236 +1,233 @@ // @flow import { useDrawerStatus } from '@react-navigation/drawer'; import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { FlatList, Platform, View, Text, TouchableOpacity } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useSelector } from 'react-redux'; import { fetchPrimaryInviteLinkActionTypes, - fetchPrimaryInviteLinks, + useFetchPrimaryInviteLinks, } from 'lib/actions/link-actions.js'; import { childThreadInfos, communityThreadSelector, } from 'lib/selectors/thread-selectors.js'; import { threadTypeIsCommunityRoot } from 'lib/types/thread-types-enum.js'; -import { - useDispatchActionPromise, - useServerCall, -} from 'lib/utils/action-utils.js'; +import { useDispatchActionPromise } from 'lib/utils/action-utils.js'; import { createRecursiveDrawerItemsData, appendSuffix, } from 'lib/utils/drawer-utils.react.js'; import { useResolvedThreadInfos } from 'lib/utils/entity-helpers.js'; import CommunityDrawerItem from './community-drawer-item.react.js'; import { CommunityCreationRouteName } from './route-names.js'; import { useNavigateToThread } from '../chat/message-list-types.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import { useStyles } from '../themes/colors.js'; import { flattenDrawerItemsData, filterOutThreadAndDescendantIDs, } from '../utils/drawer-utils.react.js'; const maxDepth = 2; const safeAreaEdges = Platform.select({ ios: ['top'], default: ['top', 'bottom'], }); function CommunityDrawerContent(): React.Node { const communities = useSelector(communityThreadSelector); const resolvedCommunities = useResolvedThreadInfos(communities); const communitiesSuffixed = React.useMemo( () => appendSuffix(resolvedCommunities), [resolvedCommunities], ); const styles = useStyles(unboundStyles); - const callFetchPrimaryLinks = useServerCall(fetchPrimaryInviteLinks); + const callFetchPrimaryLinks = useFetchPrimaryInviteLinks(); const dispatchActionPromise = useDispatchActionPromise(); const drawerStatus = useDrawerStatus(); React.useEffect(() => { (async () => { if (drawerStatus !== 'open') { return; } await dispatchActionPromise( fetchPrimaryInviteLinkActionTypes, callFetchPrimaryLinks(), ); })(); }, [callFetchPrimaryLinks, dispatchActionPromise, drawerStatus]); const [expanded, setExpanded] = React.useState(() => { if (communitiesSuffixed.length === 1) { return new Set([communitiesSuffixed[0].id]); } return new Set(); }); const setOpenCommunityOrClose = React.useCallback( (id: string) => expanded.has(id) ? setExpanded(new Set()) : setExpanded(new Set([id])), [expanded], ); const labelStylesObj = useStyles(labelUnboundStyles); const labelStyles = React.useMemo( () => [ labelStylesObj.level0Label, labelStylesObj.level1Label, labelStylesObj.level2Label, ], [labelStylesObj], ); const childThreadInfosMap = useSelector(childThreadInfos); const drawerItemsData = React.useMemo( () => createRecursiveDrawerItemsData( childThreadInfosMap, communitiesSuffixed, labelStyles, maxDepth, ), [childThreadInfosMap, communitiesSuffixed, labelStyles], ); const toggleExpanded = React.useCallback( (id: string) => setExpanded(expandedState => { if (expanded.has(id)) { return new Set( filterOutThreadAndDescendantIDs( [...expandedState.values()], drawerItemsData, id, ), ); } return new Set([...expanded.values(), id]); }), [drawerItemsData, expanded], ); const navigateToThread = useNavigateToThread(); const renderItem = React.useCallback( ({ item }) => { const isCommunity = threadTypeIsCommunityRoot(item.threadInfo.type); return ( ); }, [expanded, navigateToThread, setOpenCommunityOrClose, toggleExpanded], ); const { navigate } = useNavigation(); const onPressCommunityCreation = React.useCallback(() => { navigate(CommunityCreationRouteName); }, [navigate]); const communityCreationButton = ( Create community ); const flattenedDrawerItemsData = React.useMemo( () => flattenDrawerItemsData(drawerItemsData, [...expanded.values()]), [drawerItemsData, expanded], ); return ( {communityCreationButton} ); } const unboundStyles = { drawerContent: { flex: 1, paddingRight: 8, paddingTop: 8, paddingBottom: 8, }, communityCreationContainer: { flexDirection: 'row', padding: 24, alignItems: 'center', borderTopWidth: 1, borderColor: 'panelSeparator', }, communityCreationText: { color: 'panelForegroundLabel', fontSize: 16, lineHeight: 24, fontWeight: '500', }, communityCreationIconContainer: { height: 28, width: 28, justifyContent: 'center', alignItems: 'center', borderRadius: 16, marginRight: 12, backgroundColor: 'panelSecondaryForeground', }, communityCreationIcon: { color: 'panelForegroundLabel', }, }; const labelUnboundStyles = { level0Label: { color: 'drawerItemLabelLevel0', fontSize: 16, lineHeight: 24, fontWeight: '500', }, level1Label: { color: 'drawerItemLabelLevel1', fontSize: 14, lineHeight: 22, fontWeight: '500', }, level2Label: { color: 'drawerItemLabelLevel2', fontSize: 14, lineHeight: 22, fontWeight: '400', }, }; const MemoizedCommunityDrawerContent: React.ComponentType<{}> = React.memo( CommunityDrawerContent, ); export default MemoizedCommunityDrawerContent; diff --git a/web/invite-links/invite-links-refresher.react.js b/web/invite-links/invite-links-refresher.react.js index e0bc855a6..a52440fbf 100644 --- a/web/invite-links/invite-links-refresher.react.js +++ b/web/invite-links/invite-links-refresher.react.js @@ -1,36 +1,33 @@ // @flow import * as React from 'react'; import { fetchPrimaryInviteLinkActionTypes, - fetchPrimaryInviteLinks, + useFetchPrimaryInviteLinks, } from 'lib/actions/link-actions.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; -import { - useDispatchActionPromise, - useServerCall, -} from 'lib/utils/action-utils.js'; +import { useDispatchActionPromise } from 'lib/utils/action-utils.js'; import { useSelector } from '../redux/redux-utils.js'; function InviteLinksRefresher(): React.Node { const isActive = useSelector(state => state.windowActive); const loggedIn = useSelector(isLoggedIn); - const callFetchPrimaryLinks = useServerCall(fetchPrimaryInviteLinks); + const callFetchPrimaryLinks = useFetchPrimaryInviteLinks(); const dispatchActionPromise = useDispatchActionPromise(); React.useEffect(() => { if (!isActive || !loggedIn) { return; } dispatchActionPromise( fetchPrimaryInviteLinkActionTypes, callFetchPrimaryLinks(), ); }, [callFetchPrimaryLinks, dispatchActionPromise, isActive, loggedIn]); return null; } export default InviteLinksRefresher;