diff --git a/lib/actions/relationship-actions.js b/lib/actions/relationship-actions.js index dd5592e92..fd561e144 100644 --- a/lib/actions/relationship-actions.js +++ b/lib/actions/relationship-actions.js @@ -1,37 +1,37 @@ // @flow import type { CallSingleKeyserverEndpoint } from '../keyserver-conn/call-single-keyserver-endpoint.js'; import type { RelationshipErrors, - LegacyRelationshipRequest, + RelationshipRequest, } from '../types/relationship-types.js'; import { ServerError } from '../utils/errors.js'; const updateRelationshipsActionTypes = Object.freeze({ started: 'UPDATE_RELATIONSHIPS_STARTED', success: 'UPDATE_RELATIONSHIPS_SUCCESS', failed: 'UPDATE_RELATIONSHIPS_FAILED', }); const updateRelationships = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, - ): ((request: LegacyRelationshipRequest) => Promise) => + ): ((request: RelationshipRequest) => Promise) => async request => { const errors = await callSingleKeyserverEndpoint( - 'update_relationships', + 'update_relationships2', request, ); const { invalid_user, already_friends, user_blocked } = errors; if (invalid_user) { throw new ServerError('invalid_user', errors); } else if (already_friends) { throw new ServerError('already_friends', errors); } else if (user_blocked) { throw new ServerError('user_blocked', errors); } return errors; }; export { updateRelationshipsActionTypes, updateRelationships }; diff --git a/lib/components/farcaster-data-handler.react.js b/lib/components/farcaster-data-handler.react.js index b16cfe82f..ab4e625cb 100644 --- a/lib/components/farcaster-data-handler.react.js +++ b/lib/components/farcaster-data-handler.react.js @@ -1,245 +1,229 @@ // @flow import * as React from 'react'; import { setAuxUserFIDsActionType } from '../actions/aux-user-actions.js'; -import { - updateRelationships as serverUpdateRelationships, - updateRelationshipsActionTypes, -} from '../actions/relationship-actions.js'; +import { updateRelationshipsActionTypes } from '../actions/relationship-actions.js'; import { setSyncedMetadataEntryActionType } from '../actions/synced-metadata-actions.js'; import { useFindUserIdentities } from '../actions/user-actions.js'; import { NeynarClientContext } from '../components/neynar-client-provider.react.js'; import { useIsLoggedInToIdentityAndAuthoritativeKeyserver } from '../hooks/account-hooks.js'; -import { useLegacyAshoatKeyserverCall } from '../keyserver-conn/legacy-keyserver-call.js'; +import { useUpdateRelationships } from '../hooks/relationship-hooks.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import { relationshipActions } from '../types/relationship-types.js'; import { syncedMetadataNames } from '../types/synced-metadata-types.js'; import { useCurrentUserFID, useUnlinkFID } from '../utils/farcaster-utils.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; import { useSelector, useDispatch } from '../utils/redux-utils.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; type Props = { +children?: React.Node, }; function FarcasterDataHandler(props: Props): React.Node { const { children } = props; const isActive = useSelector(state => state.lifecycleState !== 'background'); const currentUserID = useSelector(state => state.currentUserInfo?.id); const loggedIn = useIsLoggedInToIdentityAndAuthoritativeKeyserver(); const neynarClient = React.useContext(NeynarClientContext)?.client; const identityServiceClient = React.useContext(IdentityClientContext); const getFarcasterUsers = identityServiceClient?.identityClient.getFarcasterUsers; const findUserIdentities = useFindUserIdentities(); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); - const updateRelationships = useLegacyAshoatKeyserverCall( - serverUpdateRelationships, - ); + + const updateRelationships = useUpdateRelationships(); const createThreadsAndRobotextForFarcasterMutuals = React.useCallback( - (userIDsToFID: { +[userID: string]: string }) => - updateRelationships({ - action: relationshipActions.FARCASTER_MUTUAL, - userIDsToFID, - }), + (userIDs: $ReadOnlyArray) => + updateRelationships(relationshipActions.FARCASTER_MUTUAL, userIDs), [updateRelationships], ); const userInfos = useSelector(state => state.userStore.userInfos); const fid = useCurrentUserFID(); const unlinkFID = useUnlinkFID(); const prevCanQueryRef = React.useRef(); // It's possible for the user to log out while handleFarcasterMutuals below // is running. It's not a big deal, but this can lead to a useless server call // at the end. To avoid that useless server call, we want to check whether the // user is logged in beforehand, but the value of loggedIn bound in the // callback will be outdated. Instead, we can check this ref, which will be // updated on every render. const loggedInRef = React.useRef(loggedIn); loggedInRef.current = loggedIn; const handleFarcasterMutuals = React.useCallback(async () => { const canQuery = isActive && !!fid && loggedIn; if (canQuery === prevCanQueryRef.current) { return; } prevCanQueryRef.current = canQuery; if ( !loggedIn || !isActive || !fid || !neynarClient || !getFarcasterUsers || !currentUserID ) { return; } const followerFIDs = await neynarClient.fetchFriendFIDs(fid); const commFCUsers = await getFarcasterUsers(followerFIDs); const newCommUsers = commFCUsers.filter(({ userID }) => !userInfos[userID]); if (newCommUsers.length === 0) { return; } - const userIDsToFID: { +[userID: string]: string } = Object.fromEntries( - newCommUsers.map(({ userID, farcasterID }) => [userID, farcasterID]), - ); - - const userIDsToFIDIncludingCurrentUser: { +[userID: string]: string } = { - ...userIDsToFID, - [(currentUserID: string)]: fid, - }; + const newCommUserIDs = newCommUsers.map(({ userID }) => userID); if (!loggedInRef.current) { return; } void dispatchActionPromise( updateRelationshipsActionTypes, - createThreadsAndRobotextForFarcasterMutuals( - userIDsToFIDIncludingCurrentUser, - ), + createThreadsAndRobotextForFarcasterMutuals(newCommUserIDs), ); }, [ isActive, fid, loggedIn, neynarClient, getFarcasterUsers, userInfos, dispatchActionPromise, createThreadsAndRobotextForFarcasterMutuals, currentUserID, ]); const handleUserStoreFIDs = React.useCallback(async () => { if (!loggedIn || !isActive) { return; } const userStoreIDs = Object.keys(userInfos); const { identities: userIdentities } = await findUserIdentities(userStoreIDs); const userStoreFarcasterUsers = Object.entries(userIdentities) .filter(([, identity]) => identity.farcasterID !== null) .map(([userID, identity]) => ({ userID, username: identity.username, farcasterID: identity.farcasterID, })); dispatch({ type: setAuxUserFIDsActionType, payload: { farcasterUsers: userStoreFarcasterUsers, }, }); }, [loggedIn, isActive, findUserIdentities, userInfos, dispatch]); const prevCanQueryHandleCurrentUserFIDRef = React.useRef(); const canQueryHandleCurrentUserFID = isActive && loggedIn; const [fidLoaded, setFIDLoaded] = React.useState(false); const handleCurrentUserFID = React.useCallback(async () => { if ( canQueryHandleCurrentUserFID === prevCanQueryHandleCurrentUserFIDRef.current ) { return; } prevCanQueryHandleCurrentUserFIDRef.current = canQueryHandleCurrentUserFID; if (!canQueryHandleCurrentUserFID || !currentUserID || !neynarClient) { return; } if (fid) { const isCurrentUserFIDValid = await neynarClient.checkIfCurrentUserFIDIsValid(fid); if (!isCurrentUserFIDValid) { await unlinkFID(); return; } return; } const { identities: userIdentities } = await findUserIdentities([ currentUserID, ]); const identityFID = userIdentities[currentUserID]?.farcasterID; if (identityFID) { dispatch({ type: setSyncedMetadataEntryActionType, payload: { name: syncedMetadataNames.CURRENT_USER_FID, data: identityFID, }, }); } setFIDLoaded(true); }, [ canQueryHandleCurrentUserFID, findUserIdentities, currentUserID, neynarClient, fid, unlinkFID, dispatch, ]); React.useEffect(() => { if (!usingCommServicesAccessToken) { return; } void handleFarcasterMutuals(); void handleUserStoreFIDs(); void handleCurrentUserFID(); }, [handleCurrentUserFID, handleFarcasterMutuals, handleUserStoreFIDs]); React.useEffect(() => { if (loggedIn) { return; } setFIDLoaded(false); }, [loggedIn]); const farcasterDataHandler = React.useMemo(() => { if (!fidLoaded) { return null; } return children; }, [children, fidLoaded]); return farcasterDataHandler; } export { FarcasterDataHandler }; diff --git a/lib/handlers/user-infos-handler.react.js b/lib/handlers/user-infos-handler.react.js index 0116017e7..fb83c12d7 100644 --- a/lib/handlers/user-infos-handler.react.js +++ b/lib/handlers/user-infos-handler.react.js @@ -1,194 +1,190 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; -import { - updateRelationships, - updateRelationshipsActionTypes, -} from '../actions/relationship-actions.js'; +import { updateRelationshipsActionTypes } from '../actions/relationship-actions.js'; import { useFindUserIdentities, findUserIdentitiesActionTypes, } from '../actions/user-actions.js'; import { useIsLoggedInToAuthoritativeKeyserver } from '../hooks/account-hooks.js'; import { useGetAndUpdateDeviceListsForUsers } from '../hooks/peer-list-hooks.js'; -import { useLegacyAshoatKeyserverCall } from '../keyserver-conn/legacy-keyserver-call.js'; +import { useUpdateRelationships } from '../hooks/relationship-hooks.js'; import { usersWithMissingDeviceListSelector } from '../selectors/user-selectors.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import { useTunnelbroker } from '../tunnelbroker/tunnelbroker-context.js'; import { relationshipActions } from '../types/relationship-types.js'; import { getMessageForException, FetchTimeout } from '../utils/errors.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; import { useSelector } from '../utils/redux-utils.js'; import { relyingOnAuthoritativeKeyserver, usingCommServicesAccessToken, } from '../utils/services-utils.js'; function UserInfosHandler(): React.Node { const client = React.useContext(IdentityClientContext); invariant(client, 'Identity context should be set'); const { getAuthMetadata } = client; const userInfos = useSelector(state => state.userStore.userInfos); const userInfosWithMissingUsernames = React.useMemo(() => { const entriesWithoutUsernames = Object.entries(userInfos).filter( ([, value]) => !value.username, ); return Object.fromEntries(entriesWithoutUsernames); }, [userInfos]); const dispatchActionPromise = useDispatchActionPromise(); const findUserIdentities = useFindUserIdentities(); const requestedIDsRef = React.useRef(new Set()); const requestedAvatarsRef = React.useRef(new Set()); - const callUpdateRelationships = - useLegacyAshoatKeyserverCall(updateRelationships); + const updateRelationships = useUpdateRelationships(); const currentUserInfo = useSelector(state => state.currentUserInfo); const loggedInToAuthKeyserver = useIsLoggedInToAuthoritativeKeyserver(); React.useEffect(() => { if (!loggedInToAuthKeyserver) { return; } const newUserIDs = Object.keys(userInfosWithMissingUsernames).filter( id => !requestedIDsRef.current.has(id), ); if (!usingCommServicesAccessToken || newUserIDs.length === 0) { return; } void (async () => { const authMetadata = await getAuthMetadata(); if (!authMetadata) { return; } // 1. Fetch usernames from identity const promise = (async () => { newUserIDs.forEach(id => requestedIDsRef.current.add(id)); const { identities, reservedUserIdentifiers } = await findUserIdentities(newUserIDs); newUserIDs.forEach(id => requestedIDsRef.current.delete(id)); const newUserInfos = []; for (const id in identities) { newUserInfos.push({ id, username: identities[id].username, }); } for (const id in reservedUserIdentifiers) { newUserInfos.push({ id, username: reservedUserIdentifiers[id], }); } return { userInfos: newUserInfos }; })(); void dispatchActionPromise(findUserIdentitiesActionTypes, promise); // 2. Fetch avatars from auth keyserver if (relyingOnAuthoritativeKeyserver) { const userIDsWithoutOwnID = newUserIDs.filter( id => id !== currentUserInfo?.id && !requestedAvatarsRef.current.has(id), ); if (userIDsWithoutOwnID.length === 0) { return; } userIDsWithoutOwnID.forEach(id => requestedAvatarsRef.current.add(id)); const updateRelationshipsPromise = (async () => { try { - return await callUpdateRelationships({ - action: relationshipActions.ACKNOWLEDGE, - userIDs: userIDsWithoutOwnID, - }); + return await updateRelationships( + relationshipActions.ACKNOWLEDGE, + userIDsWithoutOwnID, + ); } catch (e) { if (e instanceof FetchTimeout) { userIDsWithoutOwnID.forEach(id => requestedAvatarsRef.current.delete(id), ); } throw e; } })(); void dispatchActionPromise( updateRelationshipsActionTypes, updateRelationshipsPromise, ); } })(); }, [ getAuthMetadata, - callUpdateRelationships, + updateRelationships, currentUserInfo?.id, dispatchActionPromise, findUserIdentities, userInfos, userInfosWithMissingUsernames, loggedInToAuthKeyserver, ]); const usersWithMissingDeviceListSelected = useSelector( usersWithMissingDeviceListSelector, ); const getAndUpdateDeviceListsForUsers = useGetAndUpdateDeviceListsForUsers(); const { socketState } = useTunnelbroker(); const requestedDeviceListsIDsRef = React.useRef(new Set()); React.useEffect(() => { const usersWithMissingDeviceList = usersWithMissingDeviceListSelected.filter( id => !requestedDeviceListsIDsRef.current.has(id), ); if ( !usingCommServicesAccessToken || usersWithMissingDeviceList.length === 0 || !socketState.isAuthorized ) { return; } void (async () => { const authMetadata = await getAuthMetadata(); if (!authMetadata) { return; } try { usersWithMissingDeviceList.forEach(id => requestedDeviceListsIDsRef.current.add(id), ); const foundDeviceListIDs = await getAndUpdateDeviceListsForUsers( usersWithMissingDeviceList, true, ); Object.keys(foundDeviceListIDs).forEach(id => requestedDeviceListsIDsRef.current.delete(id), ); } catch (e) { console.log( `Error getting and setting peer device list: ${ getMessageForException(e) ?? 'unknown' }`, ); } })(); }, [ getAndUpdateDeviceListsForUsers, getAuthMetadata, socketState.isAuthorized, usersWithMissingDeviceListSelected, ]); } export { UserInfosHandler }; diff --git a/lib/hooks/relationship-hooks.js b/lib/hooks/relationship-hooks.js new file mode 100644 index 000000000..803a29ae8 --- /dev/null +++ b/lib/hooks/relationship-hooks.js @@ -0,0 +1,36 @@ +// @flow + +import * as React from 'react'; + +import { updateRelationships as serverUpdateRelationships } from '../actions/relationship-actions.js'; +import { useLegacyAshoatKeyserverCall } from '../keyserver-conn/legacy-keyserver-call.js'; +import type { + RelationshipAction, + RelationshipErrors, +} from '../types/relationship-types.js'; + +function useUpdateRelationships(): ( + action: RelationshipAction, + userIDs: $ReadOnlyArray, +) => Promise { + const updateRelationships = useLegacyAshoatKeyserverCall( + serverUpdateRelationships, + ); + return React.useCallback( + (action: RelationshipAction, userIDs: $ReadOnlyArray) => + updateRelationships({ + action, + users: Object.fromEntries( + userIDs.map(userID => [ + userID, + { + createRobotextInThinThread: true, + }, + ]), + ), + }), + [updateRelationships], + ); +} + +export { useUpdateRelationships }; diff --git a/lib/hooks/relationship-prompt.js b/lib/hooks/relationship-prompt.js index 7b87f345b..24f2e2dd0 100644 --- a/lib/hooks/relationship-prompt.js +++ b/lib/hooks/relationship-prompt.js @@ -1,171 +1,163 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; -import { - updateRelationships as serverUpdateRelationships, - updateRelationshipsActionTypes, -} from '../actions/relationship-actions.js'; -import { useLegacyAshoatKeyserverCall } from '../keyserver-conn/legacy-keyserver-call.js'; +import { updateRelationshipsActionTypes } from '../actions/relationship-actions.js'; +import { useUpdateRelationships } from '../hooks/relationship-hooks.js'; import { getSingleOtherUser } from '../shared/thread-utils.js'; import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import { relationshipActions, type TraditionalRelationshipAction, } from '../types/relationship-types.js'; import type { UserInfo } from '../types/user-types.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; import { useSelector } from '../utils/redux-utils.js'; type RelationshipCallbacks = { +blockUser: () => void, +unblockUser: () => void, +friendUser: () => void, +unfriendUser: () => void, }; type RelationshipLoadingState = { +isLoadingBlockUser: boolean, +isLoadingUnblockUser: boolean, +isLoadingFriendUser: boolean, +isLoadingUnfriendUser: boolean, }; type RelationshipPromptData = { +otherUserInfo: ?UserInfo, +callbacks: RelationshipCallbacks, +loadingState: RelationshipLoadingState, }; function useRelationshipPrompt( threadInfo: ThreadInfo, onErrorCallback?: () => void, pendingPersonalThreadUserInfo?: ?UserInfo, ): RelationshipPromptData { // We're fetching the info from state because we need the most recent // relationship status. Additionally, member info does not contain info // about relationship. const otherUserInfo = useSelector(state => { const otherUserID = getSingleOtherUser(threadInfo, state.currentUserInfo?.id) ?? pendingPersonalThreadUserInfo?.id; const { userInfos } = state.userStore; return otherUserID && userInfos[otherUserID] ? userInfos[otherUserID] : pendingPersonalThreadUserInfo; }); const { callbacks, loadingState } = useRelationshipCallbacks( otherUserInfo?.id, onErrorCallback, ); return React.useMemo( () => ({ otherUserInfo, callbacks, loadingState, }), [callbacks, loadingState, otherUserInfo], ); } function useRelationshipCallbacks( otherUserID?: string, onErrorCallback?: () => void, ): { +callbacks: RelationshipCallbacks, +loadingState: RelationshipLoadingState, } { const [isLoadingBlockUser, setIsLoadingBlockUser] = React.useState(false); const [isLoadingUnblockUser, setIsLoadingUnblockUser] = React.useState(false); const [isLoadingFriendUser, setIsLoadingFriendUser] = React.useState(false); const [isLoadingUnfriendUser, setIsLoadingUnfriendUser] = React.useState(false); - const callUpdateRelationships = useLegacyAshoatKeyserverCall( - serverUpdateRelationships, - ); + const updateRelationships = useUpdateRelationships(); const updateRelationship = React.useCallback( async ( action: TraditionalRelationshipAction, setInProgress: boolean => mixed, ) => { try { setInProgress(true); invariant(otherUserID, 'Other user info id should be present'); - return await callUpdateRelationships({ - action, - userIDs: [otherUserID], - }); + return await updateRelationships(action, [otherUserID]); } catch (e) { onErrorCallback?.(); throw e; } finally { setInProgress(false); } }, - [callUpdateRelationships, onErrorCallback, otherUserID], + [updateRelationships, onErrorCallback, otherUserID], ); const dispatchActionPromise = useDispatchActionPromise(); const onButtonPress = React.useCallback( ( action: TraditionalRelationshipAction, setInProgress: boolean => mixed, ) => { void dispatchActionPromise( updateRelationshipsActionTypes, updateRelationship(action, setInProgress), ); }, [dispatchActionPromise, updateRelationship], ); const blockUser = React.useCallback( () => onButtonPress(relationshipActions.BLOCK, setIsLoadingBlockUser), [onButtonPress], ); const unblockUser = React.useCallback( () => onButtonPress(relationshipActions.UNBLOCK, setIsLoadingUnblockUser), [onButtonPress], ); const friendUser = React.useCallback( () => onButtonPress(relationshipActions.FRIEND, setIsLoadingFriendUser), [onButtonPress], ); const unfriendUser = React.useCallback( () => onButtonPress(relationshipActions.UNFRIEND, setIsLoadingUnfriendUser), [onButtonPress], ); return React.useMemo( () => ({ callbacks: { blockUser, unblockUser, friendUser, unfriendUser, }, loadingState: { isLoadingBlockUser, isLoadingUnblockUser, isLoadingFriendUser, isLoadingUnfriendUser, }, }), [ blockUser, friendUser, isLoadingBlockUser, isLoadingFriendUser, isLoadingUnblockUser, isLoadingUnfriendUser, unblockUser, unfriendUser, ], ); } export { useRelationshipPrompt, useRelationshipCallbacks }; diff --git a/native/chat/settings/thread-settings-edit-relationship.react.js b/native/chat/settings/thread-settings-edit-relationship.react.js index b3969315b..e8e78fcfe 100644 --- a/native/chat/settings/thread-settings-edit-relationship.react.js +++ b/native/chat/settings/thread-settings-edit-relationship.react.js @@ -1,135 +1,127 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, View } from 'react-native'; -import { - updateRelationships as serverUpdateRelationships, - updateRelationshipsActionTypes, -} from 'lib/actions/relationship-actions.js'; +import { updateRelationshipsActionTypes } from 'lib/actions/relationship-actions.js'; import { useENSNames } from 'lib/hooks/ens-cache.js'; -import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; +import { useUpdateRelationships } from 'lib/hooks/relationship-hooks.js'; import { getRelationshipActionText, getRelationshipDispatchAction, } from 'lib/shared/relationship-utils.js'; import { getSingleOtherUser } from 'lib/shared/thread-utils.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import { type TraditionalRelationshipAction, type RelationshipButton, } from 'lib/types/relationship-types.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import Button from '../../components/button.react.js'; import { useSelector } from '../../redux/redux-utils.js'; import { useColors, useStyles } from '../../themes/colors.js'; import type { ViewStyle } from '../../types/styles.js'; import { unknownErrorAlertDetails } from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; type Props = { +threadInfo: ThreadInfo, +buttonStyle: ViewStyle, +relationshipButton: RelationshipButton, }; const ThreadSettingsEditRelationship: React.ComponentType = React.memo(function ThreadSettingsEditRelationship(props: Props) { const otherUserInfoFromRedux = useSelector(state => { const currentUserID = state.currentUserInfo?.id; const otherUserID = getSingleOtherUser(props.threadInfo, currentUserID); invariant(otherUserID, 'Other user should be specified'); const { userInfos } = state.userStore; return userInfos[otherUserID]; }); invariant(otherUserInfoFromRedux, 'Other user info should be specified'); const [otherUserInfo] = useENSNames([otherUserInfoFromRedux]); - const callUpdateRelationships = useLegacyAshoatKeyserverCall( - serverUpdateRelationships, - ); + const updateRelationships = useUpdateRelationships(); const updateRelationship = React.useCallback( async (action: TraditionalRelationshipAction) => { try { - return await callUpdateRelationships({ - action, - userIDs: [otherUserInfo.id], - }); + return await updateRelationships(action, [otherUserInfo.id]); } catch (e) { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK' }], { cancelable: true, }, ); throw e; } }, - [callUpdateRelationships, otherUserInfo], + [updateRelationships, otherUserInfo], ); const { relationshipButton } = props; const relationshipAction = React.useMemo( () => getRelationshipDispatchAction(relationshipButton), [relationshipButton], ); const dispatchActionPromise = useDispatchActionPromise(); const onButtonPress = React.useCallback(() => { void dispatchActionPromise( updateRelationshipsActionTypes, updateRelationship(relationshipAction), ); }, [dispatchActionPromise, relationshipAction, updateRelationship]); const colors = useColors(); const { panelIosHighlightUnderlay } = colors; const styles = useStyles(unboundStyles); const otherUserInfoUsername = otherUserInfo.username; invariant(otherUserInfoUsername, 'Other user username should be specified'); const relationshipButtonText = React.useMemo( () => getRelationshipActionText(relationshipButton, otherUserInfoUsername), [otherUserInfoUsername, relationshipButton], ); return ( ); }); const unboundStyles = { button: { flexDirection: 'row', paddingHorizontal: 12, paddingVertical: 10, }, container: { backgroundColor: 'panelForeground', paddingHorizontal: 12, }, text: { color: 'redText', flex: 1, fontSize: 16, }, }; export default ThreadSettingsEditRelationship; diff --git a/native/profile/relationship-list-item.react.js b/native/profile/relationship-list-item.react.js index f8c4850f9..4693237c2 100644 --- a/native/profile/relationship-list-item.react.js +++ b/native/profile/relationship-list-item.react.js @@ -1,363 +1,359 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, Text, TouchableOpacity, ActivityIndicator } from 'react-native'; -import { - updateRelationshipsActionTypes, - updateRelationships, -} from 'lib/actions/relationship-actions.js'; -import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; +import { updateRelationshipsActionTypes } from 'lib/actions/relationship-actions.js'; +import { useUpdateRelationships } from 'lib/hooks/relationship-hooks.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { ReactRef } from 'lib/types/react-types.js'; import { type TraditionalRelationshipAction, type RelationshipErrors, userRelationshipStatus, relationshipActions, - type LegacyRelationshipRequest, + type RelationshipAction, } from 'lib/types/relationship-types.js'; import type { AccountUserInfo, GlobalAccountUserInfo, } from 'lib/types/user-types.js'; import { useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import type { RelationshipListNavigate } from './relationship-list.react.js'; import UserAvatar from '../avatars/user-avatar.react.js'; import PencilIcon from '../components/pencil-icon.react.js'; import SingleLine from '../components/single-line.react.js'; import { type KeyboardState, KeyboardContext, } from '../keyboard/keyboard-state.js'; import { OverlayContext, type OverlayContextType, } from '../navigation/overlay-context.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { UserRelationshipTooltipModalRouteName, FriendListRouteName, BlockListRouteName, } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { type Colors, useColors, useStyles } from '../themes/colors.js'; import type { VerticalBounds } from '../types/layout-types.js'; import { useNavigateToUserProfileBottomSheet } from '../user-profile/user-profile-utils.js'; import { unknownErrorAlertDetails } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; const unboundStyles = { container: { flex: 1, flexDirection: 'row', paddingHorizontal: 24, paddingVertical: 10, backgroundColor: 'panelForeground', borderColor: 'panelForegroundBorder', }, borderBottom: { borderBottomWidth: 1, }, buttonContainer: { flexDirection: 'row', }, editButtonWithMargin: { marginLeft: 15, }, username: { color: 'panelForegroundSecondaryLabel', flex: 1, fontSize: 16, lineHeight: 20, marginLeft: 8, }, editButton: { paddingLeft: 10, }, blueAction: { color: 'link', fontSize: 16, paddingLeft: 6, }, redAction: { color: 'redText', fontSize: 16, paddingLeft: 6, }, }; type BaseProps = { +userInfo: AccountUserInfo, +lastListItem: boolean, +verticalBounds: ?VerticalBounds, +relationshipListRoute: NavigationRoute<'FriendList' | 'BlockList'>, +navigate: RelationshipListNavigate, +onSelect: (selectedUser: GlobalAccountUserInfo) => void, }; type Props = { ...BaseProps, // Redux state +removeUserLoadingStatus: LoadingStatus, +colors: Colors, +styles: $ReadOnly, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +updateRelationships: ( - request: LegacyRelationshipRequest, + action: RelationshipAction, + userIDs: $ReadOnlyArray, ) => Promise, // withOverlayContext +overlayContext: ?OverlayContextType, // withKeyboardState +keyboardState: ?KeyboardState, +navigateToUserProfileBottomSheet: (userID: string) => mixed, }; class RelationshipListItem extends React.PureComponent { editButton: ReactRef> = React.createRef(); render(): React.Node { const { lastListItem, removeUserLoadingStatus, userInfo, relationshipListRoute, } = this.props; const relationshipsToEdit = { [FriendListRouteName]: [userRelationshipStatus.FRIEND], [BlockListRouteName]: [ userRelationshipStatus.BOTH_BLOCKED, userRelationshipStatus.BLOCKED_BY_VIEWER, ], }[relationshipListRoute.name]; const canEditFriendRequest = { [FriendListRouteName]: true, [BlockListRouteName]: false, }[relationshipListRoute.name]; const borderBottom = lastListItem ? null : this.props.styles.borderBottom; let editButton = null; if (removeUserLoadingStatus === 'loading') { editButton = ( ); } else if (relationshipsToEdit.includes(userInfo.relationshipStatus)) { editButton = ( ); } else if ( userInfo.relationshipStatus === userRelationshipStatus.REQUEST_RECEIVED && canEditFriendRequest ) { editButton = ( Accept Reject ); } else if ( userInfo.relationshipStatus === userRelationshipStatus.REQUEST_SENT && canEditFriendRequest ) { editButton = ( Cancel request ); } else { editButton = ( Add ); } return ( {this.props.userInfo.username} {editButton} ); } onPressUser = () => { this.props.navigateToUserProfileBottomSheet(this.props.userInfo.id); }; onSelect = () => { const { id, username } = this.props.userInfo; this.props.onSelect({ id, username }); }; visibleEntryIDs(): [string] { const { relationshipListRoute } = this.props; const id = { [FriendListRouteName]: 'unfriend', [BlockListRouteName]: 'unblock', }[relationshipListRoute.name]; return [id]; } onPressEdit = () => { if (this.props.keyboardState?.dismissKeyboardIfShowing()) { return; } const { editButton, props: { verticalBounds }, } = this; const { overlayContext, userInfo } = this.props; invariant( overlayContext, 'RelationshipListItem should have OverlayContext', ); overlayContext.setScrollBlockingModalStatus('open'); if (!editButton.current || !verticalBounds) { return; } const { relationshipStatus, ...restUserInfo } = userInfo; const relativeUserInfo = { ...restUserInfo, isViewer: false, }; editButton.current.measure((x, y, width, height, pageX, pageY) => { const coordinates = { x: pageX, y: pageY, width, height }; this.props.navigate<'UserRelationshipTooltipModal'>({ name: UserRelationshipTooltipModalRouteName, params: { presentedFrom: this.props.relationshipListRoute.key, initialCoordinates: coordinates, verticalBounds, visibleEntryIDs: this.visibleEntryIDs(), relativeUserInfo, tooltipButtonIcon: 'pencil', }, }); }); }; // We need to set onLayout in order to allow .measure() to be on the ref onLayout = () => {}; onPressFriendUser = () => { this.onPressUpdateFriendship(relationshipActions.FRIEND); }; onPressUnfriendUser = () => { this.onPressUpdateFriendship(relationshipActions.UNFRIEND); }; onPressUpdateFriendship(action: TraditionalRelationshipAction) { const { id } = this.props.userInfo; const customKeyName = `${updateRelationshipsActionTypes.started}:${id}`; void this.props.dispatchActionPromise( updateRelationshipsActionTypes, this.updateFriendship(action), { customKeyName }, ); } async updateFriendship( action: TraditionalRelationshipAction, ): Promise { try { - return await this.props.updateRelationships({ - action, - userIDs: [this.props.userInfo.id], - }); + return await this.props.updateRelationships(action, [ + this.props.userInfo.id, + ]); } catch (e) { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK' }], { cancelable: true, }, ); throw e; } } } const ConnectedRelationshipListItem: React.ComponentType = React.memo(function ConnectedRelationshipListItem( props: BaseProps, ) { const removeUserLoadingStatus = useSelector(state => createLoadingStatusSelector( updateRelationshipsActionTypes, `${updateRelationshipsActionTypes.started}:${props.userInfo.id}`, )(state), ); const colors = useColors(); const styles = useStyles(unboundStyles); const dispatchActionPromise = useDispatchActionPromise(); - const boundUpdateRelationships = - useLegacyAshoatKeyserverCall(updateRelationships); + const updateRelationships = useUpdateRelationships(); const overlayContext = React.useContext(OverlayContext); const keyboardState = React.useContext(KeyboardContext); const navigateToUserProfileBottomSheet = useNavigateToUserProfileBottomSheet(); return ( ); }); export default ConnectedRelationshipListItem; diff --git a/native/profile/relationship-list.react.js b/native/profile/relationship-list.react.js index f3b4e10da..37d9e6040 100644 --- a/native/profile/relationship-list.react.js +++ b/native/profile/relationship-list.react.js @@ -1,489 +1,482 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, Text, Platform } from 'react-native'; import { FlatList } from 'react-native-gesture-handler'; -import { - updateRelationshipsActionTypes, - updateRelationships, -} from 'lib/actions/relationship-actions.js'; +import { updateRelationshipsActionTypes } from 'lib/actions/relationship-actions.js'; import { useENSNames } from 'lib/hooks/ens-cache.js'; -import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; +import { useUpdateRelationships } from 'lib/hooks/relationship-hooks.js'; import { registerFetchKey } from 'lib/reducers/loading-reducer.js'; import { useUserSearchIndex } from 'lib/selectors/nav-selectors.js'; import { userRelationshipsSelector } from 'lib/selectors/relationship-selectors.js'; import { useSearchUsers } from 'lib/shared/search-utils.js'; import { userRelationshipStatus, relationshipActions, } from 'lib/types/relationship-types.js'; import type { GlobalAccountUserInfo, AccountUserInfo, } from 'lib/types/user-types.js'; import { values } from 'lib/utils/objects.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import RelationshipListItem from './relationship-list-item.react.js'; import LinkButton from '../components/link-button.react.js'; import { createTagInput, BaseTagInput } from '../components/tag-input.react.js'; import { KeyboardContext } from '../keyboard/keyboard-state.js'; import { OverlayContext } from '../navigation/overlay-context.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { FriendListRouteName, BlockListRouteName, } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStyles, useIndicatorStyle } from '../themes/colors.js'; import type { VerticalBounds } from '../types/layout-types.js'; import { unknownErrorAlertDetails } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; const TagInput = createTagInput(); export type RelationshipListNavigate = $PropertyType< ProfileNavigationProp<'FriendList' | 'BlockList'>, 'navigate', >; const tagInputProps = { placeholder: 'username', autoFocus: true, returnKeyType: 'go', }; type ListItem = | { +type: 'empty', +because: 'no-relationships' | 'no-results' } | { +type: 'header' } | { +type: 'footer' } | { +type: 'user', +userInfo: AccountUserInfo, +lastListItem: boolean, +verticalBounds: ?VerticalBounds, }; // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return function keyExtractor(item: ListItem) { if (item.userInfo) { return item.userInfo.id; } else if (item.type === 'empty') { return 'empty'; } else if (item.type === 'header') { return 'header'; } else if (item.type === 'footer') { return 'footer'; } invariant(false, 'keyExtractor conditions should be exhaustive'); } const tagDataLabelExtractor = (userInfo: GlobalAccountUserInfo) => userInfo.username; type Props = { +navigation: ProfileNavigationProp<'FriendList' | 'BlockList'>, +route: NavigationRoute<'FriendList' | 'BlockList'>, }; function RelationshipList(props: Props): React.Node { const { route } = props; const routeName = route.name; const excludeStatuses = React.useMemo( () => ({ [FriendListRouteName]: [ userRelationshipStatus.BLOCKED_VIEWER, userRelationshipStatus.BOTH_BLOCKED, ], [BlockListRouteName]: [], })[routeName], [routeName], ); const userInfos = useSelector(state => state.userStore.userInfos); const userInfosArray = React.useMemo( () => values(userInfos).filter(userInfo => { const relationship = userInfo.relationshipStatus; return !excludeStatuses.includes(relationship); }), [userInfos, excludeStatuses], ); const [searchInputText, setSearchInputText] = React.useState(''); const [userStoreSearchResults, setUserStoreSearchResults] = React.useState< $ReadOnlySet, >(new Set()); const serverSearchResults = useSearchUsers(searchInputText); const filteredServerSearchResults = React.useMemo( () => serverSearchResults.filter(searchUserInfo => { const userInfo = userInfos[searchUserInfo.id]; return ( !userInfo || !excludeStatuses.includes(userInfo.relationshipStatus) ); }), [serverSearchResults, userInfos, excludeStatuses], ); const userStoreSearchIndex = useUserSearchIndex(userInfosArray); const onChangeSearchText = React.useCallback( async (searchText: string) => { setSearchInputText(searchText); const results = userStoreSearchIndex.getSearchResults(searchText); setUserStoreSearchResults(new Set(results)); }, [userStoreSearchIndex], ); const overlayContext = React.useContext(OverlayContext); invariant(overlayContext, 'RelationshipList should have OverlayContext'); const scrollEnabled = overlayContext.scrollBlockingModalStatus === 'closed'; const tagInputRef = React.useRef>(); const flatListContainerRef = React.useRef>(); const keyboardState = React.useContext(KeyboardContext); const keyboardNotShowing = !!( keyboardState && !keyboardState.keyboardShowing ); const [verticalBounds, setVerticalBounds] = React.useState(null); const onFlatListContainerLayout = React.useCallback(() => { if (!flatListContainerRef.current) { return; } if (!keyboardNotShowing) { return; } flatListContainerRef.current.measure( (x, y, width, height, pageX, pageY) => { if ( height === null || height === undefined || pageY === null || pageY === undefined ) { return; } setVerticalBounds({ height, y: pageY }); }, ); }, [keyboardNotShowing]); const [currentTags, setCurrentTags] = React.useState< $ReadOnlyArray, >([]); const onSelect = React.useCallback( (selectedUser: GlobalAccountUserInfo) => { if (currentTags.find(o => o.id === selectedUser.id)) { return; } setSearchInputText(''); setCurrentTags(prevCurrentTags => prevCurrentTags.concat(selectedUser)); }, [currentTags], ); const onUnknownErrorAlertAcknowledged = React.useCallback(() => { setCurrentTags([]); setSearchInputText(''); tagInputRef.current?.focus(); }, []); - const callUpdateRelationships = - useLegacyAshoatKeyserverCall(updateRelationships); + const updateRelationships = useUpdateRelationships(); const updateRelationshipsOnServer = React.useCallback(async () => { const action = { [FriendListRouteName]: relationshipActions.FRIEND, [BlockListRouteName]: relationshipActions.BLOCK, }[routeName]; const userIDs = currentTags.map(userInfo => userInfo.id); try { - const result = await callUpdateRelationships({ - action, - userIDs, - }); + const result = await updateRelationships(action, userIDs); setCurrentTags([]); setSearchInputText(''); return result; } catch (e) { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: onUnknownErrorAlertAcknowledged }], { cancelable: true, onDismiss: onUnknownErrorAlertAcknowledged }, ); throw e; } }, [ routeName, currentTags, - callUpdateRelationships, + updateRelationships, onUnknownErrorAlertAcknowledged, ]); const dispatchActionPromise = useDispatchActionPromise(); const noCurrentTags = currentTags.length === 0; const onPressAdd = React.useCallback(() => { if (noCurrentTags) { return; } void dispatchActionPromise( updateRelationshipsActionTypes, updateRelationshipsOnServer(), ); }, [noCurrentTags, dispatchActionPromise, updateRelationshipsOnServer]); const inputProps = React.useMemo( () => ({ ...tagInputProps, onSubmitEditing: onPressAdd, }), [onPressAdd], ); const { navigation } = props; const { navigate } = navigation; const styles = useStyles(unboundStyles); const renderItem = React.useCallback( // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return ({ item }: { item: ListItem, ... }) => { if (item.type === 'empty') { const action = { [FriendListRouteName]: 'added', [BlockListRouteName]: 'blocked', }[routeName]; const emptyMessage = item.because === 'no-relationships' ? `You haven't ${action} any users yet` : 'No results'; return {emptyMessage}; } else if (item.type === 'header' || item.type === 'footer') { return ; } else if (item.type === 'user') { return ( ); } else { invariant(false, `unexpected RelationshipList item type ${item.type}`); } }, [routeName, navigate, route, onSelect, styles.emptyText, styles.separator], ); const { setOptions } = navigation; const prevNoCurrentTags = React.useRef(noCurrentTags); React.useEffect(() => { let setSaveButtonDisabled; if (!prevNoCurrentTags.current && noCurrentTags) { setSaveButtonDisabled = true; } else if (prevNoCurrentTags.current && !noCurrentTags) { setSaveButtonDisabled = false; } prevNoCurrentTags.current = noCurrentTags; if (setSaveButtonDisabled === undefined) { return; } setOptions({ headerRight: () => ( ), }); }, [setOptions, noCurrentTags, onPressAdd]); const relationships = useSelector(userRelationshipsSelector); const viewerID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const usersWithoutENSNames = React.useMemo(() => { if (searchInputText === '') { return { [FriendListRouteName]: relationships.friends, [BlockListRouteName]: relationships.blocked, }[routeName]; } const mergedUserInfos: { [id: string]: AccountUserInfo } = {}; for (const userInfo of filteredServerSearchResults) { mergedUserInfos[userInfo.id] = userInfo; } for (const id of userStoreSearchResults) { const { username, relationshipStatus } = userInfos[id]; if (username) { mergedUserInfos[id] = { id, username, relationshipStatus }; } } const excludeUserIDsArray = currentTags .map(userInfo => userInfo.id) .concat(viewerID || []); const excludeUserIDs = new Set(excludeUserIDsArray); const sortToEnd = []; const userSearchResults = []; const sortRelationshipTypesToEnd = { [FriendListRouteName]: [userRelationshipStatus.FRIEND], [BlockListRouteName]: [ userRelationshipStatus.BLOCKED_BY_VIEWER, userRelationshipStatus.BOTH_BLOCKED, ], }[routeName]; for (const userID in mergedUserInfos) { if (excludeUserIDs.has(userID)) { continue; } const userInfo = mergedUserInfos[userID]; if (sortRelationshipTypesToEnd.includes(userInfo.relationshipStatus)) { sortToEnd.push(userInfo); } else { userSearchResults.push(userInfo); } } return userSearchResults.concat(sortToEnd); }, [ searchInputText, relationships, routeName, viewerID, currentTags, filteredServerSearchResults, userStoreSearchResults, userInfos, ]); const displayUsers = useENSNames(usersWithoutENSNames); const listData = React.useMemo(() => { let emptyItem; if (displayUsers.length === 0 && searchInputText === '') { emptyItem = { type: 'empty', because: 'no-relationships' }; } else if (displayUsers.length === 0) { emptyItem = { type: 'empty', because: 'no-results' }; } const mappedUsers = displayUsers.map((userInfo, index) => ({ type: 'user', userInfo, lastListItem: displayUsers.length - 1 === index, verticalBounds, })); return [] .concat(emptyItem ? emptyItem : []) .concat(emptyItem ? [] : { type: 'header' }) .concat(mappedUsers) .concat(emptyItem ? [] : { type: 'footer' }); }, [displayUsers, verticalBounds, searchInputText]); const indicatorStyle = useIndicatorStyle(); const currentTagsWithENSNames = useENSNames(currentTags); return ( Search: ); } const unboundStyles = { container: { flex: 1, backgroundColor: 'panelBackground', }, contentContainer: { paddingTop: 12, paddingBottom: 24, }, separator: { backgroundColor: 'panelForegroundBorder', height: Platform.OS === 'android' ? 1.5 : 1, }, emptyText: { color: 'panelForegroundSecondaryLabel', flex: 1, fontSize: 16, lineHeight: 20, textAlign: 'center', paddingHorizontal: 12, paddingVertical: 10, marginHorizontal: 12, }, tagInput: { flex: 1, marginLeft: 8, paddingRight: 12, }, tagInputLabel: { color: 'panelForegroundTertiaryLabel', fontSize: 16, paddingLeft: 12, }, tagInputContainer: { alignItems: 'center', backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', flexDirection: 'row', paddingVertical: 6, }, }; registerFetchKey(updateRelationshipsActionTypes); const MemoizedRelationshipList: React.ComponentType = React.memo(RelationshipList); MemoizedRelationshipList.displayName = 'RelationshipList'; export default MemoizedRelationshipList; diff --git a/native/profile/user-relationship-tooltip-modal.react.js b/native/profile/user-relationship-tooltip-modal.react.js index 0c3b03348..1610a122b 100644 --- a/native/profile/user-relationship-tooltip-modal.react.js +++ b/native/profile/user-relationship-tooltip-modal.react.js @@ -1,172 +1,167 @@ // @flow import * as React from 'react'; import { TouchableOpacity } from 'react-native'; -import { - updateRelationshipsActionTypes, - updateRelationships, -} from 'lib/actions/relationship-actions.js'; -import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; +import { updateRelationshipsActionTypes } from 'lib/actions/relationship-actions.js'; +import { useUpdateRelationships } from 'lib/hooks/relationship-hooks.js'; import { stringForUser } from 'lib/shared/user-utils.js'; import type { RelativeUserInfo } from 'lib/types/user-types.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import PencilIcon from '../components/pencil-icon.react.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import { useColors } from '../themes/colors.js'; import { createTooltip, type TooltipParams, type BaseTooltipProps, type TooltipMenuProps, type TooltipRoute, } from '../tooltip/tooltip.react.js'; import type { UserProfileBottomSheetNavigationProp } from '../user-profile/user-profile-bottom-sheet-navigator.react.js'; import { unknownErrorAlertDetails } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; type Action = 'unfriend' | 'block' | 'unblock'; type TooltipButtonIcon = 'pencil' | 'menu'; export type UserRelationshipTooltipModalParams = TooltipParams<{ +tooltipButtonIcon: TooltipButtonIcon, +relativeUserInfo: RelativeUserInfo, }>; type OnRemoveUserProps = { ...UserRelationshipTooltipModalParams, +action: Action, }; function useRelationshipAction(input: OnRemoveUserProps) { - const boundRemoveRelationships = - useLegacyAshoatKeyserverCall(updateRelationships); + const updateRelationships = useUpdateRelationships(); const dispatchActionPromise = useDispatchActionPromise(); const userText = stringForUser(input.relativeUserInfo); return React.useCallback(() => { const callRemoveRelationships = async () => { try { - return await boundRemoveRelationships({ - action: input.action, - userIDs: [input.relativeUserInfo.id], - }); + return await updateRelationships(input.action, [ + input.relativeUserInfo.id, + ]); } catch (e) { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK' }], { cancelable: true, }, ); throw e; } }; const onConfirmRemoveUser = () => { const customKeyName = `${updateRelationshipsActionTypes.started}:${input.relativeUserInfo.id}`; void dispatchActionPromise( updateRelationshipsActionTypes, callRemoveRelationships(), { customKeyName }, ); }; const action = { unfriend: 'removal', block: 'block', unblock: 'unblock', }[input.action]; const message = { unfriend: `remove ${userText} from friends?`, block: `block ${userText}`, unblock: `unblock ${userText}?`, }[input.action]; Alert.alert( `Confirm ${action}`, `Are you sure you want to ${message}`, [ { text: 'Cancel', style: 'cancel' }, { text: 'OK', onPress: onConfirmRemoveUser }, ], { cancelable: true }, ); - }, [boundRemoveRelationships, dispatchActionPromise, userText, input]); + }, [updateRelationships, dispatchActionPromise, userText, input]); } function TooltipMenu( props: TooltipMenuProps<'UserRelationshipTooltipModal'>, ): React.Node { const { route, tooltipItem: TooltipItem } = props; const onRemoveUser = useRelationshipAction({ ...route.params, action: 'unfriend', }); const onBlockUser = useRelationshipAction({ ...route.params, action: 'block', }); const onUnblockUser = useRelationshipAction({ ...route.params, action: 'unblock', }); return ( <> ); } type Props = { +navigation: UserProfileBottomSheetNavigationProp<'UserRelationshipTooltipModal'>, +route: TooltipRoute<'UserRelationshipTooltipModal'>, ... }; function UserRelationshipTooltipButton(props: Props): React.Node { const { navigation, route } = props; const { goBackOnce } = navigation; const { tooltipButtonIcon } = route.params; const colors = useColors(); const icon = React.useMemo(() => { if (tooltipButtonIcon === 'pencil') { return ; } return ( ); }, [colors.modalBackgroundLabel, tooltipButtonIcon]); return {icon}; } const UserRelationshipTooltipModal: React.ComponentType< BaseTooltipProps<'UserRelationshipTooltipModal'>, > = createTooltip<'UserRelationshipTooltipModal'>( UserRelationshipTooltipButton, TooltipMenu, ); export default UserRelationshipTooltipModal; diff --git a/web/modals/threads/settings/thread-settings-relationship-button.react.js b/web/modals/threads/settings/thread-settings-relationship-button.react.js index 2e64360da..6fd18fd58 100644 --- a/web/modals/threads/settings/thread-settings-relationship-button.react.js +++ b/web/modals/threads/settings/thread-settings-relationship-button.react.js @@ -1,144 +1,137 @@ // @flow import { faUserMinus, faUserPlus, faUserShield, faUserSlash, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import invariant from 'invariant'; import * as React from 'react'; -import { - updateRelationships, - updateRelationshipsActionTypes, -} from 'lib/actions/relationship-actions.js'; -import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; +import { updateRelationshipsActionTypes } from 'lib/actions/relationship-actions.js'; +import { useUpdateRelationships } from 'lib/hooks/relationship-hooks.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { getRelationshipActionText, getRelationshipDispatchAction, } from 'lib/shared/relationship-utils.js'; import type { SetState } from 'lib/types/hook-types.js'; import { relationshipButtons, type RelationshipButton, } from 'lib/types/relationship-types.js'; import type { UserInfo } from 'lib/types/user-types.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import css from './thread-settings-relationship-tab.css'; import Button, { buttonThemes } from '../../../components/button.react.js'; import { useSelector } from '../../../redux/redux-utils.js'; const loadingStatusSelector = createLoadingStatusSelector( updateRelationshipsActionTypes, ); type ButtonProps = { +relationshipButton: RelationshipButton, +otherUserInfo: UserInfo, +setErrorMessage?: SetState, }; function ThreadSettingsRelationshipButton(props: ButtonProps): React.Node { const { relationshipButton, otherUserInfo, setErrorMessage } = props; const disabled = useSelector(loadingStatusSelector) === 'loading'; const { username } = otherUserInfo; invariant(username, 'Other username should be specified'); let color; if (relationshipButton === relationshipButtons.FRIEND) { color = buttonThemes.success; } else if (relationshipButton === relationshipButtons.UNFRIEND) { color = buttonThemes.danger; } else if (relationshipButton === relationshipButtons.BLOCK) { color = buttonThemes.danger; } else if (relationshipButton === relationshipButtons.UNBLOCK) { color = buttonThemes.success; } else if (relationshipButton === relationshipButtons.ACCEPT) { color = buttonThemes.success; } else if (relationshipButton === relationshipButtons.REJECT) { color = buttonThemes.danger; } else if (relationshipButton === relationshipButtons.WITHDRAW) { color = buttonThemes.danger; } const { text, action } = React.useMemo(() => { return { text: getRelationshipActionText(relationshipButton, username), action: getRelationshipDispatchAction(relationshipButton), }; }, [relationshipButton, username]); const icon = React.useMemo(() => { let buttonIcon = null; if (relationshipButton === relationshipButtons.FRIEND) { buttonIcon = faUserPlus; } else if (relationshipButton === relationshipButtons.UNFRIEND) { buttonIcon = faUserMinus; } else if (relationshipButton === relationshipButtons.BLOCK) { buttonIcon = faUserShield; } else if (relationshipButton === relationshipButtons.UNBLOCK) { buttonIcon = faUserShield; } else if (relationshipButton === relationshipButtons.ACCEPT) { buttonIcon = faUserPlus; } else if (relationshipButton === relationshipButtons.REJECT) { buttonIcon = faUserSlash; } else if (relationshipButton === relationshipButtons.WITHDRAW) { buttonIcon = faUserMinus; } if (buttonIcon) { return ( ); } return undefined; }, [relationshipButton]); const dispatchActionPromise = useDispatchActionPromise(); - const callUpdateRelationships = - useLegacyAshoatKeyserverCall(updateRelationships); + const updateRelationships = useUpdateRelationships(); const updateRelationshipsActionPromise = React.useCallback(async () => { try { setErrorMessage?.(''); - return await callUpdateRelationships({ - action, - userIDs: [otherUserInfo.id], - }); + return await updateRelationships(action, [otherUserInfo.id]); } catch (e) { setErrorMessage?.('Error updating relationship'); throw e; } - }, [action, callUpdateRelationships, otherUserInfo.id, setErrorMessage]); + }, [action, updateRelationships, otherUserInfo.id, setErrorMessage]); const onClick = React.useCallback(() => { void dispatchActionPromise( updateRelationshipsActionTypes, updateRelationshipsActionPromise(), ); }, [dispatchActionPromise, updateRelationshipsActionPromise]); return ( ); } export default ThreadSettingsRelationshipButton; diff --git a/web/settings/relationship/add-users-list-modal.react.js b/web/settings/relationship/add-users-list-modal.react.js index 4424c7e2a..9ff02fd90 100644 --- a/web/settings/relationship/add-users-list-modal.react.js +++ b/web/settings/relationship/add-users-list-modal.react.js @@ -1,173 +1,169 @@ // @flow import * as React from 'react'; -import { - updateRelationships, - updateRelationshipsActionTypes, -} from 'lib/actions/relationship-actions.js'; +import { updateRelationshipsActionTypes } from 'lib/actions/relationship-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; -import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; +import { useUpdateRelationships } from 'lib/hooks/relationship-hooks.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { UserRelationshipStatus, TraditionalRelationshipAction, } from 'lib/types/relationship-types.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useAddUsersListContext } from './add-users-list-provider.react.js'; import css from './add-users-list.css'; import AddUsersList from './add-users-list.react.js'; import { useUserRelationshipUserInfos } from './add-users-utils.js'; import type { ButtonColor } from '../../components/button.react.js'; import Button from '../../components/button.react.js'; import LoadingIndicator from '../../loading-indicator.react.js'; import SearchModal from '../../modals/search-modal.react.js'; import { useSelector } from '../../redux/redux-utils.js'; const loadingStatusSelector = createLoadingStatusSelector( updateRelationshipsActionTypes, ); type AddUsersListModalContentProps = { +searchText: string, +excludedStatuses: $ReadOnlySet, }; function AddUsersListModalContent( props: AddUsersListModalContentProps, ): React.Node { const { searchText, excludedStatuses } = props; const { mergedUserInfos, sortedUsersWithENSNames } = useUserRelationshipUserInfos({ searchText, excludedStatuses, }); const addUsersListModalContent = React.useMemo( () => ( 0} userInfos={mergedUserInfos} sortedUsersWithENSNames={sortedUsersWithENSNames} /> ), [mergedUserInfos, searchText.length, sortedUsersWithENSNames], ); return addUsersListModalContent; } type Props = { +name: string, +excludedStatuses: $ReadOnlySet, +confirmButtonContent: React.Node, +confirmButtonColor?: ButtonColor, +relationshipAction: TraditionalRelationshipAction, }; function AddUsersListModal(props: Props): React.Node { const { name, excludedStatuses, confirmButtonContent, confirmButtonColor, relationshipAction, } = props; const { popModal } = useModalContext(); const { pendingUsersToAdd, setErrorMessage } = useAddUsersListContext(); const addUsersListChildGenerator = React.useCallback( (searchText: string) => ( ), [excludedStatuses], ); - const callUpdateRelationships = - useLegacyAshoatKeyserverCall(updateRelationships); + const updateRelationships = useUpdateRelationships(); const dispatchActionPromise = useDispatchActionPromise(); const updateRelationshipsPromiseCreator = React.useCallback(async () => { try { setErrorMessage(''); - const result = await callUpdateRelationships({ - action: relationshipAction, - userIDs: Array.from(pendingUsersToAdd.keys()), - }); + const result = await updateRelationships( + relationshipAction, + Array.from(pendingUsersToAdd.keys()), + ); popModal(); return result; } catch (e) { setErrorMessage('unknown error'); throw e; } }, [ setErrorMessage, - callUpdateRelationships, + updateRelationships, relationshipAction, pendingUsersToAdd, popModal, ]); const confirmSelection = React.useCallback( () => dispatchActionPromise( updateRelationshipsActionTypes, updateRelationshipsPromiseCreator(), ), [dispatchActionPromise, updateRelationshipsPromiseCreator], ); const loadingStatus = useSelector(loadingStatusSelector); const primaryButton = React.useMemo(() => { let buttonContent = confirmButtonContent; if (loadingStatus === 'loading') { buttonContent = ( <>
{confirmButtonContent}
); } return ( ); }, [ confirmButtonColor, confirmButtonContent, confirmSelection, loadingStatus, pendingUsersToAdd.size, ]); return ( {addUsersListChildGenerator} ); } export default AddUsersListModal;