diff --git a/lib/hooks/relationship-prompt.js b/lib/hooks/relationship-prompt.js index 7e255c224..c0cfcd174 100644 --- a/lib/hooks/relationship-prompt.js +++ b/lib/hooks/relationship-prompt.js @@ -1,127 +1,127 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { updateRelationships as serverUpdateRelationships, updateRelationshipsActionTypes, } from '../actions/relationship-actions.js'; import { getSingleOtherUser } from '../shared/thread-utils.js'; import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import { - type RelationshipAction, relationshipActions, + type TraditionalRelationshipAction, } from '../types/relationship-types.js'; import type { UserInfo } from '../types/user-types.js'; import { useLegacyAshoatKeyserverCall } from '../utils/action-utils.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 RelationshipPromptData = { +otherUserInfo: ?UserInfo, +callbacks: RelationshipCallbacks, }; 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 = useRelationshipCallbacks( otherUserInfo?.id, onErrorCallback, ); return React.useMemo( () => ({ otherUserInfo, callbacks, }), [callbacks, otherUserInfo], ); } function useRelationshipCallbacks( otherUserID?: string, onErrorCallback?: () => void, ): RelationshipCallbacks { const callUpdateRelationships = useLegacyAshoatKeyserverCall( serverUpdateRelationships, ); const updateRelationship = React.useCallback( - async (action: RelationshipAction) => { + async (action: TraditionalRelationshipAction) => { try { invariant(otherUserID, 'Other user info id should be present'); return await callUpdateRelationships({ action, userIDs: [otherUserID], }); } catch (e) { onErrorCallback?.(); throw e; } }, [callUpdateRelationships, onErrorCallback, otherUserID], ); const dispatchActionPromise = useDispatchActionPromise(); const onButtonPress = React.useCallback( - (action: RelationshipAction) => { + (action: TraditionalRelationshipAction) => { void dispatchActionPromise( updateRelationshipsActionTypes, updateRelationship(action), ); }, [dispatchActionPromise, updateRelationship], ); const blockUser = React.useCallback( () => onButtonPress(relationshipActions.BLOCK), [onButtonPress], ); const unblockUser = React.useCallback( () => onButtonPress(relationshipActions.UNBLOCK), [onButtonPress], ); const friendUser = React.useCallback( () => onButtonPress(relationshipActions.FRIEND), [onButtonPress], ); const unfriendUser = React.useCallback( () => onButtonPress(relationshipActions.UNFRIEND), [onButtonPress], ); return React.useMemo( () => ({ blockUser, unblockUser, friendUser, unfriendUser, }), [blockUser, friendUser, unblockUser, unfriendUser], ); } export { useRelationshipPrompt, useRelationshipCallbacks }; diff --git a/lib/shared/relationship-utils.js b/lib/shared/relationship-utils.js index a10cc6687..56d518bf8 100644 --- a/lib/shared/relationship-utils.js +++ b/lib/shared/relationship-utils.js @@ -1,122 +1,122 @@ // @flow import invariant from 'invariant'; import { type RelationshipButton, type UserRelationshipStatus, userRelationshipStatus, relationshipButtons, relationshipActions, - type RelationshipAction, + type TraditionalRelationshipAction, } from '../types/relationship-types.js'; import type { UserInfo } from '../types/user-types.js'; function sortUserIDs(firstId: string, secondId: string): string[] { if (!isNaN(Number(firstId)) && !isNaN(Number(secondId))) { return [Number(firstId), Number(secondId)] .sort((a, b) => a - b) .map(num => num.toString()); } else if (!isNaN(Number(firstId))) { return [firstId, secondId]; } else if (!isNaN(Number(secondId))) { return [secondId, firstId]; } return [firstId, secondId].sort(); } function getAvailableRelationshipButtons( userInfo: UserInfo, ): RelationshipButton[] { const relationship = userInfo.relationshipStatus; if (relationship === userRelationshipStatus.FRIEND) { return [relationshipButtons.UNFRIEND, relationshipButtons.BLOCK]; } else if (relationship === userRelationshipStatus.BLOCKED_VIEWER) { return [relationshipButtons.BLOCK]; } else if ( relationship === userRelationshipStatus.BOTH_BLOCKED || relationship === userRelationshipStatus.BLOCKED_BY_VIEWER ) { return [relationshipButtons.UNBLOCK]; } else if (relationship === userRelationshipStatus.REQUEST_RECEIVED) { return [ relationshipButtons.ACCEPT, relationshipButtons.REJECT, relationshipButtons.BLOCK, ]; } else if (relationship === userRelationshipStatus.REQUEST_SENT) { return [relationshipButtons.WITHDRAW, relationshipButtons.BLOCK]; } else { return [relationshipButtons.FRIEND, relationshipButtons.BLOCK]; } } function relationshipBlockedInEitherDirection( relationshipStatus: ?UserRelationshipStatus, ): boolean { return ( relationshipStatus === userRelationshipStatus.BLOCKED_VIEWER || relationshipStatus === userRelationshipStatus.BLOCKED_BY_VIEWER || relationshipStatus === userRelationshipStatus.BOTH_BLOCKED ); } // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return function getRelationshipDispatchAction( relationshipButton: RelationshipButton, -): RelationshipAction { +): TraditionalRelationshipAction { if (relationshipButton === relationshipButtons.BLOCK) { return relationshipActions.BLOCK; } else if ( relationshipButton === relationshipButtons.FRIEND || relationshipButton === relationshipButtons.ACCEPT ) { return relationshipActions.FRIEND; } else if ( relationshipButton === relationshipButtons.UNFRIEND || relationshipButton === relationshipButtons.REJECT || relationshipButton === relationshipButtons.WITHDRAW ) { return relationshipActions.UNFRIEND; } else if (relationshipButton === relationshipButtons.UNBLOCK) { return relationshipActions.UNBLOCK; } invariant(false, 'relationshipButton conditions should be exhaustive'); } // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return function getRelationshipActionText( relationshipButton: RelationshipButton, username: string, ): string { switch (relationshipButton) { case relationshipButtons.BLOCK: return `Block ${username}`; case relationshipButtons.FRIEND: return `Add ${username} to friends`; case relationshipButtons.UNFRIEND: return `Unfriend ${username}`; case relationshipButtons.UNBLOCK: return `Unblock ${username}`; case relationshipButtons.ACCEPT: return `Accept friend request from ${username}`; case relationshipButtons.REJECT: return `Reject friend request from ${username}`; case relationshipButtons.WITHDRAW: return `Withdraw request to friend ${username}`; default: invariant(false, 'invalid relationshipButton value'); } } export { sortUserIDs, getAvailableRelationshipButtons, relationshipBlockedInEitherDirection, getRelationshipDispatchAction, getRelationshipActionText, }; diff --git a/lib/types/relationship-types.js b/lib/types/relationship-types.js index 93a60c075..755e3e39c 100644 --- a/lib/types/relationship-types.js +++ b/lib/types/relationship-types.js @@ -1,117 +1,117 @@ // @flow import type { TInterface, TRefinement } from 'tcomb'; import t from 'tcomb'; import type { AccountUserInfo } from './user-types.js'; import { values } from '../utils/objects.js'; import { tNumEnum, tShape, tString } from '../utils/validation-utils.js'; export const undirectedStatus = Object.freeze({ KNOW_OF: 0, FRIEND: 2, }); export type UndirectedStatus = $Values; export const directedStatus = Object.freeze({ PENDING_FRIEND: 1, BLOCKED: 3, }); export type DirectedStatus = $Values; export const userRelationshipStatus = Object.freeze({ REQUEST_SENT: 1, REQUEST_RECEIVED: 2, FRIEND: 3, BLOCKED_BY_VIEWER: 4, BLOCKED_VIEWER: 5, BOTH_BLOCKED: 6, }); export type UserRelationshipStatus = $Values; export const userRelationshipStatusValidator: TRefinement = tNumEnum( values(userRelationshipStatus), ); const traditionalRelationshipActions = Object.freeze({ FRIEND: 'friend', UNFRIEND: 'unfriend', BLOCK: 'block', UNBLOCK: 'unblock', }); const farcasterRelationshipActions = Object.freeze({ FARCASTER_MUTUAL: 'farcaster', }); export const relationshipActions = Object.freeze({ ...traditionalRelationshipActions, ...farcasterRelationshipActions, }); export type RelationshipAction = $Values; export const relationshipActionsList: $ReadOnlyArray = values(relationshipActions); export type TraditionalRelationshipAction = $Values< typeof traditionalRelationshipActions, >; export const relationshipButtons = Object.freeze({ FRIEND: 'friend', UNFRIEND: 'unfriend', BLOCK: 'block', UNBLOCK: 'unblock', ACCEPT: 'accept', WITHDRAW: 'withdraw', REJECT: 'reject', }); export type RelationshipButton = $Values; export type TraditionalRelationshipRequest = { - +action: RelationshipAction, + +action: TraditionalRelationshipAction, +userIDs: $ReadOnlyArray, }; export type FarcasterRelationshipRequest = { +action: 'farcaster', +userIDsToFID: { +[userID: string]: string }, }; export type RelationshipRequest = | TraditionalRelationshipRequest | FarcasterRelationshipRequest; const exactlyTwoDictEntriesValidator = t.refinement( t.dict(t.String, t.String), dict => Object.keys(dict).length === 2, ); export const updateFarcasterRelationshipInputValidator: TInterface = tShape({ action: tString('farcaster'), userIDsToFID: exactlyTwoDictEntriesValidator, }); type SharedRelationshipRow = { user1: string, user2: string, }; export type DirectedRelationshipRow = { ...SharedRelationshipRow, status: DirectedStatus, }; export type UndirectedRelationshipRow = { ...SharedRelationshipRow, status: UndirectedStatus, }; export type RelationshipErrors = Partial<{ invalid_user: string[], already_friends: string[], user_blocked: string[], }>; export type UserRelationships = { +friends: $ReadOnlyArray, +blocked: $ReadOnlyArray, }; diff --git a/native/chat/settings/thread-settings-edit-relationship.react.js b/native/chat/settings/thread-settings-edit-relationship.react.js index b2ed31b64..b6d9ef126 100644 --- a/native/chat/settings/thread-settings-edit-relationship.react.js +++ b/native/chat/settings/thread-settings-edit-relationship.react.js @@ -1,135 +1,135 @@ // @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 { useENSNames } from 'lib/hooks/ens-cache.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 RelationshipAction, + type TraditionalRelationshipAction, type RelationshipButton, } from 'lib/types/relationship-types.js'; import { useLegacyAshoatKeyserverCall } from 'lib/utils/action-utils.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 updateRelationship = React.useCallback( - async (action: RelationshipAction) => { + async (action: TraditionalRelationshipAction) => { try { return await callUpdateRelationships({ action, userIDs: [otherUserInfo.id], }); } catch (e) { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, [{ text: 'OK' }], { cancelable: true, }, ); throw e; } }, [callUpdateRelationships, 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 d4f1c8ad2..c27c3a364 100644 --- a/native/profile/relationship-list-item.react.js +++ b/native/profile/relationship-list-item.react.js @@ -1,363 +1,363 @@ // @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 { 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 RelationshipAction, + type TraditionalRelationshipAction, type RelationshipErrors, userRelationshipStatus, relationshipActions, type RelationshipRequest, } from 'lib/types/relationship-types.js'; import type { AccountUserInfo, GlobalAccountUserInfo, } from 'lib/types/user-types.js'; import { useLegacyAshoatKeyserverCall } from 'lib/utils/action-utils.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: RelationshipRequest, ) => 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: RelationshipAction) { + 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: RelationshipAction, + action: TraditionalRelationshipAction, ): Promise { try { return await this.props.updateRelationships({ action, userIDs: [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 overlayContext = React.useContext(OverlayContext); const keyboardState = React.useContext(KeyboardContext); const navigateToUserProfileBottomSheet = useNavigateToUserProfileBottomSheet(); return ( ); }); export default ConnectedRelationshipListItem; diff --git a/web/settings/relationship/add-users-list-modal.react.js b/web/settings/relationship/add-users-list-modal.react.js index a8e0e5487..05e02e98a 100644 --- a/web/settings/relationship/add-users-list-modal.react.js +++ b/web/settings/relationship/add-users-list-modal.react.js @@ -1,173 +1,173 @@ // @flow import * as React from 'react'; import { updateRelationships, updateRelationshipsActionTypes, } from 'lib/actions/relationship-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { UserRelationshipStatus, - RelationshipAction, + TraditionalRelationshipAction, } from 'lib/types/relationship-types.js'; import { useLegacyAshoatKeyserverCall } from 'lib/utils/action-utils.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: RelationshipAction, + +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 dispatchActionPromise = useDispatchActionPromise(); const updateRelationshipsPromiseCreator = React.useCallback(async () => { try { setErrorMessage(''); const result = await callUpdateRelationships({ action: relationshipAction, userIDs: Array.from(pendingUsersToAdd.keys()), }); popModal(); return result; } catch (e) { setErrorMessage('unknown error'); throw e; } }, [ setErrorMessage, callUpdateRelationships, 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;