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 };