diff --git a/native/account/fullscreen-siwe-panel.react.js b/native/account/fullscreen-siwe-panel.react.js index bf4f09702..deef997b3 100644 --- a/native/account/fullscreen-siwe-panel.react.js +++ b/native/account/fullscreen-siwe-panel.react.js @@ -1,202 +1,202 @@ // @flow import { useNavigation } from '@react-navigation/native'; import invariant from 'invariant'; import * as React from 'react'; import { ActivityIndicator, View } from 'react-native'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { useWalletLogIn } from 'lib/hooks/login-hooks.js'; import { type SIWEResult, SIWEMessageTypes } from 'lib/types/siwe-types.js'; import { ServerError, getMessageForException } from 'lib/utils/errors.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { useGetEthereumAccountFromSIWEResult } from './registration/ethereum-utils.js'; import { RegistrationContext } from './registration/registration-context.js'; import { enableNewRegistrationMode } from './registration/registration-types.js'; import { useLegacySIWEServerCall } from './siwe-hooks.js'; import SIWEPanel from './siwe-panel.react.js'; import { commRustModule } from '../native-modules.js'; import { AccountDoesNotExistRouteName, RegistrationRouteName, } from '../navigation/route-names.js'; -import { UnknownErrorAlertDetails } from '../utils/alert-messages.js'; +import { unknownErrorAlertDetails } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; type Props = { +goBackToPrompt: () => mixed, +closing: boolean, }; function FullscreenSIWEPanel(props: Props): React.Node { const [loading, setLoading] = React.useState(true); const activity = loading ? : null; const activityContainer = React.useMemo( () => ({ flex: 1, }), [], ); const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { setSkipEthereumLoginOnce, register: registrationServerCall } = registrationContext; const getEthereumAccountFromSIWEResult = useGetEthereumAccountFromSIWEResult(); const { navigate } = useNavigation(); const { goBackToPrompt } = props; const onAccountDoesNotExist = React.useCallback( async (result: SIWEResult) => { await getEthereumAccountFromSIWEResult(result); setSkipEthereumLoginOnce(true); goBackToPrompt(); navigate<'Registration'>(RegistrationRouteName, { screen: AccountDoesNotExistRouteName, }); }, [ getEthereumAccountFromSIWEResult, navigate, goBackToPrompt, setSkipEthereumLoginOnce, ], ); const onNonceExpired = React.useCallback( (registrationOrLogin: 'registration' | 'login') => { Alert.alert( registrationOrLogin === 'registration' ? 'Registration attempt timed out' : 'Login attempt timed out', 'Please try again', [{ text: 'OK', onPress: goBackToPrompt }], { cancelable: false }, ); }, [goBackToPrompt], ); const legacySiweServerCall = useLegacySIWEServerCall(); const walletLogIn = useWalletLogIn(); const successRef = React.useRef(false); const dispatch = useDispatch(); const onSuccess = React.useCallback( async (result: SIWEResult) => { successRef.current = true; if (usingCommServicesAccessToken) { try { const findUserIDResponseString = await commRustModule.findUserIDForWalletAddress(result.address); const findUserIDResponse = JSON.parse(findUserIDResponseString); if (findUserIDResponse.userID || findUserIDResponse.isReserved) { try { await walletLogIn( result.address, result.message, result.signature, ); } catch (e) { const messageForException = getMessageForException(e); if (messageForException === 'nonce expired') { onNonceExpired('login'); } else { throw e; } } } else if (enableNewRegistrationMode) { await onAccountDoesNotExist(result); } else { try { await registrationServerCall({ farcasterID: null, accountSelection: { accountType: 'ethereum', ...result, avatarURI: null, }, avatarData: null, clearCachedSelections: () => {}, onNonceExpired: () => onNonceExpired('registration'), }); } catch { // We swallow exceptions here because registrationServerCall // already handles showing Alerts, and we don't want to show two } } } catch (e) { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, [{ text: 'OK', onPress: goBackToPrompt }], { cancelable: false }, ); } } else { try { await legacySiweServerCall({ ...result, doNotRegister: enableNewRegistrationMode, }); } catch (e) { if ( e instanceof ServerError && e.message === 'account_does_not_exist' ) { await onAccountDoesNotExist(result); return; } Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, [{ text: 'OK', onPress: goBackToPrompt }], { cancelable: false }, ); return; } dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); } }, [ walletLogIn, registrationServerCall, goBackToPrompt, dispatch, legacySiweServerCall, onAccountDoesNotExist, onNonceExpired, ], ); const ifBeforeSuccessGoBackToPrompt = React.useCallback(() => { if (!successRef.current) { goBackToPrompt(); } }, [goBackToPrompt]); const { closing } = props; return ( <> {activity} ); } export default FullscreenSIWEPanel; diff --git a/native/account/legacy-register-panel.react.js b/native/account/legacy-register-panel.react.js index 598fab641..fec6d14ba 100644 --- a/native/account/legacy-register-panel.react.js +++ b/native/account/legacy-register-panel.react.js @@ -1,521 +1,521 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, View, StyleSheet, Platform, Keyboard, Linking, } from 'react-native'; import Animated from 'react-native-reanimated'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { legacyKeyserverRegisterActionTypes, legacyKeyserverRegister, getOlmSessionInitializationDataActionTypes, } from 'lib/actions/user-actions.js'; import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; import { createLoadingStatusSelector, combineLoadingStatuses, } from 'lib/selectors/loading-selectors.js'; import { validUsernameRegex } from 'lib/shared/account-utils.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import type { LegacyRegisterInfo, LegacyLogInExtraInfo, LegacyRegisterResult, LegacyLogInStartingPayload, } from 'lib/types/account-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { Dispatch } from 'lib/types/redux-types.js'; import { useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { TextInput } from './modal-components.react.js'; import { setNativeCredentials } from './native-credentials.js'; import { PanelButton, Panel } from './panel-components.react.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { nativeLegacyLogInExtraInfoSelector } from '../selectors/account-selectors.js'; import type { KeyPressEvent } from '../types/react-native.js'; import { - AppOutOfDateAlertDetails, - UsernameReservedAlertDetails, - UsernameTakenAlertDetails, - UnknownErrorAlertDetails, + appOutOfDateAlertDetails, + usernameReservedAlertDetails, + usernameTakenAlertDetails, + unknownErrorAlertDetails, } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; import { type StateContainer } from '../utils/state-container.js'; type WritableLegacyRegisterState = { usernameInputText: string, passwordInputText: string, confirmPasswordInputText: string, }; export type LegacyRegisterState = $ReadOnly; type BaseProps = { +setActiveAlert: (activeAlert: boolean) => void, +opacityValue: Animated.Node, +legacyRegisterState: StateContainer, }; type Props = { ...BaseProps, +loadingStatus: LoadingStatus, +legacyLogInExtraInfo: () => Promise, +dispatch: Dispatch, +dispatchActionPromise: DispatchActionPromise, +legacyRegister: ( registerInfo: LegacyRegisterInfo, ) => Promise, +getInitialNotificationsEncryptedMessage: () => Promise, }; type State = { +confirmPasswordFocused: boolean, }; class LegacyRegisterPanel extends React.PureComponent { state: State = { confirmPasswordFocused: false, }; usernameInput: ?TextInput; passwordInput: ?TextInput; confirmPasswordInput: ?TextInput; passwordBeingAutoFilled = false; render(): React.Node { let confirmPasswordTextInputExtraProps; if ( Platform.OS !== 'ios' || this.state.confirmPasswordFocused || this.props.legacyRegisterState.state.confirmPasswordInputText.length > 0 ) { confirmPasswordTextInputExtraProps = { secureTextEntry: true, textContentType: 'password', }; } let onPasswordKeyPress; if (Platform.OS === 'ios') { onPasswordKeyPress = this.onPasswordKeyPress; } const privatePolicyNotice = ( By signing up, you agree to our{' '} Terms {' & '} Privacy Policy . ); return ( {privatePolicyNotice} ); } usernameInputRef = (usernameInput: ?TextInput) => { this.usernameInput = usernameInput; }; passwordInputRef = (passwordInput: ?TextInput) => { this.passwordInput = passwordInput; }; confirmPasswordInputRef = (confirmPasswordInput: ?TextInput) => { this.confirmPasswordInput = confirmPasswordInput; }; focusUsernameInput = () => { invariant(this.usernameInput, 'ref should be set'); this.usernameInput.focus(); }; focusPasswordInput = () => { invariant(this.passwordInput, 'ref should be set'); this.passwordInput.focus(); }; focusConfirmPasswordInput = () => { invariant(this.confirmPasswordInput, 'ref should be set'); this.confirmPasswordInput.focus(); }; onTermsOfUsePressed = () => { void Linking.openURL('https://comm.app/terms'); }; onPrivacyPolicyPressed = () => { void Linking.openURL('https://comm.app/privacy'); }; onChangeUsernameInputText = (text: string) => { this.props.legacyRegisterState.setState({ usernameInputText: text }); }; onChangePasswordInputText = (text: string) => { const stateUpdate: Partial = {}; stateUpdate.passwordInputText = text; if (this.passwordBeingAutoFilled) { this.passwordBeingAutoFilled = false; stateUpdate.confirmPasswordInputText = text; } this.props.legacyRegisterState.setState(stateUpdate); }; onPasswordKeyPress = (event: KeyPressEvent) => { const { key } = event.nativeEvent; if ( key.length > 1 && key !== 'Backspace' && key !== 'Enter' && this.props.legacyRegisterState.state.confirmPasswordInputText.length === 0 ) { this.passwordBeingAutoFilled = true; } }; onChangeConfirmPasswordInputText = (text: string) => { this.props.legacyRegisterState.setState({ confirmPasswordInputText: text }); }; onConfirmPasswordFocus = () => { this.setState({ confirmPasswordFocused: true }); }; onSubmit = async () => { this.props.setActiveAlert(true); if (this.props.legacyRegisterState.state.passwordInputText === '') { Alert.alert( 'Empty password', 'Password cannot be empty', [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }], { cancelable: false }, ); } else if ( this.props.legacyRegisterState.state.passwordInputText !== this.props.legacyRegisterState.state.confirmPasswordInputText ) { Alert.alert( 'Passwords don’t match', 'Password fields must contain the same password', [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }], { cancelable: false }, ); } else if ( this.props.legacyRegisterState.state.usernameInputText.search( validUsernameRegex, ) === -1 ) { Alert.alert( 'Invalid username', 'Usernames must be at least six characters long, start with either a ' + 'letter or a number, and may contain only letters, numbers, or the ' + 'characters “-” and “_”', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else { Keyboard.dismiss(); const extraInfo = await this.props.legacyLogInExtraInfo(); const initialNotificationsEncryptedMessage = await this.props.getInitialNotificationsEncryptedMessage(); void this.props.dispatchActionPromise( legacyKeyserverRegisterActionTypes, this.legacyRegisterAction({ ...extraInfo, initialNotificationsEncryptedMessage, }), undefined, ({ calendarQuery: extraInfo.calendarQuery, }: LegacyLogInStartingPayload), ); } }; onPasswordAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.legacyRegisterState.setState( { passwordInputText: '', confirmPasswordInputText: '', }, () => { invariant(this.passwordInput, 'ref should exist'); this.passwordInput.focus(); }, ); }; onUsernameAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.legacyRegisterState.setState( { usernameInputText: '', }, () => { invariant(this.usernameInput, 'ref should exist'); this.usernameInput.focus(); }, ); }; async legacyRegisterAction( extraInfo: LegacyLogInExtraInfo, ): Promise { try { const result = await this.props.legacyRegister({ ...extraInfo, username: this.props.legacyRegisterState.state.usernameInputText, password: this.props.legacyRegisterState.state.passwordInputText, }); this.props.setActiveAlert(false); this.props.dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); await setNativeCredentials({ username: result.currentUserInfo.username, password: this.props.legacyRegisterState.state.passwordInputText, }); return result; } catch (e) { if (e.message === 'username_reserved') { Alert.alert( - UsernameReservedAlertDetails.title, - UsernameReservedAlertDetails.message, + usernameReservedAlertDetails.title, + usernameReservedAlertDetails.message, [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else if (e.message === 'username_taken') { Alert.alert( - UsernameTakenAlertDetails.title, - UsernameTakenAlertDetails.message, + usernameTakenAlertDetails.title, + usernameTakenAlertDetails.message, [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else if (e.message === 'client_version_unsupported') { Alert.alert( - AppOutOfDateAlertDetails.title, - AppOutOfDateAlertDetails.message, + appOutOfDateAlertDetails.title, + appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } } onUnknownErrorAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.legacyRegisterState.setState( { usernameInputText: '', passwordInputText: '', confirmPasswordInputText: '', }, () => { invariant(this.usernameInput, 'ref should exist'); this.usernameInput.focus(); }, ); }; onAppOutOfDateAlertAcknowledged = () => { this.props.setActiveAlert(false); }; } const styles = StyleSheet.create({ container: { zIndex: 2, }, footer: { alignItems: 'stretch', flexDirection: 'row', flexShrink: 1, justifyContent: 'space-between', paddingLeft: 24, }, hyperlinkText: { color: '#036AFF', fontWeight: 'bold', }, icon: { bottom: 10, left: 4, position: 'absolute', }, input: { paddingLeft: 35, }, notice: { alignSelf: 'center', display: 'flex', flexShrink: 1, maxWidth: 190, paddingBottom: 18, paddingRight: 8, paddingTop: 12, }, noticeText: { color: '#444', fontSize: 13, lineHeight: 20, textAlign: 'center', }, row: { marginHorizontal: 24, }, }); const registerLoadingStatusSelector = createLoadingStatusSelector( legacyKeyserverRegisterActionTypes, ); const olmSessionInitializationDataLoadingStatusSelector = createLoadingStatusSelector(getOlmSessionInitializationDataActionTypes); const ConnectedLegacyRegisterPanel: React.ComponentType = React.memo(function ConnectedLegacyRegisterPanel( props: BaseProps, ) { const registerLoadingStatus = useSelector(registerLoadingStatusSelector); const olmSessionInitializationDataLoadingStatus = useSelector( olmSessionInitializationDataLoadingStatusSelector, ); const loadingStatus = combineLoadingStatuses( registerLoadingStatus, olmSessionInitializationDataLoadingStatus, ); const legacyLogInExtraInfo = useSelector( nativeLegacyLogInExtraInfoSelector, ); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const callLegacyRegister = useLegacyAshoatKeyserverCall( legacyKeyserverRegister, ); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(authoritativeKeyserverID); return ( ); }); export default ConnectedLegacyRegisterPanel; diff --git a/native/account/log-in-panel.react.js b/native/account/log-in-panel.react.js index 030cc8483..85ac84f7e 100644 --- a/native/account/log-in-panel.react.js +++ b/native/account/log-in-panel.react.js @@ -1,481 +1,481 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, StyleSheet, Keyboard, Platform } from 'react-native'; import Animated from 'react-native-reanimated'; import { legacyLogInActionTypes, useLegacyLogIn, getOlmSessionInitializationDataActionTypes, } from 'lib/actions/user-actions.js'; import { usePasswordLogIn } from 'lib/hooks/login-hooks.js'; import { createLoadingStatusSelector, combineLoadingStatuses, } from 'lib/selectors/loading-selectors.js'; import { validEmailRegex, oldValidUsernameRegex, } from 'lib/shared/account-utils.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import { type LegacyLogInInfo, type LegacyLogInExtraInfo, type LegacyLogInResult, type LegacyLogInStartingPayload, logInActionSources, } from 'lib/types/account-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { TextInput } from './modal-components.react.js'; import { fetchNativeCredentials, setNativeCredentials, } from './native-credentials.js'; import { PanelButton, Panel } from './panel-components.react.js'; import PasswordInput from './password-input.react.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { nativeLegacyLogInExtraInfoSelector } from '../selectors/account-selectors.js'; import type { KeyPressEvent } from '../types/react-native.js'; import { - AppOutOfDateAlertDetails, - UnknownErrorAlertDetails, - UserNotFoundAlertDetails, + appOutOfDateAlertDetails, + unknownErrorAlertDetails, + userNotFoundAlertDetails, } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; import type { StateContainer } from '../utils/state-container.js'; export type LogInState = { +usernameInputText: ?string, +passwordInputText: ?string, }; type BaseProps = { +setActiveAlert: (activeAlert: boolean) => void, +opacityValue: Animated.Node, +logInState: StateContainer, }; type Props = { ...BaseProps, +loadingStatus: LoadingStatus, +legacyLogInExtraInfo: () => Promise, +dispatchActionPromise: DispatchActionPromise, +legacyLogIn: (logInInfo: LegacyLogInInfo) => Promise, +identityPasswordLogIn: (username: string, password: string) => Promise, +getInitialNotificationsEncryptedMessage: () => Promise, }; type State = { +logInPending: boolean, }; class LogInPanel extends React.PureComponent { usernameInput: ?TextInput; passwordInput: ?PasswordInput; state: State = { logInPending: false }; componentDidMount() { void this.attemptToFetchCredentials(); } get usernameInputText(): string { return this.props.logInState.state.usernameInputText || ''; } get passwordInputText(): string { return this.props.logInState.state.passwordInputText || ''; } async attemptToFetchCredentials() { if ( this.props.logInState.state.usernameInputText !== null && this.props.logInState.state.usernameInputText !== undefined ) { return; } const credentials = await fetchNativeCredentials(); if (!credentials) { return; } if ( this.props.logInState.state.usernameInputText !== null && this.props.logInState.state.usernameInputText !== undefined ) { return; } this.props.logInState.setState({ usernameInputText: credentials.username, passwordInputText: credentials.password, }); } render(): React.Node { return ( ); } getLoadingStatus(): LoadingStatus { if (this.props.loadingStatus === 'loading') { return 'loading'; } if (this.state.logInPending) { return 'loading'; } return 'inactive'; } usernameInputRef: (usernameInput: ?TextInput) => void = usernameInput => { this.usernameInput = usernameInput; if (Platform.OS === 'ios' && usernameInput) { setTimeout(() => usernameInput.focus()); } }; focusUsernameInput: () => void = () => { invariant(this.usernameInput, 'ref should be set'); this.usernameInput.focus(); }; passwordInputRef: (passwordInput: ?PasswordInput) => void = passwordInput => { this.passwordInput = passwordInput; }; focusPasswordInput: () => void = () => { invariant(this.passwordInput, 'ref should be set'); this.passwordInput.focus(); }; onChangeUsernameInputText: (text: string) => void = text => { this.props.logInState.setState({ usernameInputText: text.trim() }); }; onUsernameKeyPress: (event: KeyPressEvent) => void = event => { const { key } = event.nativeEvent; if ( key.length > 1 && key !== 'Backspace' && key !== 'Enter' && this.passwordInputText.length === 0 ) { this.focusPasswordInput(); } }; onChangePasswordInputText: (text: string) => void = text => { this.props.logInState.setState({ passwordInputText: text }); }; onSubmit: () => Promise = async () => { this.props.setActiveAlert(true); if (this.usernameInputText.search(validEmailRegex) > -1) { Alert.alert( 'Can’t log in with email', 'You need to log in with your username now', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); return; } else if (this.usernameInputText.search(oldValidUsernameRegex) === -1) { Alert.alert( 'Invalid username', 'Alphanumeric usernames only', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); return; } else if (this.passwordInputText === '') { Alert.alert( 'Empty password', 'Password cannot be empty', [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }], { cancelable: false }, ); return; } Keyboard.dismiss(); if (usingCommServicesAccessToken) { await this.identityPasswordLogIn(); return; } const extraInfo = await this.props.legacyLogInExtraInfo(); const initialNotificationsEncryptedMessage = await this.props.getInitialNotificationsEncryptedMessage(); void this.props.dispatchActionPromise( legacyLogInActionTypes, this.legacyLogInAction({ ...extraInfo, initialNotificationsEncryptedMessage, }), undefined, ({ calendarQuery: extraInfo.calendarQuery }: LegacyLogInStartingPayload), ); }; async legacyLogInAction( extraInfo: LegacyLogInExtraInfo, ): Promise { try { const result = await this.props.legacyLogIn({ ...extraInfo, username: this.usernameInputText, password: this.passwordInputText, authActionSource: logInActionSources.logInFromNativeForm, }); this.props.setActiveAlert(false); await setNativeCredentials({ username: result.currentUserInfo.username, password: this.passwordInputText, }); return result; } catch (e) { if (e.message === 'invalid_credentials') { Alert.alert( - UserNotFoundAlertDetails.title, - UserNotFoundAlertDetails.message, + userNotFoundAlertDetails.title, + userNotFoundAlertDetails.message, [{ text: 'OK', onPress: this.onUnsuccessfulLoginAlertAckowledged }], { cancelable: false }, ); } else if (e.message === 'client_version_unsupported') { Alert.alert( - AppOutOfDateAlertDetails.title, - AppOutOfDateAlertDetails.message, + appOutOfDateAlertDetails.title, + appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } } async identityPasswordLogIn(): Promise { if (this.state.logInPending) { return; } this.setState({ logInPending: true }); try { await this.props.identityPasswordLogIn( this.usernameInputText, this.passwordInputText, ); this.props.setActiveAlert(false); await setNativeCredentials({ username: this.usernameInputText, password: this.passwordInputText, }); } catch (e) { const messageForException = getMessageForException(e); if ( messageForException === 'user not found' || messageForException === 'login failed' ) { Alert.alert( - UserNotFoundAlertDetails.title, - UserNotFoundAlertDetails.message, + userNotFoundAlertDetails.title, + userNotFoundAlertDetails.message, [{ text: 'OK', onPress: this.onUnsuccessfulLoginAlertAckowledged }], { cancelable: false }, ); } else if (messageForException === 'Unsupported version') { Alert.alert( - AppOutOfDateAlertDetails.title, - AppOutOfDateAlertDetails.message, + appOutOfDateAlertDetails.title, + appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } finally { this.setState({ logInPending: false }); } } onUnsuccessfulLoginAlertAckowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { usernameInputText: '', passwordInputText: '', }, this.focusUsernameInput, ); }; onUsernameAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { usernameInputText: '', }, this.focusUsernameInput, ); }; onPasswordAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { passwordInputText: '', }, this.focusPasswordInput, ); }; onUnknownErrorAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { usernameInputText: '', passwordInputText: '', }, this.focusUsernameInput, ); }; onAppOutOfDateAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); }; } export type InnerLogInPanel = LogInPanel; const styles = StyleSheet.create({ footer: { flexDirection: 'row', justifyContent: 'flex-end', }, icon: { bottom: 10, left: 4, position: 'absolute', }, input: { paddingLeft: 35, }, row: { marginHorizontal: 24, }, }); const logInLoadingStatusSelector = createLoadingStatusSelector( legacyLogInActionTypes, ); const olmSessionInitializationDataLoadingStatusSelector = createLoadingStatusSelector(getOlmSessionInitializationDataActionTypes); const ConnectedLogInPanel: React.ComponentType = React.memo(function ConnectedLogInPanel(props: BaseProps) { const logInLoadingStatus = useSelector(logInLoadingStatusSelector); const olmSessionInitializationDataLoadingStatus = useSelector( olmSessionInitializationDataLoadingStatusSelector, ); const loadingStatus = combineLoadingStatuses( logInLoadingStatus, olmSessionInitializationDataLoadingStatus, ); const legacyLogInExtraInfo = useSelector( nativeLegacyLogInExtraInfoSelector, ); const dispatchActionPromise = useDispatchActionPromise(); const callLegacyLogIn = useLegacyLogIn(); const callIdentityPasswordLogIn = usePasswordLogIn(); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(authoritativeKeyserverID); return ( ); }); export default ConnectedLogInPanel; diff --git a/native/account/registration/existing-ethereum-account.react.js b/native/account/registration/existing-ethereum-account.react.js index 1a8f5e5b5..8e6dd6199 100644 --- a/native/account/registration/existing-ethereum-account.react.js +++ b/native/account/registration/existing-ethereum-account.react.js @@ -1,199 +1,199 @@ // @flow import type { StackNavigationEventMap, StackNavigationState, StackOptions, } from '@react-navigation/core'; import invariant from 'invariant'; import * as React from 'react'; import { Text, View } from 'react-native'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { useENSName } from 'lib/hooks/ens-cache.js'; import { useWalletLogIn } from 'lib/hooks/login-hooks.js'; import type { SIWEResult } from 'lib/types/siwe-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import RegistrationButtonContainer from './registration-button-container.react.js'; import RegistrationButton from './registration-button.react.js'; import RegistrationContainer from './registration-container.react.js'; import RegistrationContentContainer from './registration-content-container.react.js'; import { RegistrationContext } from './registration-context.js'; import type { RegistrationNavigationProp } from './registration-navigator.react.js'; import type { RootNavigationProp } from '../../navigation/root-navigator.react.js'; import type { NavigationRoute, ScreenParamList, } from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; -import { UnknownErrorAlertDetails } from '../../utils/alert-messages.js'; +import { unknownErrorAlertDetails } from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; import { useLegacySIWEServerCall } from '../siwe-hooks.js'; export type ExistingEthereumAccountParams = SIWEResult; type Props = { +navigation: RegistrationNavigationProp<'ExistingEthereumAccount'>, +route: NavigationRoute<'ExistingEthereumAccount'>, }; function ExistingEthereumAccount(props: Props): React.Node { const legacySiweServerCall = useLegacySIWEServerCall(); const walletLogIn = useWalletLogIn(); const [logInPending, setLogInPending] = React.useState(false); const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { setCachedSelections } = registrationContext; const { params } = props.route; const dispatch = useDispatch(); const { navigation } = props; const goBackToHome = navigation.getParent< ScreenParamList, 'Registration', StackNavigationState, StackOptions, StackNavigationEventMap, RootNavigationProp<'Registration'>, >()?.goBack; const onProceedToLogIn = React.useCallback(async () => { if (logInPending) { return; } setLogInPending(true); try { if (usingCommServicesAccessToken) { await walletLogIn(params.address, params.message, params.signature); } else { await legacySiweServerCall({ ...params, doNotRegister: true }); dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); } } catch (e) { const messageForException = getMessageForException(e); if (messageForException === 'nonce expired') { setCachedSelections(oldUserSelections => ({ ...oldUserSelections, ethereumAccount: undefined, })); Alert.alert( 'Login attempt timed out', 'Try logging in from the main SIWE button on the home screen', [{ text: 'OK', onPress: goBackToHome }], { cancelable: false, }, ); } else { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, [{ text: 'OK' }], { cancelable: false, }, ); } throw e; } finally { setLogInPending(false); } }, [ logInPending, legacySiweServerCall, walletLogIn, params, dispatch, goBackToHome, setCachedSelections, ]); const { address } = params; const walletIdentifier = useENSName(address); const walletIdentifierTitle = walletIdentifier === address ? 'Ethereum wallet' : 'ENS name'; const { goBack } = navigation; const styles = useStyles(unboundStyles); return ( Account already exists for wallet You can proceed to log in with this wallet, or go back and use a different wallet. {walletIdentifierTitle} {walletIdentifier} ); } const unboundStyles = { header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, body: { fontFamily: 'Arial', fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', paddingBottom: 40, }, walletTile: { backgroundColor: 'panelForeground', borderRadius: 8, padding: 24, alignItems: 'center', }, walletIdentifierTitleText: { fontSize: 17, color: 'panelForegroundLabel', textAlign: 'center', }, walletIdentifier: { backgroundColor: 'panelSecondaryForeground', paddingVertical: 8, paddingHorizontal: 24, borderRadius: 56, marginTop: 8, alignItems: 'center', }, walletIdentifierText: { fontSize: 15, color: 'panelForegroundLabel', }, }; export default ExistingEthereumAccount; diff --git a/native/account/registration/registration-server-call.js b/native/account/registration/registration-server-call.js index e9b574052..147e0b703 100644 --- a/native/account/registration/registration-server-call.js +++ b/native/account/registration/registration-server-call.js @@ -1,483 +1,483 @@ // @flow import * as React from 'react'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { setSyncedMetadataEntryActionType } from 'lib/actions/synced-metadata-actions.js'; import { legacyKeyserverRegisterActionTypes, legacyKeyserverRegister, useIdentityPasswordRegister, identityRegisterActionTypes, deleteAccountActionTypes, useDeleteDiscardedIdentityAccount, } from 'lib/actions/user-actions.js'; import { useKeyserverAuthWithRetry } from 'lib/keyserver-conn/keyserver-auth.js'; import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; import { isLoggedInToKeyserver } from 'lib/selectors/user-selectors.js'; import { type LegacyLogInStartingPayload, logInActionSources, } from 'lib/types/account-types.js'; import { syncedMetadataNames } from 'lib/types/synced-metadata-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { setURLPrefix } from 'lib/utils/url-utils.js'; import { waitUntilDatabaseDeleted } from 'lib/utils/wait-until-db-deleted.js'; import type { RegistrationServerCallInput, UsernameAccountSelection, AvatarData, } from './registration-types.js'; import { authoritativeKeyserverID } from '../../authoritative-keyserver.js'; import { useNativeSetUserAvatar, useUploadSelectedMedia, } from '../../avatars/avatar-hooks.js'; import { commCoreModule } from '../../native-modules.js'; import { useSelector } from '../../redux/redux-utils.js'; import { nativeLegacyLogInExtraInfoSelector } from '../../selectors/account-selectors.js'; import { - AppOutOfDateAlertDetails, - UsernameReservedAlertDetails, - UsernameTakenAlertDetails, - UnknownErrorAlertDetails, + appOutOfDateAlertDetails, + usernameReservedAlertDetails, + usernameTakenAlertDetails, + unknownErrorAlertDetails, } from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; import { defaultURLPrefix } from '../../utils/url-utils.js'; import { setNativeCredentials } from '../native-credentials.js'; import { useLegacySIWEServerCall, useIdentityWalletRegisterCall, } from '../siwe-hooks.js'; // We can't just do everything in one async callback, since the server calls // would get bound to Redux state from before the registration. The registration // flow has multiple steps where critical Redux state is changed, where // subsequent steps depend on accessing the updated Redux state. // To address this, we break the registration process up into multiple steps. // When each step completes we update the currentStep state, and we have Redux // selectors that trigger useEffects for subsequent steps when relevant data // starts to appear in Redux. type CurrentStep = | { +step: 'inactive' } | { +step: 'identity_registration_dispatched', +clearCachedSelections: () => void, +avatarData: ?AvatarData, +credentialsToSave: ?{ +username: string, +password: string }, +resolve: () => void, +reject: Error => void, } | { +step: 'authoritative_keyserver_registration_dispatched', +clearCachedSelections: () => void, +avatarData: ?AvatarData, +credentialsToSave: ?{ +username: string, +password: string }, +resolve: () => void, +reject: Error => void, }; const inactiveStep = { step: 'inactive' }; function useRegistrationServerCall(): RegistrationServerCallInput => Promise { const [currentStep, setCurrentStep] = React.useState(inactiveStep); // STEP 1: ACCOUNT REGISTRATION const legacyLogInExtraInfo = useSelector(nativeLegacyLogInExtraInfoSelector); const dispatchActionPromise = useDispatchActionPromise(); const callLegacyKeyserverRegister = useLegacyAshoatKeyserverCall( legacyKeyserverRegister, ); const callIdentityPasswordRegister = useIdentityPasswordRegister(); const identityRegisterUsernameAccount = React.useCallback( async ( accountSelection: UsernameAccountSelection, farcasterID: ?string, ) => { const identityRegisterPromise = (async () => { try { return await callIdentityPasswordRegister( accountSelection.username, accountSelection.password, farcasterID, ); } catch (e) { if (e.message === 'username reserved') { Alert.alert( - UsernameReservedAlertDetails.title, - UsernameReservedAlertDetails.message, + usernameReservedAlertDetails.title, + usernameReservedAlertDetails.message, ); } else if (e.message === 'username already exists') { Alert.alert( - UsernameTakenAlertDetails.title, - UsernameTakenAlertDetails.message, + usernameTakenAlertDetails.title, + usernameTakenAlertDetails.message, ); } else if (e.message === 'Unsupported version') { Alert.alert( - AppOutOfDateAlertDetails.title, - AppOutOfDateAlertDetails.message, + appOutOfDateAlertDetails.title, + appOutOfDateAlertDetails.message, ); } else { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, ); } throw e; } })(); void dispatchActionPromise( identityRegisterActionTypes, identityRegisterPromise, ); await identityRegisterPromise; }, [callIdentityPasswordRegister, dispatchActionPromise], ); const legacyKeyserverRegisterUsernameAccount = React.useCallback( async ( accountSelection: UsernameAccountSelection, keyserverURL: string, ) => { const extraInfo = await legacyLogInExtraInfo(); const legacyKeyserverRegisterPromise = (async () => { try { return await callLegacyKeyserverRegister( { ...extraInfo, username: accountSelection.username, password: accountSelection.password, }, { urlPrefixOverride: keyserverURL, }, ); } catch (e) { if (e.message === 'username_reserved') { Alert.alert( - UsernameReservedAlertDetails.title, - UsernameReservedAlertDetails.message, + usernameReservedAlertDetails.title, + usernameReservedAlertDetails.message, ); } else if (e.message === 'username_taken') { Alert.alert( - UsernameTakenAlertDetails.title, - UsernameTakenAlertDetails.message, + usernameTakenAlertDetails.title, + usernameTakenAlertDetails.message, ); } else if (e.message === 'client_version_unsupported') { Alert.alert( - AppOutOfDateAlertDetails.title, - AppOutOfDateAlertDetails.message, + appOutOfDateAlertDetails.title, + appOutOfDateAlertDetails.message, ); } else { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, ); } throw e; } })(); void dispatchActionPromise( legacyKeyserverRegisterActionTypes, legacyKeyserverRegisterPromise, undefined, ({ calendarQuery: extraInfo.calendarQuery, }: LegacyLogInStartingPayload), ); await legacyKeyserverRegisterPromise; }, [legacyLogInExtraInfo, callLegacyKeyserverRegister, dispatchActionPromise], ); const legacySiweServerCall = useLegacySIWEServerCall(); const identityWalletRegisterCall = useIdentityWalletRegisterCall(); const dispatch = useDispatch(); const returnedFunc = React.useCallback( (input: RegistrationServerCallInput) => new Promise( // eslint-disable-next-line no-async-promise-executor async (resolve, reject) => { try { if (currentStep.step !== 'inactive') { return; } const { accountSelection, avatarData, keyserverURL: passedKeyserverURL, farcasterID, siweBackupSecrets, clearCachedSelections, onNonceExpired, } = input; const keyserverURL = passedKeyserverURL ?? defaultURLPrefix; if ( accountSelection.accountType === 'username' && !usingCommServicesAccessToken ) { await legacyKeyserverRegisterUsernameAccount( accountSelection, keyserverURL, ); } else if (accountSelection.accountType === 'username') { await identityRegisterUsernameAccount( accountSelection, farcasterID, ); } else if (!usingCommServicesAccessToken) { try { await legacySiweServerCall(accountSelection, { urlPrefixOverride: keyserverURL, }); } catch (e) { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, ); throw e; } } else { try { await identityWalletRegisterCall({ address: accountSelection.address, message: accountSelection.message, signature: accountSelection.signature, fid: farcasterID, }); } catch (e) { const messageForException = getMessageForException(e); if (messageForException === 'nonce expired') { onNonceExpired(); } else { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, ); } throw e; } } if (passedKeyserverURL) { dispatch({ type: setURLPrefix, payload: passedKeyserverURL, }); } if (farcasterID) { dispatch({ type: setSyncedMetadataEntryActionType, payload: { name: syncedMetadataNames.CURRENT_USER_FID, data: farcasterID, }, }); } if (siweBackupSecrets) { await commCoreModule.setSIWEBackupSecrets(siweBackupSecrets); } const credentialsToSave = accountSelection.accountType === 'username' ? { username: accountSelection.username, password: accountSelection.password, } : null; if (usingCommServicesAccessToken) { setCurrentStep({ step: 'identity_registration_dispatched', avatarData, clearCachedSelections, credentialsToSave, resolve, reject, }); } else { setCurrentStep({ step: 'authoritative_keyserver_registration_dispatched', avatarData, clearCachedSelections, credentialsToSave, resolve, reject, }); } } catch (e) { reject(e); } }, ), [ currentStep, legacyKeyserverRegisterUsernameAccount, identityRegisterUsernameAccount, legacySiweServerCall, dispatch, identityWalletRegisterCall, ], ); // STEP 2: REGISTERING ON AUTHORITATIVE KEYSERVER const keyserverAuth = useKeyserverAuthWithRetry(authoritativeKeyserverID); const isRegisteredOnIdentity = useSelector( state => !!state.commServicesAccessToken && !!state.currentUserInfo && !state.currentUserInfo.anonymous, ); // We call deleteDiscardedIdentityAccount in order to reset state if identity // registration succeeds but authoritative keyserver auth fails const deleteDiscardedIdentityAccount = useDeleteDiscardedIdentityAccount(); const registeringOnAuthoritativeKeyserverRef = React.useRef(false); React.useEffect(() => { if ( !isRegisteredOnIdentity || currentStep.step !== 'identity_registration_dispatched' || registeringOnAuthoritativeKeyserverRef.current ) { return; } registeringOnAuthoritativeKeyserverRef.current = true; const { avatarData, clearCachedSelections, credentialsToSave, resolve, reject, } = currentStep; void (async () => { try { await keyserverAuth({ authActionSource: process.env.BROWSER ? logInActionSources.keyserverAuthFromWeb : logInActionSources.keyserverAuthFromNative, setInProgress: () => {}, hasBeenCancelled: () => false, doNotRegister: false, }); setCurrentStep({ step: 'authoritative_keyserver_registration_dispatched', avatarData, clearCachedSelections, credentialsToSave, resolve, reject, }); } catch (keyserverAuthException) { const discardIdentityAccountPromise = (async () => { try { const deletionResult = await deleteDiscardedIdentityAccount(); Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, ); return deletionResult; } catch (deleteException) { Alert.alert( 'Account created but login failed', 'We were able to create your account, but were unable to log ' + 'you in. Try going back to the login screen and logging in ' + 'with your new credentials.', ); throw deleteException; } })(); void dispatchActionPromise( deleteAccountActionTypes, discardIdentityAccountPromise, ); await waitUntilDatabaseDeleted(); reject(keyserverAuthException); setCurrentStep(inactiveStep); } finally { registeringOnAuthoritativeKeyserverRef.current = false; } })(); }, [ currentStep, isRegisteredOnIdentity, keyserverAuth, dispatchActionPromise, deleteDiscardedIdentityAccount, ]); // STEP 3: SETTING AVATAR const uploadSelectedMedia = useUploadSelectedMedia(); const nativeSetUserAvatar = useNativeSetUserAvatar(); const isLoggedInToAuthoritativeKeyserver = useSelector( isLoggedInToKeyserver(authoritativeKeyserverID), ); const avatarBeingSetRef = React.useRef(false); React.useEffect(() => { if ( !isLoggedInToAuthoritativeKeyserver || currentStep.step !== 'authoritative_keyserver_registration_dispatched' || avatarBeingSetRef.current ) { return; } avatarBeingSetRef.current = true; const { avatarData, resolve, clearCachedSelections, credentialsToSave } = currentStep; void (async () => { try { if (!avatarData) { return; } let updateUserAvatarRequest; if (!avatarData.needsUpload) { ({ updateUserAvatarRequest } = avatarData); } else { const { mediaSelection } = avatarData; updateUserAvatarRequest = await uploadSelectedMedia(mediaSelection); if (!updateUserAvatarRequest) { return; } } await nativeSetUserAvatar(updateUserAvatarRequest); } finally { dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); clearCachedSelections(); if (credentialsToSave) { void setNativeCredentials(credentialsToSave); } setCurrentStep(inactiveStep); avatarBeingSetRef.current = false; resolve(); } })(); }, [ currentStep, isLoggedInToAuthoritativeKeyserver, uploadSelectedMedia, nativeSetUserAvatar, dispatch, ]); return returnedFunc; } export { useRegistrationServerCall }; diff --git a/native/account/siwe-panel.react.js b/native/account/siwe-panel.react.js index 5ac01b906..e7146338b 100644 --- a/native/account/siwe-panel.react.js +++ b/native/account/siwe-panel.react.js @@ -1,298 +1,298 @@ // @flow import BottomSheet from '@gorhom/bottom-sheet'; import invariant from 'invariant'; import * as React from 'react'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import WebView from 'react-native-webview'; import { getSIWENonce, getSIWENonceActionTypes, legacySiweAuthActionTypes, } from 'lib/actions/siwe-actions.js'; import { identityGenerateNonceActionTypes, useIdentityGenerateNonce, } from 'lib/actions/user-actions.js'; import type { ServerCallSelectorParams } from 'lib/keyserver-conn/call-keyserver-endpoint-provider.react.js'; import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { SIWEWebViewMessage, SIWEResult, SIWEMessageType, } from 'lib/types/siwe-types.js'; import { getContentSigningKey } from 'lib/utils/crypto-utils.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { useKeyboardHeight } from '../keyboard/keyboard-hooks.js'; import { useSelector } from '../redux/redux-utils.js'; import type { BottomSheetRef } from '../types/bottom-sheet.js'; import type { WebViewMessageEvent } from '../types/web-view-types.js'; -import { UnknownErrorAlertDetails } from '../utils/alert-messages.js'; +import { unknownErrorAlertDetails } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; import { defaultLandingURLPrefix } from '../utils/url-utils.js'; const commSIWE = `${defaultLandingURLPrefix}/siwe`; const getSIWENonceLoadingStatusSelector = createLoadingStatusSelector( getSIWENonceActionTypes, ); const identityGenerateNonceLoadingStatusSelector = createLoadingStatusSelector( identityGenerateNonceActionTypes, ); const legacySiweAuthLoadingStatusSelector = createLoadingStatusSelector( legacySiweAuthActionTypes, ); type NonceInfo = { +nonce: string, +nonceTimestamp: number, }; type Props = { +onClosed: () => mixed, +onClosing: () => mixed, +onSuccessfulWalletSignature: SIWEResult => mixed, +siweMessageType: SIWEMessageType, +closing: boolean, +setLoading: boolean => mixed, +keyserverCallParamOverride?: Partial, }; function SIWEPanel(props: Props): React.Node { const dispatchActionPromise = useDispatchActionPromise(); const getSIWENonceCall = useLegacyAshoatKeyserverCall( getSIWENonce, props.keyserverCallParamOverride, ); const identityGenerateNonce = useIdentityGenerateNonce(); const legacyGetSIWENonceCallFailed = useSelector( state => getSIWENonceLoadingStatusSelector(state) === 'error', ); const identityGenerateNonceFailed = useSelector( state => identityGenerateNonceLoadingStatusSelector(state) === 'error', ); const { onClosing } = props; const { siweMessageType } = props; const legacySiweAuthCallLoading = useSelector( state => legacySiweAuthLoadingStatusSelector(state) === 'loading', ); const [nonceInfo, setNonceInfo] = React.useState(null); const [primaryIdentityPublicKey, setPrimaryIdentityPublicKey] = React.useState(null); // This is set if we either succeed or fail, at which point we expect // to be unmounted/remounted by our parent component prior to a retry const nonceNotNeededRef = React.useRef(false); React.useEffect(() => { if (nonceNotNeededRef.current) { return; } const generateNonce = async (nonceFunction: () => Promise) => { try { const response = await nonceFunction(); setNonceInfo({ nonce: response, nonceTimestamp: Date.now() }); } catch (e) { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, [ { text: 'OK', onPress: () => { nonceNotNeededRef.current = true; onClosing(); }, }, ], { cancelable: false }, ); throw e; } }; void (async () => { if (usingCommServicesAccessToken) { void dispatchActionPromise( identityGenerateNonceActionTypes, generateNonce(identityGenerateNonce), ); } else { void dispatchActionPromise( getSIWENonceActionTypes, generateNonce(getSIWENonceCall), ); } const ed25519 = await getContentSigningKey(); setPrimaryIdentityPublicKey(ed25519); })(); }, [ dispatchActionPromise, getSIWENonceCall, identityGenerateNonce, onClosing, ]); const [isLoading, setLoading] = React.useState(true); const [walletConnectModalHeight, setWalletConnectModalHeight] = React.useState(0); const insets = useSafeAreaInsets(); const keyboardHeight = useKeyboardHeight(); const bottomInset = insets.bottom; const snapPoints = React.useMemo(() => { if (isLoading) { return [1]; } else if (walletConnectModalHeight) { const baseHeight = bottomInset + walletConnectModalHeight + keyboardHeight; if (baseHeight < 400) { return [baseHeight - 10]; } else { return [baseHeight + 5]; } } else { const baseHeight = bottomInset + keyboardHeight; return [baseHeight + 435, baseHeight + 600]; } }, [isLoading, walletConnectModalHeight, bottomInset, keyboardHeight]); const bottomSheetRef = React.useRef(); const snapToIndex = bottomSheetRef.current?.snapToIndex; React.useEffect(() => { // When the snapPoints change, always reset to the first one // Without this, when we close the WalletConnect modal we don't resize snapToIndex?.(0); }, [snapToIndex, snapPoints]); const closeBottomSheet = bottomSheetRef.current?.close; const { closing, onSuccessfulWalletSignature } = props; const nonceTimestamp = nonceInfo?.nonceTimestamp; const handleMessage = React.useCallback( async (event: WebViewMessageEvent) => { const data: SIWEWebViewMessage = JSON.parse(event.nativeEvent.data); if (data.type === 'siwe_success') { const { address, message, signature } = data; if (address && signature) { nonceNotNeededRef.current = true; closeBottomSheet?.(); invariant(nonceTimestamp, 'nonceTimestamp should be set'); await onSuccessfulWalletSignature({ address, message, signature, nonceTimestamp, }); } } else if (data.type === 'siwe_closed') { nonceNotNeededRef.current = true; onClosing(); closeBottomSheet?.(); } else if (data.type === 'walletconnect_modal_update') { const height = data.state === 'open' ? data.height : 0; if (!walletConnectModalHeight || height > 0) { setWalletConnectModalHeight(height); } } }, [ onSuccessfulWalletSignature, onClosing, closeBottomSheet, walletConnectModalHeight, nonceTimestamp, ], ); const prevClosingRef = React.useRef(); React.useEffect(() => { if (closing && !prevClosingRef.current) { nonceNotNeededRef.current = true; closeBottomSheet?.(); } prevClosingRef.current = closing; }, [closing, closeBottomSheet]); const nonce = nonceInfo?.nonce; const source = React.useMemo( () => ({ uri: commSIWE, headers: { 'siwe-nonce': nonce, 'siwe-primary-identity-public-key': primaryIdentityPublicKey, 'siwe-message-type': siweMessageType, }, }), [nonce, primaryIdentityPublicKey, siweMessageType], ); const onWebViewLoaded = React.useCallback(() => { setLoading(false); }, []); const walletConnectModalOpen = walletConnectModalHeight !== 0; const backgroundStyle = React.useMemo( () => ({ backgroundColor: walletConnectModalOpen ? '#3396ff' : '#242529', }), [walletConnectModalOpen], ); const bottomSheetHandleIndicatorStyle = React.useMemo( () => ({ backgroundColor: 'white', }), [], ); const { onClosed } = props; const onBottomSheetChange = React.useCallback( (index: number) => { if (index === -1) { onClosed(); } }, [onClosed], ); let bottomSheet; if (nonce && primaryIdentityPublicKey) { bottomSheet = ( ); } const setLoadingProp = props.setLoading; const loading = !legacyGetSIWENonceCallFailed && !identityGenerateNonceFailed && (isLoading || legacySiweAuthCallLoading); React.useEffect(() => { setLoadingProp(loading); }, [setLoadingProp, loading]); return bottomSheet; } export default SIWEPanel; diff --git a/native/chat/compose-subchannel.react.js b/native/chat/compose-subchannel.react.js index 29f120739..0d5e08d64 100644 --- a/native/chat/compose-subchannel.react.js +++ b/native/chat/compose-subchannel.react.js @@ -1,369 +1,369 @@ // @flow import invariant from 'invariant'; import _filter from 'lodash/fp/filter.js'; import _flow from 'lodash/fp/flow.js'; import _sortBy from 'lodash/fp/sortBy.js'; import * as React from 'react'; import { Text, View } from 'react-native'; import { newThreadActionTypes, useNewThread, } from 'lib/actions/thread-actions.js'; import { useENSNames } from 'lib/hooks/ens-cache.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import { userInfoSelectorForPotentialMembers } from 'lib/selectors/user-selectors.js'; import { usePotentialMemberItems } from 'lib/shared/search-utils.js'; import { threadInFilterList, userIsMember } from 'lib/shared/thread-utils.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import { type ThreadType, threadTypes } from 'lib/types/thread-types-enum.js'; import { type AccountUserInfo } from 'lib/types/user-types.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import type { ChatNavigationProp } from './chat.react.js'; import { useNavigateToThread } from './message-list-types.js'; import ParentThreadHeader from './parent-thread-header.react.js'; import LinkButton from '../components/link-button.react.js'; import { type BaseTagInput, createTagInput, } from '../components/tag-input.react.js'; import ThreadList from '../components/thread-list.react.js'; import UserList from '../components/user-list.react.js'; import { useCalendarQuery } from '../navigation/nav-selectors.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; -import { UnknownErrorAlertDetails } from '../utils/alert-messages.js'; +import { unknownErrorAlertDetails } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; const TagInput = createTagInput(); const tagInputProps = { placeholder: 'username', autoFocus: true, returnKeyType: 'go', }; const tagDataLabelExtractor = (userInfo: AccountUserInfo) => userInfo.username; export type ComposeSubchannelParams = { +threadType: ThreadType, +parentThreadInfo: ThreadInfo, }; type Props = { +navigation: ChatNavigationProp<'ComposeSubchannel'>, +route: NavigationRoute<'ComposeSubchannel'>, }; function ComposeSubchannel(props: Props): React.Node { const [usernameInputText, setUsernameInputText] = React.useState(''); const [userInfoInputArray, setUserInfoInputArray] = React.useState< $ReadOnlyArray, >([]); const [createButtonEnabled, setCreateButtonEnabled] = React.useState(true); const tagInputRef = React.useRef>(); const onUnknownErrorAlertAcknowledged = React.useCallback(() => { setUsernameInputText(''); tagInputRef.current?.focus(); }, []); const waitingOnThreadIDRef = React.useRef(); const { threadType, parentThreadInfo } = props.route.params; const userInfoInputIDs = userInfoInputArray.map(userInfo => userInfo.id); const callNewThread = useNewThread(); const calendarQuery = useCalendarQuery(); const newChatThreadAction = React.useCallback(async () => { try { const assumedThreadType = threadType ?? threadTypes.COMMUNITY_SECRET_SUBTHREAD; const query = calendarQuery(); invariant( assumedThreadType === 3 || assumedThreadType === 4 || assumedThreadType === 6 || assumedThreadType === 7, "Sidebars and communities can't be created from the thread composer", ); const result = await callNewThread({ type: assumedThreadType, parentThreadID: parentThreadInfo.id, initialMemberIDs: userInfoInputIDs, color: parentThreadInfo.color, calendarQuery: query, }); waitingOnThreadIDRef.current = result.newThreadID; return result; } catch (e) { setCreateButtonEnabled(true); Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, [{ text: 'OK', onPress: onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); throw e; } }, [ threadType, userInfoInputIDs, calendarQuery, parentThreadInfo, callNewThread, onUnknownErrorAlertAcknowledged, ]); const dispatchActionPromise = useDispatchActionPromise(); const dispatchNewChatThreadAction = React.useCallback(() => { setCreateButtonEnabled(false); void dispatchActionPromise(newThreadActionTypes, newChatThreadAction()); }, [dispatchActionPromise, newChatThreadAction]); const userInfoInputArrayEmpty = userInfoInputArray.length === 0; const onPressCreateThread = React.useCallback(() => { if (!createButtonEnabled) { return; } if (userInfoInputArrayEmpty) { Alert.alert( 'Chatting to yourself?', 'Are you sure you want to create a channel containing only yourself?', [ { text: 'Cancel', style: 'cancel' }, { text: 'Confirm', onPress: dispatchNewChatThreadAction }, ], { cancelable: true }, ); } else { dispatchNewChatThreadAction(); } }, [ createButtonEnabled, userInfoInputArrayEmpty, dispatchNewChatThreadAction, ]); const { navigation } = props; const { setOptions } = navigation; React.useEffect(() => { setOptions({ headerRight: () => ( ), }); }, [setOptions, onPressCreateThread, createButtonEnabled]); const { setParams } = navigation; const parentThreadInfoID = parentThreadInfo.id; const reduxParentThreadInfo = useSelector( state => threadInfoSelector(state)[parentThreadInfoID], ); React.useEffect(() => { if (reduxParentThreadInfo) { setParams({ parentThreadInfo: reduxParentThreadInfo }); } }, [reduxParentThreadInfo, setParams]); const threadInfos = useSelector(threadInfoSelector); const newlyCreatedThreadInfo = waitingOnThreadIDRef.current ? threadInfos[waitingOnThreadIDRef.current] : null; const { pushNewThread } = navigation; React.useEffect(() => { if (!newlyCreatedThreadInfo) { return; } const waitingOnThreadID = waitingOnThreadIDRef.current; if (waitingOnThreadID === null || waitingOnThreadID === undefined) { return; } waitingOnThreadIDRef.current = undefined; pushNewThread(newlyCreatedThreadInfo); }, [newlyCreatedThreadInfo, pushNewThread]); const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers); const { community } = parentThreadInfo; const communityThreadInfo = useSelector(state => community ? threadInfoSelector(state)[community] : null, ); const userSearchResults = usePotentialMemberItems({ text: usernameInputText, userInfos: otherUserInfos, excludeUserIDs: userInfoInputIDs, inputParentThreadInfo: parentThreadInfo, inputCommunityThreadInfo: communityThreadInfo, threadType, }); const existingThreads: $ReadOnlyArray = React.useMemo(() => { if (userInfoInputIDs.length === 0) { return []; } return _flow( _filter( (threadInfo: ThreadInfo) => threadInFilterList(threadInfo) && threadInfo.parentThreadID === parentThreadInfo.id && userInfoInputIDs.every(userID => userIsMember(threadInfo, userID)), ), _sortBy( ([ 'members.length', (threadInfo: ThreadInfo) => (threadInfo.name ? 1 : 0), ]: $ReadOnlyArray mixed)>), ), )(threadInfos); }, [userInfoInputIDs, threadInfos, parentThreadInfo]); const navigateToThread = useNavigateToThread(); const onSelectExistingThread = React.useCallback( (threadID: string) => { const threadInfo = threadInfos[threadID]; navigateToThread({ threadInfo }); }, [threadInfos, navigateToThread], ); const onUserSelect = React.useCallback( ({ id }: AccountUserInfo) => { if (userInfoInputIDs.some(existingUserID => id === existingUserID)) { return; } setUserInfoInputArray(oldUserInfoInputArray => [ ...oldUserInfoInputArray, otherUserInfos[id], ]); setUsernameInputText(''); }, [userInfoInputIDs, otherUserInfos], ); const styles = useStyles(unboundStyles); let existingThreadsSection = null; if (existingThreads.length > 0) { existingThreadsSection = ( Existing channels ); } const inputProps = React.useMemo( () => ({ ...tagInputProps, onSubmitEditing: onPressCreateThread, }), [onPressCreateThread], ); const userSearchResultWithENSNames = useENSNames(userSearchResults); const userInfoInputArrayWithENSNames = useENSNames(userInfoInputArray); return ( To: {existingThreadsSection} ); } const unboundStyles = { container: { flex: 1, }, existingThreadList: { backgroundColor: 'modalBackground', flex: 1, paddingRight: 12, }, existingThreads: { flex: 1, }, existingThreadsLabel: { color: 'modalForegroundSecondaryLabel', fontSize: 16, paddingLeft: 12, textAlign: 'center', }, existingThreadsRow: { backgroundColor: 'modalForeground', borderBottomWidth: 1, borderColor: 'modalForegroundBorder', borderTopWidth: 1, paddingVertical: 6, }, listItem: { color: 'modalForegroundLabel', }, tagInputContainer: { flex: 1, marginLeft: 8, paddingRight: 12, }, tagInputLabel: { color: 'modalForegroundSecondaryLabel', fontSize: 16, paddingLeft: 12, }, userList: { backgroundColor: 'modalBackground', flex: 1, paddingLeft: 35, paddingRight: 12, }, userSelectionRow: { alignItems: 'center', backgroundColor: 'modalForeground', borderBottomWidth: 1, borderColor: 'modalForegroundBorder', flexDirection: 'row', paddingVertical: 6, }, }; const MemoizedComposeSubchannel: React.ComponentType = React.memo(ComposeSubchannel); export default MemoizedComposeSubchannel; diff --git a/native/chat/relationship-prompt.react.js b/native/chat/relationship-prompt.react.js index 813e535b7..c76495522 100644 --- a/native/chat/relationship-prompt.react.js +++ b/native/chat/relationship-prompt.react.js @@ -1,172 +1,172 @@ // @flow import Icon from '@expo/vector-icons/FontAwesome5.js'; import * as React from 'react'; import { Text, View } from 'react-native'; import { useRelationshipPrompt } from 'lib/hooks/relationship-prompt.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import { userRelationshipStatus } from 'lib/types/relationship-types.js'; import type { UserInfo } from 'lib/types/user-types.js'; import Button from '../components/button.react.js'; import { useStyles } from '../themes/colors.js'; -import { UnknownErrorAlertDetails } from '../utils/alert-messages.js'; +import { unknownErrorAlertDetails } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; type Props = { +pendingPersonalThreadUserInfo: ?UserInfo, +threadInfo: ThreadInfo, }; const RelationshipPrompt: React.ComponentType = React.memo( function RelationshipPrompt({ pendingPersonalThreadUserInfo, threadInfo, }: Props) { const onErrorCallback = React.useCallback(() => { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, [{ text: 'OK' }], ); }, []); const { otherUserInfo, callbacks: { blockUser, unblockUser, friendUser, unfriendUser }, } = useRelationshipPrompt( threadInfo, onErrorCallback, pendingPersonalThreadUserInfo, ); const styles = useStyles(unboundStyles); if ( !otherUserInfo || !otherUserInfo.username || otherUserInfo.relationshipStatus === userRelationshipStatus.FRIEND ) { return null; } if ( otherUserInfo.relationshipStatus === userRelationshipStatus.BLOCKED_VIEWER ) { return ( ); } if ( otherUserInfo.relationshipStatus === userRelationshipStatus.BOTH_BLOCKED || otherUserInfo.relationshipStatus === userRelationshipStatus.BLOCKED_BY_VIEWER ) { return ( ); } if ( otherUserInfo.relationshipStatus === userRelationshipStatus.REQUEST_RECEIVED ) { return ( ); } if ( otherUserInfo.relationshipStatus === userRelationshipStatus.REQUEST_SENT ) { return ( ); } return ( ); }, ); const unboundStyles = { container: { paddingVertical: 10, paddingHorizontal: 5, backgroundColor: 'panelBackground', flexDirection: 'row', }, button: { padding: 10, borderRadius: 5, flex: 1, flexDirection: 'row', justifyContent: 'center', marginHorizontal: 5, }, greenButton: { backgroundColor: 'vibrantGreenButton', }, redButton: { backgroundColor: 'vibrantRedButton', }, buttonText: { fontSize: 11, color: 'white', fontWeight: 'bold', textAlign: 'center', marginLeft: 5, }, }; export default RelationshipPrompt; diff --git a/native/chat/settings/add-users-modal.react.js b/native/chat/settings/add-users-modal.react.js index 149f5a4f7..5deea882d 100644 --- a/native/chat/settings/add-users-modal.react.js +++ b/native/chat/settings/add-users-modal.react.js @@ -1,286 +1,286 @@ // @flow import * as React from 'react'; import { ActivityIndicator, Text, View } from 'react-native'; import { changeThreadSettingsActionTypes, useChangeThreadSettings, } from 'lib/actions/thread-actions.js'; import { useENSNames } from 'lib/hooks/ens-cache.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import { userInfoSelectorForPotentialMembers } from 'lib/selectors/user-selectors.js'; import { usePotentialMemberItems } from 'lib/shared/search-utils.js'; import { threadActualMembers } from 'lib/shared/thread-utils.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import { type AccountUserInfo } from 'lib/types/user-types.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import Button from '../../components/button.react.js'; import Modal from '../../components/modal.react.js'; import { type BaseTagInput, createTagInput, } from '../../components/tag-input.react.js'; import UserList from '../../components/user-list.react.js'; import type { RootNavigationProp } from '../../navigation/root-navigator.react.js'; import type { NavigationRoute } from '../../navigation/route-names.js'; import { useSelector } from '../../redux/redux-utils.js'; import { useStyles } from '../../themes/colors.js'; -import { UnknownErrorAlertDetails } from '../../utils/alert-messages.js'; +import { unknownErrorAlertDetails } from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; const TagInput = createTagInput(); const tagInputProps = { placeholder: 'Select users to add', autoFocus: true, returnKeyType: 'go', }; const tagDataLabelExtractor = (userInfo: AccountUserInfo) => userInfo.username; export type AddUsersModalParams = { +presentedFrom: string, +threadInfo: ThreadInfo, }; type Props = { +navigation: RootNavigationProp<'AddUsersModal'>, +route: NavigationRoute<'AddUsersModal'>, }; function AddUsersModal(props: Props): React.Node { const [usernameInputText, setUsernameInputText] = React.useState(''); const [userInfoInputArray, setUserInfoInputArray] = React.useState< $ReadOnlyArray, >([]); const tagInputRef = React.useRef>(); const onUnknownErrorAlertAcknowledged = React.useCallback(() => { setUsernameInputText(''); setUserInfoInputArray([]); tagInputRef.current?.focus(); }, []); const { navigation } = props; const { goBackOnce } = navigation; const close = React.useCallback(() => { goBackOnce(); }, [goBackOnce]); const callChangeThreadSettings = useChangeThreadSettings(); const userInfoInputIDs = userInfoInputArray.map(userInfo => userInfo.id); const { route } = props; const { threadInfo } = route.params; const threadID = threadInfo.id; const addUsersToThread = React.useCallback(async () => { try { const result = await callChangeThreadSettings({ threadID: threadID, changes: { newMemberIDs: userInfoInputIDs }, }); close(); return result; } catch (e) { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, [{ text: 'OK', onPress: onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); throw e; } }, [ callChangeThreadSettings, threadID, userInfoInputIDs, close, onUnknownErrorAlertAcknowledged, ]); const inputLength = userInfoInputArray.length; const dispatchActionPromise = useDispatchActionPromise(); const userInfoInputArrayEmpty = inputLength === 0; const onPressAdd = React.useCallback(() => { if (userInfoInputArrayEmpty) { return; } void dispatchActionPromise( changeThreadSettingsActionTypes, addUsersToThread(), ); }, [userInfoInputArrayEmpty, dispatchActionPromise, addUsersToThread]); const changeThreadSettingsLoadingStatus = useSelector( createLoadingStatusSelector(changeThreadSettingsActionTypes), ); const isLoading = changeThreadSettingsLoadingStatus === 'loading'; const styles = useStyles(unboundStyles); let addButton = null; if (inputLength > 0) { let activityIndicator = null; if (isLoading) { activityIndicator = ( ); } const addButtonText = `Add (${inputLength})`; addButton = ( ); } let cancelButton; if (!isLoading) { cancelButton = ( ); } else { cancelButton = ; } const threadMemberIDs = React.useMemo( () => threadActualMembers(threadInfo.members), [threadInfo.members], ); const excludeUserIDs = React.useMemo( () => userInfoInputIDs.concat(threadMemberIDs), [userInfoInputIDs, threadMemberIDs], ); const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers); const { parentThreadID, community } = props.route.params.threadInfo; const parentThreadInfo = useSelector(state => parentThreadID ? threadInfoSelector(state)[parentThreadID] : null, ); const communityThreadInfo = useSelector(state => community ? threadInfoSelector(state)[community] : null, ); const userSearchResults = usePotentialMemberItems({ text: usernameInputText, userInfos: otherUserInfos, excludeUserIDs, inputParentThreadInfo: parentThreadInfo, inputCommunityThreadInfo: communityThreadInfo, threadType: threadInfo.type, }); const onChangeTagInput = React.useCallback( (newUserInfoInputArray: $ReadOnlyArray) => { if (!isLoading) { setUserInfoInputArray(newUserInfoInputArray); } }, [isLoading], ); const onChangeTagInputText = React.useCallback( (text: string) => { if (!isLoading) { setUsernameInputText(text); } }, [isLoading], ); const onUserSelect = React.useCallback( ({ id }: AccountUserInfo) => { if (isLoading) { return; } if (userInfoInputIDs.some(existingUserID => id === existingUserID)) { return; } setUserInfoInputArray(oldUserInfoInputArray => [ ...oldUserInfoInputArray, otherUserInfos[id], ]); setUsernameInputText(''); }, [isLoading, userInfoInputIDs, otherUserInfos], ); const inputProps = React.useMemo( () => ({ ...tagInputProps, onSubmitEditing: onPressAdd, }), [onPressAdd], ); const userSearchResultWithENSNames = useENSNames(userSearchResults); const userInfoInputArrayWithENSNames = useENSNames(userInfoInputArray); return ( {cancelButton} {addButton} ); } const unboundStyles = { activityIndicator: { paddingRight: 6, }, addButton: { backgroundColor: 'vibrantGreenButton', borderRadius: 3, flexDirection: 'row', paddingHorizontal: 10, paddingVertical: 4, }, addText: { color: 'white', fontSize: 18, }, buttons: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 12, }, cancelButton: { backgroundColor: 'modalButton', borderRadius: 3, paddingHorizontal: 10, paddingVertical: 4, }, cancelText: { color: 'modalButtonLabel', fontSize: 18, }, }; const MemoizedAddUsersModal: React.ComponentType = React.memo(AddUsersModal); export default MemoizedAddUsersModal; diff --git a/native/chat/settings/color-selector-modal.react.js b/native/chat/settings/color-selector-modal.react.js index 191a01ad9..6c5f9fb45 100644 --- a/native/chat/settings/color-selector-modal.react.js +++ b/native/chat/settings/color-selector-modal.react.js @@ -1,193 +1,193 @@ // @flow import Icon from '@expo/vector-icons/FontAwesome.js'; import * as React from 'react'; import { TouchableHighlight } from 'react-native'; import { changeThreadSettingsActionTypes, useChangeThreadSettings, } from 'lib/actions/thread-actions.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import { type ChangeThreadSettingsPayload, type UpdateThreadRequest, } from 'lib/types/thread-types.js'; import { type DispatchActionPromise, useDispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import ColorSelector from '../../components/color-selector.react.js'; import Modal from '../../components/modal.react.js'; import type { RootNavigationProp } from '../../navigation/root-navigator.react.js'; import type { NavigationRoute } from '../../navigation/route-names.js'; import { useSelector } from '../../redux/redux-utils.js'; import { type Colors, useColors, useStyles } from '../../themes/colors.js'; -import { UnknownErrorAlertDetails } from '../../utils/alert-messages.js'; +import { unknownErrorAlertDetails } from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; export type ColorSelectorModalParams = { +presentedFrom: string, +color: string, +threadInfo: ThreadInfo, +setColor: (color: string) => void, }; const unboundStyles = { closeButton: { borderRadius: 3, height: 18, position: 'absolute', right: 5, top: 5, width: 18, }, closeButtonIcon: { color: 'modalBackgroundSecondaryLabel', left: 3, position: 'absolute', }, colorSelector: { bottom: 10, left: 10, position: 'absolute', right: 10, top: 10, }, colorSelectorContainer: { backgroundColor: 'modalBackground', borderRadius: 5, flex: 0, marginHorizontal: 15, marginVertical: 20, }, }; type BaseProps = { +navigation: RootNavigationProp<'ColorSelectorModal'>, +route: NavigationRoute<'ColorSelectorModal'>, }; type Props = { ...BaseProps, // Redux state +colors: Colors, +styles: $ReadOnly, +windowWidth: number, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +changeThreadSettings: ( request: UpdateThreadRequest, ) => Promise, }; function ColorSelectorModal(props: Props): React.Node { const { changeThreadSettings: updateThreadSettings, dispatchActionPromise, windowWidth, } = props; const { threadInfo, setColor } = props.route.params; const close = props.navigation.goBackOnce; const onErrorAcknowledged = React.useCallback(() => { setColor(threadInfo.color); }, [setColor, threadInfo.color]); const editColor = React.useCallback( async (newColor: string) => { const threadID = threadInfo.id; try { return await updateThreadSettings({ threadID, changes: { color: newColor }, }); } catch (e) { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, [{ text: 'OK', onPress: onErrorAcknowledged }], { cancelable: false }, ); throw e; } }, [onErrorAcknowledged, threadInfo.id, updateThreadSettings], ); const onColorSelected = React.useCallback( (color: string) => { const colorEditValue = color.substr(1); setColor(colorEditValue); close(); const action = changeThreadSettingsActionTypes.started; const threadID = props.route.params.threadInfo.id; void dispatchActionPromise( changeThreadSettingsActionTypes, editColor(colorEditValue), { customKeyName: `${action}:${threadID}:color`, }, ); }, [ setColor, close, dispatchActionPromise, editColor, props.route.params.threadInfo.id, ], ); const { colorSelectorContainer, closeButton, closeButtonIcon } = props.styles; // Based on the assumption we are always in portrait, // and consequently width is the lowest dimensions const modalStyle = React.useMemo( () => [colorSelectorContainer, { height: 0.75 * windowWidth }], [colorSelectorContainer, windowWidth], ); const { modalIosHighlightUnderlay } = props.colors; const { color } = props.route.params; return ( ); } const ConnectedColorSelectorModal: React.ComponentType = React.memo(function ConnectedColorSelectorModal(props: BaseProps) { const styles = useStyles(unboundStyles); const colors = useColors(); const windowWidth = useSelector(state => state.dimensions.width); const dispatchActionPromise = useDispatchActionPromise(); const callChangeThreadSettings = useChangeThreadSettings(); return ( ); }); export default ConnectedColorSelectorModal; diff --git a/native/chat/settings/delete-thread.react.js b/native/chat/settings/delete-thread.react.js index f01d3f6b1..084000b83 100644 --- a/native/chat/settings/delete-thread.react.js +++ b/native/chat/settings/delete-thread.react.js @@ -1,299 +1,299 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { ActivityIndicator, Text, TextInput as BaseTextInput, View, } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; import type { DeleteThreadInput } from 'lib/actions/thread-actions.js'; import { deleteThreadActionTypes, useDeleteThread, } from 'lib/actions/thread-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { containedThreadInfos, threadInfoSelector, } from 'lib/selectors/thread-selectors.js'; import { getThreadsToDeleteText, identifyInvalidatedThreads, } from 'lib/shared/thread-utils.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { ResolvedThreadInfo, ThreadInfo, } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import type { LeaveThreadPayload } from 'lib/types/thread-types.js'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; import { type DispatchActionPromise, useDispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import Button from '../../components/button.react.js'; import { clearThreadsActionType } from '../../navigation/action-types.js'; import { type NavAction, NavContext, } from '../../navigation/navigation-context.js'; import type { NavigationRoute } from '../../navigation/route-names.js'; import { useSelector } from '../../redux/redux-utils.js'; import { type Colors, useColors, useStyles } from '../../themes/colors.js'; -import { UnknownErrorAlertDetails } from '../../utils/alert-messages.js'; +import { unknownErrorAlertDetails } from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; import type { ChatNavigationProp } from '../chat.react.js'; export type DeleteThreadParams = { +threadInfo: ThreadInfo, }; const unboundStyles = { deleteButton: { backgroundColor: 'vibrantRedButton', borderRadius: 5, flex: 1, marginHorizontal: 24, marginVertical: 12, padding: 12, }, deleteText: { color: 'white', fontSize: 18, textAlign: 'center', }, header: { color: 'panelBackgroundLabel', fontSize: 12, fontWeight: '400', paddingBottom: 3, paddingHorizontal: 24, }, input: { color: 'panelForegroundLabel', flex: 1, fontFamily: 'Arial', fontSize: 16, paddingVertical: 0, borderBottomColor: 'transparent', }, scrollView: { backgroundColor: 'panelBackground', }, scrollViewContentContainer: { paddingTop: 24, }, section: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, flexDirection: 'row', justifyContent: 'space-between', marginBottom: 24, paddingHorizontal: 24, paddingVertical: 12, }, warningText: { color: 'panelForegroundLabel', fontSize: 16, marginBottom: 24, marginHorizontal: 24, textAlign: 'center', }, }; type BaseProps = { +navigation: ChatNavigationProp<'DeleteThread'>, +route: NavigationRoute<'DeleteThread'>, }; type Props = { ...BaseProps, // Redux state +threadInfo: ResolvedThreadInfo, +shouldUseDeleteConfirmationAlert: boolean, +loadingStatus: LoadingStatus, +colors: Colors, +styles: $ReadOnly, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +deleteThread: (input: DeleteThreadInput) => Promise, // withNavContext +navDispatch: (action: NavAction) => void, }; class DeleteThread extends React.PureComponent { mounted = false; passwordInput: ?React.ElementRef; componentDidMount() { this.mounted = true; } componentWillUnmount() { this.mounted = false; } render(): React.Node { const buttonContent = this.props.loadingStatus === 'loading' ? ( ) : ( Delete chat ); const { threadInfo } = this.props; return ( {`The chat "${threadInfo.uiName}" will be permanently deleted. `} There is no way to reverse this. ); } passwordInputRef = ( passwordInput: ?React.ElementRef, ) => { this.passwordInput = passwordInput; }; focusPasswordInput = () => { invariant(this.passwordInput, 'passwordInput should be set'); this.passwordInput.focus(); }; dispatchDeleteThreadAction = () => { void this.props.dispatchActionPromise( deleteThreadActionTypes, this.deleteThread(), ); }; submitDeletion = () => { if (!this.props.shouldUseDeleteConfirmationAlert) { this.dispatchDeleteThreadAction(); return; } Alert.alert( 'Warning', `${getThreadsToDeleteText( this.props.threadInfo, )} will also be permanently deleted.`, [ { text: 'Cancel', style: 'cancel' }, { text: 'Continue', onPress: this.dispatchDeleteThreadAction }, ], { cancelable: false }, ); }; async deleteThread(): Promise { const { threadInfo, navDispatch } = this.props; navDispatch({ type: clearThreadsActionType, payload: { threadIDs: [threadInfo.id] }, }); try { const result = await this.props.deleteThread({ threadID: threadInfo.id }); const invalidated = identifyInvalidatedThreads( result.updatesResult.newUpdates, ); navDispatch({ type: clearThreadsActionType, payload: { threadIDs: [...invalidated] }, }); return result; } catch (e) { if (e.message === 'invalid_credentials') { Alert.alert( 'Permission not granted', 'You do not have permission to delete this thread', [{ text: 'OK' }], { cancelable: false }, ); } else { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, [{ text: 'OK' }], { cancelable: false, }, ); } throw e; } } } const loadingStatusSelector = createLoadingStatusSelector( deleteThreadActionTypes, ); const ConnectedDeleteThread: React.ComponentType = React.memo(function ConnectedDeleteThread(props: BaseProps) { const threadID = props.route.params.threadInfo.id; const reduxThreadInfo = useSelector( state => threadInfoSelector(state)[threadID], ); const reduxContainedThreadInfos = useSelector( state => containedThreadInfos(state)[threadID], ); const { setParams } = props.navigation; React.useEffect(() => { if (reduxThreadInfo) { setParams({ threadInfo: reduxThreadInfo }); } }, [reduxThreadInfo, setParams]); const threadInfo = reduxThreadInfo ?? props.route.params.threadInfo; const resolvedThreadInfo = useResolvedThreadInfo(threadInfo); const loadingStatus = useSelector(loadingStatusSelector); const colors = useColors(); const styles = useStyles(unboundStyles); const dispatchActionPromise = useDispatchActionPromise(); const callDeleteThread = useDeleteThread(); const navContext = React.useContext(NavContext); invariant(navContext, 'NavContext should be set in DeleteThread'); const navDispatch = navContext.dispatch; const shouldUseDeleteConfirmationAlert = reduxContainedThreadInfos && reduxContainedThreadInfos.length > 0; return ( ); }); export default ConnectedDeleteThread; diff --git a/native/chat/settings/thread-settings-description.react.js b/native/chat/settings/thread-settings-description.react.js index 32ed54dce..ac732bde9 100644 --- a/native/chat/settings/thread-settings-description.react.js +++ b/native/chat/settings/thread-settings-description.react.js @@ -1,329 +1,329 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { ActivityIndicator, Text, TextInput as BaseTextInput, View, } from 'react-native'; import { changeThreadSettingsActionTypes, useChangeThreadSettings, } from 'lib/actions/thread-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { useThreadHasPermission } from 'lib/shared/thread-utils.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { type ChangeThreadSettingsPayload, type UpdateThreadRequest, } from 'lib/types/thread-types.js'; import { type DispatchActionPromise, useDispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import SaveSettingButton from './save-setting-button.react.js'; import { ThreadSettingsCategoryFooter, ThreadSettingsCategoryHeader, } from './thread-settings-category.react.js'; import Button from '../../components/button.react.js'; import EditSettingButton from '../../components/edit-setting-button.react.js'; import SWMansionIcon from '../../components/swmansion-icon.react.js'; import TextInput from '../../components/text-input.react.js'; import { useSelector } from '../../redux/redux-utils.js'; import { type Colors, useColors, useStyles } from '../../themes/colors.js'; import type { ContentSizeChangeEvent, LayoutEvent, } from '../../types/react-native.js'; -import { UnknownErrorAlertDetails } from '../../utils/alert-messages.js'; +import { unknownErrorAlertDetails } from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; const unboundStyles = { addDescriptionButton: { flexDirection: 'row', paddingHorizontal: 24, paddingVertical: 10, }, addDescriptionText: { color: 'panelForegroundTertiaryLabel', flex: 1, fontSize: 16, }, editIcon: { color: 'panelForegroundTertiaryLabel', paddingLeft: 10, textAlign: 'right', }, outlineCategory: { backgroundColor: 'panelForeground', borderColor: 'panelForegroundBorder', borderRadius: 1, borderStyle: 'dashed', borderWidth: 1, marginLeft: -1, marginRight: -1, }, row: { backgroundColor: 'panelForeground', flexDirection: 'row', paddingHorizontal: 24, paddingVertical: 4, }, text: { color: 'panelForegroundSecondaryLabel', flex: 1, fontFamily: 'Arial', fontSize: 16, margin: 0, padding: 0, borderBottomColor: 'transparent', }, }; type BaseProps = { +threadInfo: ThreadInfo, +descriptionEditValue: ?string, +setDescriptionEditValue: (value: ?string, callback?: () => void) => void, +descriptionTextHeight: ?number, +setDescriptionTextHeight: (number: number) => void, +canChangeSettings: boolean, }; type Props = { ...BaseProps, // Redux state +loadingStatus: LoadingStatus, +colors: Colors, +styles: $ReadOnly, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +changeThreadSettings: ( update: UpdateThreadRequest, ) => Promise, +canEditThreadDescription: boolean, }; class ThreadSettingsDescription extends React.PureComponent { textInput: ?React.ElementRef; render(): React.Node { if ( this.props.descriptionEditValue !== null && this.props.descriptionEditValue !== undefined ) { const textInputStyle: { height?: number } = {}; if ( this.props.descriptionTextHeight !== undefined && this.props.descriptionTextHeight !== null ) { textInputStyle.height = this.props.descriptionTextHeight; } return ( {this.renderButton()} ); } if (this.props.threadInfo.description) { return ( {this.props.threadInfo.description} {this.renderButton()} ); } const { panelIosHighlightUnderlay } = this.props.colors; if (this.props.canEditThreadDescription) { return ( ); } return null; } renderButton(): React.Node { if (this.props.loadingStatus === 'loading') { return ( ); } else if ( this.props.descriptionEditValue === null || this.props.descriptionEditValue === undefined ) { return ( ); } return ; } textInputRef = (textInput: ?React.ElementRef) => { this.textInput = textInput; }; onLayoutText = (event: LayoutEvent) => { this.props.setDescriptionTextHeight(event.nativeEvent.layout.height); }; onTextInputContentSizeChange = (event: ContentSizeChangeEvent) => { this.props.setDescriptionTextHeight(event.nativeEvent.contentSize.height); }; onPressEdit = () => { this.props.setDescriptionEditValue(this.props.threadInfo.description); }; onSubmit = () => { invariant( this.props.descriptionEditValue !== null && this.props.descriptionEditValue !== undefined, 'should be set', ); const description = this.props.descriptionEditValue.trim(); if (description === this.props.threadInfo.description) { this.props.setDescriptionEditValue(null); return; } const editDescriptionPromise = this.editDescription(description); const action = changeThreadSettingsActionTypes.started; const threadID = this.props.threadInfo.id; void this.props.dispatchActionPromise( changeThreadSettingsActionTypes, editDescriptionPromise, { customKeyName: `${action}:${threadID}:description`, }, ); void editDescriptionPromise.then(() => { this.props.setDescriptionEditValue(null); }); }; async editDescription( newDescription: string, ): Promise { try { return await this.props.changeThreadSettings({ threadID: this.props.threadInfo.id, changes: { description: newDescription }, }); } catch (e) { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onErrorAcknowledged }], { cancelable: false }, ); throw e; } } onErrorAcknowledged = () => { this.props.setDescriptionEditValue( this.props.threadInfo.description, () => { invariant(this.textInput, 'textInput should be set'); this.textInput.focus(); }, ); }; } const ConnectedThreadSettingsDescription: React.ComponentType = React.memo(function ConnectedThreadSettingsDescription( props: BaseProps, ) { const threadID = props.threadInfo.id; const loadingStatus = useSelector( createLoadingStatusSelector( changeThreadSettingsActionTypes, `${changeThreadSettingsActionTypes.started}:${threadID}:description`, ), ); const colors = useColors(); const styles = useStyles(unboundStyles); const dispatchActionPromise = useDispatchActionPromise(); const callChangeThreadSettings = useChangeThreadSettings(); const canEditThreadDescription = useThreadHasPermission( props.threadInfo, threadPermissions.EDIT_THREAD_DESCRIPTION, ); return ( ); }); export default ConnectedThreadSettingsDescription; diff --git a/native/chat/settings/thread-settings-edit-relationship.react.js b/native/chat/settings/thread-settings-edit-relationship.react.js index f6d741602..b3969315b 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 { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.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 { 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: TraditionalRelationshipAction) => { try { return await callUpdateRelationships({ action, userIDs: [otherUserInfo.id], }); } catch (e) { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + 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/chat/settings/thread-settings-leave-thread.react.js b/native/chat/settings/thread-settings-leave-thread.react.js index 8de53dc9a..b0b8660bf 100644 --- a/native/chat/settings/thread-settings-leave-thread.react.js +++ b/native/chat/settings/thread-settings-leave-thread.react.js @@ -1,191 +1,191 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { ActivityIndicator, Text, View } from 'react-native'; import type { LeaveThreadInput } from 'lib/actions/thread-actions.js'; import { leaveThreadActionTypes, useLeaveThread, } from 'lib/actions/thread-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { otherUsersButNoOtherAdmins } from 'lib/selectors/thread-selectors.js'; import { identifyInvalidatedThreads } from 'lib/shared/thread-utils.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import type { LeaveThreadPayload } from 'lib/types/thread-types.js'; import { type DispatchActionPromise, useDispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import Button from '../../components/button.react.js'; import { clearThreadsActionType } from '../../navigation/action-types.js'; import { NavContext, type NavContextType, } from '../../navigation/navigation-context.js'; import { useSelector } from '../../redux/redux-utils.js'; import { type Colors, useColors, useStyles } from '../../themes/colors.js'; import type { ViewStyle } from '../../types/styles.js'; -import { UnknownErrorAlertDetails } from '../../utils/alert-messages.js'; +import { unknownErrorAlertDetails } from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; const unboundStyles = { button: { flexDirection: 'row', paddingHorizontal: 12, paddingVertical: 10, }, container: { backgroundColor: 'panelForeground', paddingHorizontal: 12, }, text: { color: 'redText', flex: 1, fontSize: 16, }, }; type BaseProps = { +threadInfo: ThreadInfo, +buttonStyle: ViewStyle, }; type Props = { ...BaseProps, // Redux state +loadingStatus: LoadingStatus, +otherUsersButNoOtherAdmins: boolean, +colors: Colors, +styles: $ReadOnly, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +leaveThread: (input: LeaveThreadInput) => Promise, // withNavContext +navContext: ?NavContextType, }; class ThreadSettingsLeaveThread extends React.PureComponent { render(): React.Node { const { panelIosHighlightUnderlay, panelForegroundSecondaryLabel } = this.props.colors; const loadingIndicator = this.props.loadingStatus === 'loading' ? ( ) : null; return ( ); } onPress = () => { if (this.props.otherUsersButNoOtherAdmins) { Alert.alert( 'Need another admin', 'Make somebody else an admin before you leave!', undefined, { cancelable: true }, ); return; } Alert.alert( 'Confirm action', 'Are you sure you want to leave this chat?', [ { text: 'Cancel', style: 'cancel' }, { text: 'OK', onPress: this.onConfirmLeaveThread }, ], { cancelable: true }, ); }; onConfirmLeaveThread = () => { const threadID = this.props.threadInfo.id; void this.props.dispatchActionPromise( leaveThreadActionTypes, this.leaveThread(), { customKeyName: `${leaveThreadActionTypes.started}:${threadID}`, }, ); }; async leaveThread(): Promise { const threadID = this.props.threadInfo.id; const { navContext } = this.props; invariant(navContext, 'navContext should exist in leaveThread'); navContext.dispatch({ type: clearThreadsActionType, payload: { threadIDs: [threadID] }, }); try { const result = await this.props.leaveThread({ threadID }); const invalidated = identifyInvalidatedThreads( result.updatesResult.newUpdates, ); navContext.dispatch({ type: clearThreadsActionType, payload: { threadIDs: [...invalidated] }, }); return result; } catch (e) { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, undefined, { cancelable: true, }, ); throw e; } } } const ConnectedThreadSettingsLeaveThread: React.ComponentType = React.memo(function ConnectedThreadSettingsLeaveThread( props: BaseProps, ) { const threadID = props.threadInfo.id; const loadingStatus = useSelector( createLoadingStatusSelector( leaveThreadActionTypes, `${leaveThreadActionTypes.started}:${threadID}`, ), ); const otherUsersButNoOtherAdminsValue = useSelector( otherUsersButNoOtherAdmins(props.threadInfo.id), ); const colors = useColors(); const styles = useStyles(unboundStyles); const dispatchActionPromise = useDispatchActionPromise(); const callLeaveThread = useLeaveThread(); const navContext = React.useContext(NavContext); return ( ); }); export default ConnectedThreadSettingsLeaveThread; diff --git a/native/chat/settings/thread-settings-name.react.js b/native/chat/settings/thread-settings-name.react.js index 81c395754..fb54150a1 100644 --- a/native/chat/settings/thread-settings-name.react.js +++ b/native/chat/settings/thread-settings-name.react.js @@ -1,247 +1,247 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, ActivityIndicator, TextInput as BaseTextInput, View, } from 'react-native'; import { changeThreadSettingsActionTypes, useChangeThreadSettings, } from 'lib/actions/thread-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { ResolvedThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import type { ChangeThreadSettingsPayload, UpdateThreadRequest, } from 'lib/types/thread-types.js'; import { useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import { firstLine } from 'lib/utils/string-utils.js'; import { chatNameMaxLength } from 'lib/utils/validation-utils.js'; import SaveSettingButton from './save-setting-button.react.js'; import EditSettingButton from '../../components/edit-setting-button.react.js'; import SingleLine from '../../components/single-line.react.js'; import TextInput from '../../components/text-input.react.js'; import { useSelector } from '../../redux/redux-utils.js'; import { type Colors, useStyles, useColors } from '../../themes/colors.js'; -import { UnknownErrorAlertDetails } from '../../utils/alert-messages.js'; +import { unknownErrorAlertDetails } from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; const unboundStyles = { currentValue: { color: 'panelForegroundSecondaryLabel', flex: 1, fontFamily: 'Arial', fontSize: 16, margin: 0, paddingLeft: 4, paddingRight: 0, paddingVertical: 0, borderBottomColor: 'transparent', }, label: { color: 'panelForegroundTertiaryLabel', fontSize: 16, width: 96, }, row: { backgroundColor: 'panelForeground', flexDirection: 'row', paddingHorizontal: 24, paddingVertical: 8, }, }; type BaseProps = { +threadInfo: ResolvedThreadInfo, +nameEditValue: ?string, +setNameEditValue: (value: ?string, callback?: () => void) => void, +canChangeSettings: boolean, }; type Props = { ...BaseProps, // Redux state +loadingStatus: LoadingStatus, +colors: Colors, +styles: $ReadOnly, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +changeThreadSettings: ( update: UpdateThreadRequest, ) => Promise, }; class ThreadSettingsName extends React.PureComponent { textInput: ?React.ElementRef; render(): React.Node { return ( Name {this.renderContent()} ); } renderButton(): React.Node { if (this.props.loadingStatus === 'loading') { return ( ); } else if ( this.props.nameEditValue === null || this.props.nameEditValue === undefined ) { return ( ); } return ; } renderContent(): React.Node { if ( this.props.nameEditValue === null || this.props.nameEditValue === undefined ) { return ( {this.props.threadInfo.uiName} {this.renderButton()} ); } return ( {this.renderButton()} ); } textInputRef = (textInput: ?React.ElementRef) => { this.textInput = textInput; }; threadEditName(): string { return firstLine( this.props.threadInfo.name ? this.props.threadInfo.name : '', ); } onPressEdit = () => { this.props.setNameEditValue(this.threadEditName()); }; onSubmit = () => { invariant( this.props.nameEditValue !== null && this.props.nameEditValue !== undefined, 'should be set', ); const name = firstLine(this.props.nameEditValue); if (name === this.threadEditName()) { this.props.setNameEditValue(null); return; } const editNamePromise = this.editName(name); const action = changeThreadSettingsActionTypes.started; const threadID = this.props.threadInfo.id; void this.props.dispatchActionPromise( changeThreadSettingsActionTypes, editNamePromise, { customKeyName: `${action}:${threadID}:name`, }, ); void editNamePromise.then(() => { this.props.setNameEditValue(null); }); }; async editName(newName: string): Promise { try { return await this.props.changeThreadSettings({ threadID: this.props.threadInfo.id, changes: { name: newName }, }); } catch (e) { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onErrorAcknowledged }], { cancelable: false }, ); throw e; } } onErrorAcknowledged = () => { this.props.setNameEditValue(this.threadEditName(), () => { invariant(this.textInput, 'textInput should be set'); this.textInput.focus(); }); }; } const ConnectedThreadSettingsName: React.ComponentType = React.memo(function ConnectedThreadSettingsName(props: BaseProps) { const styles = useStyles(unboundStyles); const colors = useColors(); const threadID = props.threadInfo.id; const loadingStatus = useSelector( createLoadingStatusSelector( changeThreadSettingsActionTypes, `${changeThreadSettingsActionTypes.started}:${threadID}:name`, ), ); const dispatchActionPromise = useDispatchActionPromise(); const callChangeThreadSettings = useChangeThreadSettings(); return ( ); }); export default ConnectedThreadSettingsName; diff --git a/native/chat/settings/thread-settings-promote-sidebar.react.js b/native/chat/settings/thread-settings-promote-sidebar.react.js index dcd38d739..13ee162a3 100644 --- a/native/chat/settings/thread-settings-promote-sidebar.react.js +++ b/native/chat/settings/thread-settings-promote-sidebar.react.js @@ -1,120 +1,120 @@ // @flow import * as React from 'react'; import { ActivityIndicator, Text, View } from 'react-native'; import { usePromoteSidebar } from 'lib/hooks/promote-sidebar.react.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import Button from '../../components/button.react.js'; import { type Colors, useColors, useStyles } from '../../themes/colors.js'; import type { ViewStyle } from '../../types/styles.js'; -import { UnknownErrorAlertDetails } from '../../utils/alert-messages.js'; +import { unknownErrorAlertDetails } from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; const unboundStyles = { button: { flexDirection: 'row', paddingHorizontal: 12, paddingVertical: 10, }, container: { backgroundColor: 'panelForeground', paddingHorizontal: 12, }, text: { color: 'panelForegroundSecondaryLabel', flex: 1, fontSize: 16, }, }; type BaseProps = { +threadInfo: ThreadInfo, +buttonStyle: ViewStyle, }; type Props = { ...BaseProps, +loadingStatus: LoadingStatus, +colors: Colors, +styles: $ReadOnly, +promoteSidebar: () => mixed, }; class ThreadSettingsPromoteSidebar extends React.PureComponent { onClick = () => { Alert.alert( 'Are you sure?', 'Promoting a thread to a channel cannot be undone.', [ { text: 'Cancel', style: 'cancel', }, { text: 'Yes', onPress: this.props.promoteSidebar, }, ], ); }; render(): React.Node { const { panelIosHighlightUnderlay, panelForegroundSecondaryLabel } = this.props.colors; const loadingIndicator = this.props.loadingStatus === 'loading' ? ( ) : null; return ( ); } } const onError = () => { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, undefined, { cancelable: true, }, ); }; const ConnectedThreadSettingsPromoteSidebar: React.ComponentType = React.memo(function ConnectedThreadSettingsPromoteSidebar( props: BaseProps, ) { const { threadInfo } = props; const colors = useColors(); const styles = useStyles(unboundStyles); const { onPromoteSidebar, loading } = usePromoteSidebar( threadInfo, onError, ); return ( ); }); export default ConnectedThreadSettingsPromoteSidebar; diff --git a/native/components/version-supported.react.js b/native/components/version-supported.react.js index c1b2e4310..18ffe705f 100644 --- a/native/components/version-supported.react.js +++ b/native/components/version-supported.react.js @@ -1,60 +1,60 @@ // @flow import * as React from 'react'; import { useLogOut, logOutActionTypes } from 'lib/actions/user-actions.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { commRustModule } from '../native-modules.js'; import { useSelector } from '../redux/redux-utils.js'; -import { AppOutOfDateAlertDetails } from '../utils/alert-messages.js'; +import { appOutOfDateAlertDetails } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; function VersionSupportedChecker(): React.Node { const hasRun = React.useRef(false); const loggedIn = useSelector(isLoggedIn); const dispatchActionPromise = useDispatchActionPromise(); const callLogOut = useLogOut(); const onUsernameAlertAcknowledged = React.useCallback(() => { if (loggedIn) { void dispatchActionPromise(logOutActionTypes, callLogOut()); } }, [callLogOut, dispatchActionPromise, loggedIn]); const checkVersionSupport = React.useCallback(async () => { try { const isVersionSupported = await commRustModule.versionSupported(); if (isVersionSupported) { return; } Alert.alert( - AppOutOfDateAlertDetails.title, - AppOutOfDateAlertDetails.message, + appOutOfDateAlertDetails.title, + appOutOfDateAlertDetails.message, [ { text: 'OK', onPress: onUsernameAlertAcknowledged, }, ], { cancelable: false }, ); } catch (error) { console.log('Error checking version:', error); } }, [onUsernameAlertAcknowledged]); React.useEffect(() => { if (hasRun.current) { return; } hasRun.current = true; void checkVersionSupport(); }, [checkVersionSupport]); return null; } export default VersionSupportedChecker; diff --git a/native/profile/default-notifications-preferences.react.js b/native/profile/default-notifications-preferences.react.js index f5356e859..866d8d439 100644 --- a/native/profile/default-notifications-preferences.react.js +++ b/native/profile/default-notifications-preferences.react.js @@ -1,214 +1,214 @@ // @flow import * as React from 'react'; import { View, Text, Platform } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; import { useSetUserSettings, setUserSettingsActionTypes, } from 'lib/actions/user-actions.js'; import { registerFetchKey } from 'lib/reducers/loading-reducer.js'; import { type UpdateUserSettingsRequest, type NotificationTypes, type DefaultNotificationPayload, notificationTypes, userSettingsTypes, } from 'lib/types/account-types.js'; import { useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import Action from '../components/action-row.react.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; -import { UnknownErrorAlertDetails } from '../utils/alert-messages.js'; +import { unknownErrorAlertDetails } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; const CheckIcon = () => ( ); type ProfileRowProps = { +content: string, +onPress: () => void, +danger?: boolean, +selected?: boolean, }; function NotificationRow(props: ProfileRowProps): React.Node { const { content, onPress, danger, selected } = props; return ( {selected ? : null} ); } const unboundStyles = { scrollView: { backgroundColor: 'panelBackground', }, scrollViewContentContainer: { paddingTop: 24, }, section: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, marginVertical: 2, }, icon: { lineHeight: Platform.OS === 'ios' ? 18 : 20, }, header: { color: 'panelBackgroundLabel', fontSize: 12, fontWeight: '400', paddingBottom: 3, paddingHorizontal: 24, }, }; type BaseProps = { +navigation: ProfileNavigationProp<'DefaultNotifications'>, +route: NavigationRoute<'DefaultNotifications'>, }; type Props = { ...BaseProps, +styles: $ReadOnly, +dispatchActionPromise: DispatchActionPromise, +changeNotificationSettings: ( notificationSettingsRequest: UpdateUserSettingsRequest, ) => Promise, +selectedDefaultNotification: NotificationTypes, }; class DefaultNotificationsPreferences extends React.PureComponent { async updatedDefaultNotifications( data: NotificationTypes, ): Promise { const { changeNotificationSettings } = this.props; try { await changeNotificationSettings({ name: userSettingsTypes.DEFAULT_NOTIFICATIONS, data, }); } catch (e) { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, [{ text: 'OK', onPress: () => {} }], { cancelable: false }, ); } return { [userSettingsTypes.DEFAULT_NOTIFICATIONS]: data, }; } selectNotificationSetting = (data: NotificationTypes) => { const { dispatchActionPromise } = this.props; void dispatchActionPromise( setUserSettingsActionTypes, this.updatedDefaultNotifications(data), ); }; selectAllNotifications = () => { this.selectNotificationSetting(notificationTypes.FOCUSED); }; selectBackgroundNotifications = () => { this.selectNotificationSetting(notificationTypes.BACKGROUND); }; selectNoneNotifications = () => { this.selectNotificationSetting(notificationTypes.BADGE_ONLY); }; render(): React.Node { const { styles, selectedDefaultNotification } = this.props; return ( NOTIFICATIONS ); } } registerFetchKey(setUserSettingsActionTypes); const ConnectedDefaultNotificationPreferences: React.ComponentType = React.memo(function ConnectedDefaultNotificationPreferences( props: BaseProps, ) { const styles = useStyles(unboundStyles); const dispatchActionPromise = useDispatchActionPromise(); const changeNotificationSettings = useSetUserSettings(); const defaultNotification = userSettingsTypes.DEFAULT_NOTIFICATIONS; const selectedDefaultNotification = useSelector( ({ currentUserInfo }) => { if ( currentUserInfo?.settings && currentUserInfo?.settings[defaultNotification] ) { return currentUserInfo?.settings[defaultNotification]; } return notificationTypes.FOCUSED; }, ); return ( ); }); export default ConnectedDefaultNotificationPreferences; diff --git a/native/profile/edit-password.react.js b/native/profile/edit-password.react.js index 0cec93c91..eb860e853 100644 --- a/native/profile/edit-password.react.js +++ b/native/profile/edit-password.react.js @@ -1,376 +1,376 @@ // @flow import { CommonActions } from '@react-navigation/native'; import invariant from 'invariant'; import * as React from 'react'; import { Text, View, TextInput as BaseTextInput, ActivityIndicator, } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; import { changeKeyserverUserPasswordActionTypes, changeKeyserverUserPassword, } from 'lib/actions/user-actions.js'; import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { PasswordUpdate } from 'lib/types/user-types.js'; import { useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import { setNativeCredentials } from '../account/native-credentials.js'; import Button from '../components/button.react.js'; import TextInput from '../components/text-input.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { type Colors, useColors, useStyles } from '../themes/colors.js'; -import { UnknownErrorAlertDetails } from '../utils/alert-messages.js'; +import { unknownErrorAlertDetails } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; const unboundStyles = { header: { color: 'panelBackgroundLabel', fontSize: 12, fontWeight: '400', paddingBottom: 3, paddingHorizontal: 24, }, hr: { backgroundColor: 'panelForegroundBorder', height: 1, marginHorizontal: 15, }, input: { color: 'panelForegroundLabel', flex: 1, fontFamily: 'Arial', fontSize: 16, paddingVertical: 0, borderBottomColor: 'transparent', }, row: { flexDirection: 'row', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 9, }, saveButton: { backgroundColor: 'vibrantGreenButton', borderRadius: 5, flex: 1, marginHorizontal: 24, marginVertical: 12, padding: 12, }, saveText: { color: 'white', fontSize: 18, textAlign: 'center', }, scrollView: { backgroundColor: 'panelBackground', }, scrollViewContentContainer: { paddingTop: 24, }, section: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, paddingVertical: 3, }, }; type BaseProps = { +navigation: ProfileNavigationProp<'EditPassword'>, +route: NavigationRoute<'EditPassword'>, }; type Props = { ...BaseProps, // Redux state +loadingStatus: LoadingStatus, +username: ?string, +colors: Colors, +styles: $ReadOnly, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +changeKeyserverUserPassword: ( passwordUpdate: PasswordUpdate, ) => Promise, }; type State = { +currentPassword: string, +newPassword: string, +confirmPassword: string, }; class EditPassword extends React.PureComponent { state: State = { currentPassword: '', newPassword: '', confirmPassword: '', }; mounted = false; currentPasswordInput: ?React.ElementRef; newPasswordInput: ?React.ElementRef; confirmPasswordInput: ?React.ElementRef; componentDidMount() { this.mounted = true; } componentWillUnmount() { this.mounted = false; } render(): React.Node { const buttonContent = this.props.loadingStatus === 'loading' ? ( ) : ( Save ); const { panelForegroundTertiaryLabel } = this.props.colors; return ( CURRENT PASSWORD NEW PASSWORD ); } onChangeCurrentPassword = (currentPassword: string) => { this.setState({ currentPassword }); }; currentPasswordRef = ( currentPasswordInput: ?React.ElementRef, ) => { this.currentPasswordInput = currentPasswordInput; }; focusCurrentPassword = () => { invariant(this.currentPasswordInput, 'currentPasswordInput should be set'); this.currentPasswordInput.focus(); }; onChangeNewPassword = (newPassword: string) => { this.setState({ newPassword }); }; newPasswordRef = ( newPasswordInput: ?React.ElementRef, ) => { this.newPasswordInput = newPasswordInput; }; focusNewPassword = () => { invariant(this.newPasswordInput, 'newPasswordInput should be set'); this.newPasswordInput.focus(); }; onChangeConfirmPassword = (confirmPassword: string) => { this.setState({ confirmPassword }); }; confirmPasswordRef = ( confirmPasswordInput: ?React.ElementRef, ) => { this.confirmPasswordInput = confirmPasswordInput; }; focusConfirmPassword = () => { invariant(this.confirmPasswordInput, 'confirmPasswordInput should be set'); this.confirmPasswordInput.focus(); }; goBackOnce() { this.props.navigation.dispatch(state => ({ ...CommonActions.goBack(), target: state.key, })); } submitPassword = () => { if (this.state.newPassword === '') { Alert.alert( 'Empty password', 'New password cannot be empty', [{ text: 'OK', onPress: this.onNewPasswordAlertAcknowledged }], { cancelable: false }, ); } else if (this.state.newPassword !== this.state.confirmPassword) { Alert.alert( 'Passwords don’t match', 'New password fields must contain the same password', [{ text: 'OK', onPress: this.onNewPasswordAlertAcknowledged }], { cancelable: false }, ); } else if (this.state.newPassword === this.state.currentPassword) { this.goBackOnce(); } else { void this.props.dispatchActionPromise( changeKeyserverUserPasswordActionTypes, this.savePassword(), ); } }; async savePassword() { const { username } = this.props; if (!username) { return; } try { await this.props.changeKeyserverUserPassword({ updatedFields: { password: this.state.newPassword, }, currentPassword: this.state.currentPassword, }); await setNativeCredentials({ username, password: this.state.newPassword, }); this.goBackOnce(); } catch (e) { if (e.message === 'invalid_credentials') { Alert.alert( 'Incorrect password', 'The current password you entered is incorrect', [{ text: 'OK', onPress: this.onCurrentPasswordAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } } } onNewPasswordAlertAcknowledged = () => { this.setState( { newPassword: '', confirmPassword: '' }, this.focusNewPassword, ); }; onCurrentPasswordAlertAcknowledged = () => { this.setState({ currentPassword: '' }, this.focusCurrentPassword); }; onUnknownErrorAlertAcknowledged = () => { this.setState( { currentPassword: '', newPassword: '', confirmPassword: '' }, this.focusCurrentPassword, ); }; } const loadingStatusSelector = createLoadingStatusSelector( changeKeyserverUserPasswordActionTypes, ); const ConnectedEditPassword: React.ComponentType = React.memo(function ConnectedEditPassword(props: BaseProps) { const loadingStatus = useSelector(loadingStatusSelector); const username = useSelector(state => { if (state.currentUserInfo && !state.currentUserInfo.anonymous) { return state.currentUserInfo.username; } return undefined; }); const colors = useColors(); const styles = useStyles(unboundStyles); const dispatchActionPromise = useDispatchActionPromise(); const callChangeKeyserverUserPassword = useLegacyAshoatKeyserverCall( changeKeyserverUserPassword, ); return ( ); }); export default ConnectedEditPassword; diff --git a/native/profile/farcaster-account-settings.react.js b/native/profile/farcaster-account-settings.react.js index 1a507b5d1..427bf2664 100644 --- a/native/profile/farcaster-account-settings.react.js +++ b/native/profile/farcaster-account-settings.react.js @@ -1,144 +1,144 @@ // @flow import * as React from 'react'; import { View, Alert } from 'react-native'; import { useCurrentUserFID, useUnlinkFID } from 'lib/utils/farcaster-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import RegistrationButton from '../account/registration/registration-button.react.js'; import FarcasterPrompt from '../components/farcaster-prompt.react.js'; import FarcasterWebView from '../components/farcaster-web-view.react.js'; import type { FarcasterWebViewState } from '../components/farcaster-web-view.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useStyles } from '../themes/colors.js'; -import { UnknownErrorAlertDetails } from '../utils/alert-messages.js'; +import { unknownErrorAlertDetails } from '../utils/alert-messages.js'; import { useTryLinkFID } from '../utils/farcaster-utils.js'; type Props = { +navigation: ProfileNavigationProp<'FarcasterAccountSettings'>, +route: NavigationRoute<'FarcasterAccountSettings'>, }; // eslint-disable-next-line no-unused-vars function FarcasterAccountSettings(props: Props): React.Node { const fid = useCurrentUserFID(); const styles = useStyles(unboundStyles); const [isLoadingUnlinkFID, setIsLoadingUnlinkFID] = React.useState(false); const unlinkFID = useUnlinkFID(); const onPressDisconnect = React.useCallback(async () => { setIsLoadingUnlinkFID(true); try { await unlinkFID(); } catch { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, ); } finally { setIsLoadingUnlinkFID(false); } }, [unlinkFID]); const [webViewState, setWebViewState] = React.useState('closed'); const [isLoadingLinkFID, setIsLoadingLinkFID] = React.useState(false); const tryLinkFID = useTryLinkFID(); const onSuccess = React.useCallback( async (newFID: string) => { setWebViewState('closed'); try { await tryLinkFID(newFID); } finally { setIsLoadingLinkFID(false); } }, [tryLinkFID], ); const onPressConnectFarcaster = React.useCallback(() => { setIsLoadingLinkFID(true); setWebViewState('opening'); }, []); const disconnectButtonVariant = isLoadingUnlinkFID ? 'loading' : 'outline'; const connectButtonVariant = isLoadingLinkFID ? 'loading' : 'enabled'; const button = React.useMemo(() => { if (fid) { return ( ); } return ( ); }, [ connectButtonVariant, disconnectButtonVariant, fid, onPressConnectFarcaster, onPressDisconnect, ]); const farcasterPromptTextType = fid ? 'disconnect' : 'optional'; const farcasterAccountSettings = React.useMemo( () => ( {button} ), [ button, farcasterPromptTextType, onSuccess, styles.buttonContainer, styles.connectContainer, styles.promptContainer, webViewState, ], ); return farcasterAccountSettings; } const unboundStyles = { connectContainer: { flex: 1, backgroundColor: 'panelBackground', paddingBottom: 16, }, promptContainer: { flex: 1, padding: 16, justifyContent: 'space-between', }, buttonContainer: { marginVertical: 8, marginHorizontal: 16, }, }; export default FarcasterAccountSettings; diff --git a/native/profile/relationship-list-item.react.js b/native/profile/relationship-list-item.react.js index 00dc04b72..c2d2851c4 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 { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.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 RelationshipRequest, } 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 { 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: 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], }); } catch (e) { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + 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/native/profile/relationship-list.react.js b/native/profile/relationship-list.react.js index bbcf365a1..f3b4e10da 100644 --- a/native/profile/relationship-list.react.js +++ b/native/profile/relationship-list.react.js @@ -1,489 +1,489 @@ // @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 { useENSNames } from 'lib/hooks/ens-cache.js'; import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.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 { 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 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, }); setCurrentTags([]); setSearchInputText(''); return result; } catch (e) { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, [{ text: 'OK', onPress: onUnknownErrorAlertAcknowledged }], { cancelable: true, onDismiss: onUnknownErrorAlertAcknowledged }, ); throw e; } }, [ routeName, currentTags, callUpdateRelationships, 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 683dad9cc..0c3b03348 100644 --- a/native/profile/user-relationship-tooltip-modal.react.js +++ b/native/profile/user-relationship-tooltip-modal.react.js @@ -1,172 +1,172 @@ // @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 { 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 { 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 dispatchActionPromise = useDispatchActionPromise(); const userText = stringForUser(input.relativeUserInfo); return React.useCallback(() => { const callRemoveRelationships = async () => { try { return await boundRemoveRelationships({ action: input.action, userIDs: [input.relativeUserInfo.id], }); } catch (e) { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + 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]); } 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/native/redux/redux-setup.js b/native/redux/redux-setup.js index 7a2e4b0fd..3ab413b49 100644 --- a/native/redux/redux-setup.js +++ b/native/redux/redux-setup.js @@ -1,450 +1,450 @@ // @flow import { AppState as NativeAppState, Alert } from 'react-native'; import { createStore, applyMiddleware, type Store, compose } from 'redux'; import { persistStore, persistReducer } from 'redux-persist'; import thunk from 'redux-thunk'; import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; import { legacySiweAuthActionTypes } from 'lib/actions/siwe-actions.js'; import { logOutActionTypes, deleteAccountActionTypes, legacyLogInActionTypes, keyserverAuthActionTypes, deleteKeyserverAccountActionTypes, } from 'lib/actions/user-actions.js'; import { setNewSessionActionType } from 'lib/keyserver-conn/keyserver-conn-types.js'; import type { ThreadStoreOperation } from 'lib/ops/thread-store-ops.js'; import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js'; import { queueDBOps } from 'lib/reducers/db-ops-reducer.js'; import { reduceLoadingStatuses } from 'lib/reducers/loading-reducer.js'; import baseReducer from 'lib/reducers/master-reducer.js'; import { reduceCurrentUserInfo } from 'lib/reducers/user-reducer.js'; import { shouldClearData } from 'lib/shared/data-utils.js'; import { invalidSessionDowngrade, invalidSessionRecovery, identityInvalidSessionDowngrade, } from 'lib/shared/session-utils.js'; import { isStaff } from 'lib/shared/staff-utils.js'; import type { Dispatch, BaseAction } from 'lib/types/redux-types.js'; import { rehydrateActionType } from 'lib/types/redux-types.js'; import type { SetSessionPayload } from 'lib/types/session-types.js'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js'; import { resetUserSpecificState } from 'lib/utils/reducers-utils.js'; import { updateDimensionsActiveType, updateConnectivityActiveType, updateDeviceCameraInfoActionType, updateDeviceOrientationActionType, backgroundActionTypes, setReduxStateActionType, setStoreLoadedActionType, type Action, setLocalSettingsActionType, } from './action-types.js'; import { setAccessTokenActionType } from './action-types.js'; import { defaultState } from './default-state.js'; import { remoteReduxDevServerConfig } from './dev-tools.js'; import { persistConfig, setPersistor } from './persist.js'; import { onStateDifference } from './redux-debug-utils.js'; import type { AppState } from './state-types.js'; import { nonUserSpecificFieldsNative } from './state-types.js'; import { getGlobalNavContext } from '../navigation/icky-global.js'; import { activeMessageListSelector } from '../navigation/nav-selectors.js'; import reactotron from '../reactotron.js'; -import { AppOutOfDateAlertDetails } from '../utils/alert-messages.js'; +import { appOutOfDateAlertDetails } from '../utils/alert-messages.js'; import { isStaffRelease } from '../utils/staff-utils.js'; import { getDevServerHostname } from '../utils/url-utils.js'; function reducer(state: AppState = defaultState, inputAction: Action) { let action = inputAction; if (action.type === setReduxStateActionType) { return action.payload.state; } // We want to alert staff/developers if there's a difference between the keys // we expect to see REHYDRATED and the keys that are actually REHYDRATED. // Context: https://linear.app/comm/issue/ENG-2127/ if ( action.type === rehydrateActionType && (__DEV__ || isStaffRelease || (state.currentUserInfo && state.currentUserInfo.id && isStaff(state.currentUserInfo.id))) ) { // 1. Construct set of keys expected to be REHYDRATED const defaultKeys: $ReadOnlyArray = Object.keys(defaultState); const expectedKeys = defaultKeys.filter( each => !persistConfig.blacklist.includes(each), ); const expectedKeysSet = new Set(expectedKeys); // 2. Construct set of keys actually REHYDRATED const rehydratedKeys: $ReadOnlyArray = Object.keys( action.payload ?? {}, ); const rehydratedKeysSet = new Set(rehydratedKeys); // 3. Determine the difference between the two sets const expectedKeysNotRehydrated = expectedKeys.filter( each => !rehydratedKeysSet.has(each), ); const rehydratedKeysNotExpected = rehydratedKeys.filter( each => !expectedKeysSet.has(each), ); // 4. Display alerts with the differences between the two sets if (expectedKeysNotRehydrated.length > 0) { Alert.alert( `EXPECTED KEYS NOT REHYDRATED: ${JSON.stringify( expectedKeysNotRehydrated, )}`, ); } if (rehydratedKeysNotExpected.length > 0) { Alert.alert( `REHYDRATED KEYS NOT EXPECTED: ${JSON.stringify( rehydratedKeysNotExpected, )}`, ); } } if ( (action.type === logOutActionTypes.success || action.type === deleteAccountActionTypes.success) && identityInvalidSessionDowngrade( state, action.payload.currentUserInfo, action.payload.preRequestUserState, ) ) { return { ...state, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } if ( action.type === setNewSessionActionType && invalidSessionDowngrade( state, action.payload.sessionChange.currentUserInfo, action.payload.preRequestUserState, action.payload.keyserverID, ) ) { return { ...state, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } else if (action.type === deleteKeyserverAccountActionTypes.success) { const { currentUserInfo, preRequestUserState } = action.payload; const newKeyserverIDs = []; for (const keyserverID of action.payload.keyserverIDs) { if ( invalidSessionDowngrade( state, currentUserInfo, preRequestUserState, keyserverID, ) ) { continue; } newKeyserverIDs.push(keyserverID); } if (newKeyserverIDs.length === 0) { return { ...state, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } action = { ...action, payload: { ...action.payload, keyserverIDs: newKeyserverIDs, }, }; } if ( (action.type === setNewSessionActionType && action.payload.sessionChange.currentUserInfo && invalidSessionRecovery( state, action.payload.preRequestUserState?.currentUserInfo, action.payload.authActionSource, )) || ((action.type === legacyLogInActionTypes.success || action.type === legacySiweAuthActionTypes.success) && invalidSessionRecovery( state, action.payload.preRequestUserInfo, action.payload.authActionSource, )) || (action.type === keyserverAuthActionTypes.success && invalidSessionRecovery( state, action.payload.preRequestUserInfo, action.payload.authActionSource, )) ) { return state; } if (action.type === updateDimensionsActiveType) { return { ...state, dimensions: { ...state.dimensions, ...action.payload, }, }; } else if (action.type === updateConnectivityActiveType) { return { ...state, connectivity: action.payload, }; } else if (action.type === updateDeviceCameraInfoActionType) { return { ...state, deviceCameraInfo: { ...state.deviceCameraInfo, ...action.payload, }, }; } else if (action.type === updateDeviceOrientationActionType) { return { ...state, deviceOrientation: action.payload, }; } else if (action.type === setLocalSettingsActionType) { return { ...state, localSettings: { ...state.localSettings, ...action.payload }, }; } else if (action.type === setAccessTokenActionType) { return { ...state, commServicesAccessToken: action.payload }; } if (action.type === setNewSessionActionType) { sessionInvalidationAlert(action.payload); } if (action.type === setStoreLoadedActionType) { return { ...state, storeLoaded: true, }; } if (action.type === setClientDBStoreActionType) { state = { ...state, storeLoaded: true, }; const currentLoggedInUserID = state.currentUserInfo?.anonymous ? undefined : state.currentUserInfo?.id; const actionCurrentLoggedInUserID = action.payload.currentUserID; if ( !currentLoggedInUserID || !actionCurrentLoggedInUserID || actionCurrentLoggedInUserID !== currentLoggedInUserID ) { // If user is logged out now, was logged out at the time action was // dispatched or their ID changed between action dispatch and a // call to reducer we ignore the SQLite data since it is not valid return state; } } // We're calling this reducer twice: here and in the baseReducer. This call // is only used to determine the new current user ID. We don't want to use // the remaining part of the current user info, because it is possible that // the reducer returned a modified ID without cleared remaining parts of // the current user info - this would be a bug, but we want to be extra // careful when clearing the state. // When newCurrentUserInfo has the same ID as state.currentUserInfo the state // won't be cleared and the current user info determined in baseReducer will // be equal to the newCurrentUserInfo. // When newCurrentUserInfo has different ID than state.currentUserInfo, we // reset the state and pass it to the baseReducer. Then, in baseReducer, // reduceCurrentUserInfo acts on the cleared state and may return a different // result than newCurrentUserInfo. // Overall, the solution is a little wasteful, but makes us sure that we never // keep the info of the user when the current user ID changes. const newCurrentUserInfo = reduceCurrentUserInfo( state.currentUserInfo, action, ); if (shouldClearData(state.currentUserInfo?.id, newCurrentUserInfo?.id)) { state = resetUserSpecificState( state, defaultState, nonUserSpecificFieldsNative, ); } const baseReducerResult = baseReducer( state, (action: BaseAction), onStateDifference, ); state = baseReducerResult.state; const { storeOperations } = baseReducerResult; const fixUnreadActiveThreadResult = fixUnreadActiveThread(state, action); state = fixUnreadActiveThreadResult.state; const threadStoreOperationsWithUnreadFix = [ ...(storeOperations.threadStoreOperations ?? []), ...fixUnreadActiveThreadResult.threadStoreOperations, ]; const ops = { ...storeOperations, threadStoreOperations: threadStoreOperationsWithUnreadFix, }; state = { ...state, dbOpsStore: queueDBOps(state.dbOpsStore, action.messageSourceMetadata, ops), }; return state; } function sessionInvalidationAlert(payload: SetSessionPayload) { if ( !payload.sessionChange.cookieInvalidated || !payload.preRequestUserState || !payload.preRequestUserState.currentUserInfo || payload.preRequestUserState.currentUserInfo.anonymous ) { return; } if (payload.error === 'client_version_unsupported') { Alert.alert( - AppOutOfDateAlertDetails.title, - AppOutOfDateAlertDetails.message, + appOutOfDateAlertDetails.title, + appOutOfDateAlertDetails.message, [{ text: 'OK' }], { cancelable: true, }, ); } else { Alert.alert( 'Session invalidated', 'We’re sorry, but your session was invalidated by the server. ' + 'Please log in again.', [{ text: 'OK' }], { cancelable: true }, ); } } // Makes sure a currently focused thread is never unread. Note that we consider // a backgrounded NativeAppState to actually be active if it last changed to // inactive more than 10 seconds ago. This is because there is a delay when // NativeAppState is updating in response to a foreground, and actions don't get // processed more than 10 seconds after a backgrounding anyways. However we // don't consider this for action types that can be expected to happen while the // app is backgrounded. type FixUnreadActiveThreadResult = { +state: AppState, +threadStoreOperations: $ReadOnlyArray, }; function fixUnreadActiveThread( state: AppState, action: *, ): FixUnreadActiveThreadResult { const navContext = getGlobalNavContext(); const activeThread = activeMessageListSelector(navContext); if ( !activeThread || !state.threadStore.threadInfos[activeThread]?.currentUser.unread || (NativeAppState.currentState !== 'active' && (appLastBecameInactive + 10000 >= Date.now() || backgroundActionTypes.has(action.type))) ) { return { state, threadStoreOperations: [] }; } const activeThreadInfo = state.threadStore.threadInfos[activeThread]; const updatedActiveThreadInfo = { ...activeThreadInfo, currentUser: { ...activeThreadInfo.currentUser, unread: false, }, }; const threadStoreOperations = [ { type: 'replace', payload: { id: activeThread, threadInfo: updatedActiveThreadInfo, }, }, ]; const updatedThreadStore = threadStoreOpsHandlers.processStoreOperations( state.threadStore, threadStoreOperations, ); return { state: { ...state, threadStore: updatedThreadStore }, threadStoreOperations, }; } let appLastBecameInactive = 0; function appBecameInactive() { appLastBecameInactive = Date.now(); } const middleware = applyMiddleware(thunk, reduxLoggerMiddleware); let composeFunc = compose; if (__DEV__ && global.HermesInternal) { const { composeWithDevTools } = require('remote-redux-devtools/src/index.js'); composeFunc = composeWithDevTools({ name: 'Redux', hostname: getDevServerHostname(), ...remoteReduxDevServerConfig, }); } else if (global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { composeFunc = global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'Redux', }); } let enhancers; if (reactotron) { enhancers = composeFunc(middleware, reactotron.createEnhancer()); } else { enhancers = composeFunc(middleware); } const store: Store = createStore( persistReducer(persistConfig, reducer), defaultState, enhancers, ); const persistor = persistStore(store); setPersistor(persistor); const unsafeDispatch: any = store.dispatch; const dispatch: Dispatch = unsafeDispatch; export { store, dispatch, appBecameInactive }; diff --git a/native/user-profile/user-profile-relationship-button.react.js b/native/user-profile/user-profile-relationship-button.react.js index 135aaac77..f27371cb2 100644 --- a/native/user-profile/user-profile-relationship-button.react.js +++ b/native/user-profile/user-profile-relationship-button.react.js @@ -1,158 +1,158 @@ // @flow import * as React from 'react'; import { Text, View } from 'react-native'; import { useRelationshipPrompt } from 'lib/hooks/relationship-prompt.js'; import type { SetState } from 'lib/types/hook-types.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import { userRelationshipStatus } from 'lib/types/relationship-types.js'; import type { UserInfo } from 'lib/types/user-types'; import { userProfileActionButtonHeight } from './user-profile-constants.js'; import RelationshipButton from '../components/relationship-button.react.js'; import { useStyles } from '../themes/colors.js'; -import { UnknownErrorAlertDetails } from '../utils/alert-messages.js'; +import { unknownErrorAlertDetails } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; const onErrorCallback = () => { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, [{ text: 'OK' }], ); }; type Props = { +threadInfo: ThreadInfo, +pendingPersonalThreadUserInfo?: UserInfo, +setUserProfileRelationshipButtonHeight: SetState, }; function UserProfileRelationshipButton(props: Props): React.Node { const { threadInfo, pendingPersonalThreadUserInfo, setUserProfileRelationshipButtonHeight, } = props; const { otherUserInfo, callbacks: { friendUser, unfriendUser }, } = useRelationshipPrompt( threadInfo, onErrorCallback, pendingPersonalThreadUserInfo, ); React.useLayoutEffect(() => { if ( !otherUserInfo || otherUserInfo.relationshipStatus === userRelationshipStatus.FRIEND ) { setUserProfileRelationshipButtonHeight(0); } else if ( otherUserInfo?.relationshipStatus === userRelationshipStatus.REQUEST_RECEIVED ) { const incomingFriendRequestButtonsContainerHeight = 88; setUserProfileRelationshipButtonHeight( incomingFriendRequestButtonsContainerHeight, ); } else { setUserProfileRelationshipButtonHeight(userProfileActionButtonHeight); } }, [ otherUserInfo, otherUserInfo?.relationshipStatus, setUserProfileRelationshipButtonHeight, ]); const styles = useStyles(unboundStyles); const userProfileRelationshipButton = React.useMemo(() => { if ( !otherUserInfo || !otherUserInfo.username || otherUserInfo.relationshipStatus === userRelationshipStatus.FRIEND ) { return null; } if ( otherUserInfo.relationshipStatus === userRelationshipStatus.REQUEST_RECEIVED ) { return ( Incoming friend request ); } if ( otherUserInfo.relationshipStatus === userRelationshipStatus.REQUEST_SENT ) { return ( ); } return ( ); }, [ friendUser, otherUserInfo, styles.acceptFriendRequestButtonContainer, styles.incomingFriendRequestButtonsContainer, styles.incomingFriendRequestContainer, styles.incomingFriendRequestLabel, styles.rejectFriendRequestButtonContainer, styles.singleButtonContainer, unfriendUser, ]); return userProfileRelationshipButton; } const unboundStyles = { singleButtonContainer: { marginTop: 16, }, incomingFriendRequestContainer: { marginTop: 24, }, incomingFriendRequestLabel: { color: 'modalForegroundLabel', }, incomingFriendRequestButtonsContainer: { flexDirection: 'row', marginTop: 8, }, acceptFriendRequestButtonContainer: { flex: 1, marginRight: 4, }, rejectFriendRequestButtonContainer: { flex: 1, marginLeft: 4, }, }; export default UserProfileRelationshipButton; diff --git a/native/utils/alert-messages.js b/native/utils/alert-messages.js index ef1f60e54..7a3b6895e 100644 --- a/native/utils/alert-messages.js +++ b/native/utils/alert-messages.js @@ -1,60 +1,60 @@ // @flow import { Platform } from 'react-native'; export type AlertDetails = { +title: string, +message: string, }; const platformStore: string = Platform.select({ ios: 'App Store', android: 'Play Store', }); -const AppOutOfDateAlertDetails: AlertDetails = { +const appOutOfDateAlertDetails: AlertDetails = { title: 'App out of date', message: 'Your app version is pretty old, and the server doesn’t know how ' + `to speak to it anymore. Please use the ${platformStore} to update!`, }; -const UsernameReservedAlertDetails: AlertDetails = { +const usernameReservedAlertDetails: AlertDetails = { title: 'Username reserved', message: 'This username is currently reserved. Please contact support@' + 'comm.app if you would like to claim this account.', }; -const UsernameTakenAlertDetails: AlertDetails = { +const usernameTakenAlertDetails: AlertDetails = { title: 'Username taken', message: 'An account with that username already exists', }; -const UserNotFoundAlertDetails: AlertDetails = { +const userNotFoundAlertDetails: AlertDetails = { title: 'Incorrect username or password', message: "Either that user doesn't exist, or the password is incorrect", }; -const UnknownErrorAlertDetails: AlertDetails = { +const unknownErrorAlertDetails: AlertDetails = { title: 'Unknown error', message: 'Uhh... try again?', }; const getFarcasterAccountAlreadyLinkedAlertDetails = ( commUsername: ?string, ): AlertDetails => ({ title: 'Farcaster account already linked', message: `That Farcaster account is already linked to ${ commUsername ? commUsername : 'another account' }`, }); export { - AppOutOfDateAlertDetails, - UsernameReservedAlertDetails, - UsernameTakenAlertDetails, - UserNotFoundAlertDetails, - UnknownErrorAlertDetails, + appOutOfDateAlertDetails, + usernameReservedAlertDetails, + usernameTakenAlertDetails, + userNotFoundAlertDetails, + unknownErrorAlertDetails, getFarcasterAccountAlreadyLinkedAlertDetails, }; diff --git a/native/utils/farcaster-utils.js b/native/utils/farcaster-utils.js index b32933397..c68de939f 100644 --- a/native/utils/farcaster-utils.js +++ b/native/utils/farcaster-utils.js @@ -1,41 +1,41 @@ // @flow import * as React from 'react'; import { Alert } from 'react-native'; import { getMessageForException } from 'lib/utils/errors.js'; import { useLinkFID } from 'lib/utils/farcaster-utils.js'; import { getFarcasterAccountAlreadyLinkedAlertDetails, - UnknownErrorAlertDetails, + unknownErrorAlertDetails, } from './alert-messages.js'; function useTryLinkFID(): (newFID: string) => Promise { const linkFID = useLinkFID(); return React.useCallback( async (newFID: string) => { try { await linkFID(newFID); } catch (e) { if ( getMessageForException(e) === 'farcaster ID already associated with different user' ) { const { title, message } = getFarcasterAccountAlreadyLinkedAlertDetails(); Alert.alert(title, message); } else { Alert.alert( - UnknownErrorAlertDetails.title, - UnknownErrorAlertDetails.message, + unknownErrorAlertDetails.title, + unknownErrorAlertDetails.message, ); } } }, [linkFID], ); } export { useTryLinkFID };