diff --git a/native/account/registration/avatar-selection.react.js b/native/account/registration/avatar-selection.react.js
index 9b802bef6..3dd9431a1 100644
--- a/native/account/registration/avatar-selection.react.js
+++ b/native/account/registration/avatar-selection.react.js
@@ -1,168 +1,187 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { Text, View } from 'react-native';
import type {
UpdateUserAvatarRequest,
ClientAvatar,
} from 'lib/types/avatar-types.js';
import type { NativeMediaSelection } from 'lib/types/media-types.js';
import type { SIWEResult } from 'lib/types/siwe-types.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 type { RegistrationNavigationProp } from './registration-navigator.react.js';
import type { CoolOrNerdMode } from './registration-types.js';
import {
EditUserAvatarContext,
type UserAvatarSelection,
} from '../../avatars/edit-user-avatar-provider.react.js';
import EditUserAvatar from '../../avatars/edit-user-avatar.react.js';
import type { NavigationRoute } from '../../navigation/route-names.js';
import { useStyles } from '../../themes/colors.js';
type EthereumAccountSelections = {
+accountType: 'ethereum',
...SIWEResult,
+ +avatarURI: ?string,
};
type UsernameAccountSelections = {
+accountType: 'username',
+username: string,
+password: string,
};
export type AvatarSelectionParams = {
+userSelections: {
+coolOrNerdMode: CoolOrNerdMode,
+keyserverUsername: string,
+accountSelections: EthereumAccountSelections | UsernameAccountSelections,
},
};
type AvatarData =
| {
+needsUpload: true,
+mediaSelection: NativeMediaSelection,
+clientAvatar: ClientAvatar,
}
| {
+needsUpload: false,
+updateUserAvatarRequest: UpdateUserAvatarRequest,
+clientAvatar: ClientAvatar,
};
+const ensDefaultSelection = {
+ needsUpload: false,
+ updateUserAvatarRequest: { type: 'ens' },
+ clientAvatar: { type: 'ens' },
+};
+
type Props = {
+navigation: RegistrationNavigationProp<'AvatarSelection'>,
+route: NavigationRoute<'AvatarSelection'>,
};
function AvatarSelection(props: Props): React.Node {
const { userSelections } = props.route.params;
const { accountSelections } = userSelections;
const username =
accountSelections.accountType === 'username'
? accountSelections.username
: accountSelections.address;
const editUserAvatarContext = React.useContext(EditUserAvatarContext);
invariant(editUserAvatarContext, 'editUserAvatarContext should be set');
const { setRegistrationMode } = editUserAvatarContext;
- const [avatarData, setAvatarData] = React.useState();
+ const prefetchedAvatarURI =
+ accountSelections.accountType === 'ethereum'
+ ? accountSelections.avatarURI
+ : undefined;
+
+ const [avatarData, setAvatarData] = React.useState(
+ prefetchedAvatarURI ? ensDefaultSelection : undefined,
+ );
+
const setClientAvatarFromSelection = React.useCallback(
(selection: UserAvatarSelection) => {
if (selection.needsUpload) {
setAvatarData({
...selection,
clientAvatar: {
type: 'image',
uri: selection.mediaSelection.uri,
},
});
} else if (selection.updateUserAvatarRequest.type !== 'remove') {
const clientRequest = selection.updateUserAvatarRequest;
invariant(
clientRequest.type !== 'image',
'image avatars need to be uploaded',
);
setAvatarData({
...selection,
clientAvatar: clientRequest,
});
} else {
setAvatarData(undefined);
}
},
[],
);
React.useEffect(() => {
setRegistrationMode({
registrationMode: 'on',
successCallback: setClientAvatarFromSelection,
});
return () => {
setRegistrationMode({ registrationMode: 'off' });
};
}, [setRegistrationMode, setClientAvatarFromSelection]);
const onProceed = React.useCallback(() => {}, []);
const clientAvatar = avatarData?.clientAvatar;
const userInfoOverride = React.useMemo(
() => ({
username,
avatar: clientAvatar,
}),
[username, clientAvatar],
);
const styles = useStyles(unboundStyles);
return (
Pick an avatar
-
+
);
}
const unboundStyles = {
scrollViewContentContainer: {
paddingHorizontal: 0,
},
header: {
fontSize: 24,
color: 'panelForegroundLabel',
paddingBottom: 16,
paddingHorizontal: 16,
},
stagedAvatarSection: {
marginTop: 16,
backgroundColor: 'panelForeground',
paddingVertical: 24,
alignItems: 'center',
},
editUserAvatar: {
alignItems: 'center',
justifyContent: 'center',
},
};
export default AvatarSelection;
diff --git a/native/account/registration/connect-ethereum.react.js b/native/account/registration/connect-ethereum.react.js
index db588c355..9ba06891f 100644
--- a/native/account/registration/connect-ethereum.react.js
+++ b/native/account/registration/connect-ethereum.react.js
@@ -1,253 +1,276 @@
// @flow
import * as React from 'react';
import { Text, View } from 'react-native';
import {
exactSearchUser,
exactSearchUserActionTypes,
} from 'lib/actions/user-actions.js';
+import { ENSCacheContext } from 'lib/components/ens-cache-provider.react.js';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
import type { SIWEResult } from 'lib/types/siwe-types.js';
import {
useServerCall,
useDispatchActionPromise,
} from 'lib/utils/action-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 type { RegistrationNavigationProp } from './registration-navigator.react.js';
import type { CoolOrNerdMode } from './registration-types.js';
import {
type NavigationRoute,
ExistingEthereumAccountRouteName,
UsernameSelectionRouteName,
AvatarSelectionRouteName,
} from '../../navigation/route-names.js';
import { useSelector } from '../../redux/redux-utils.js';
import { useStyles } from '../../themes/colors.js';
import EthereumLogoDark from '../../vectors/ethereum-logo-dark.react.js';
import SIWEPanel from '../siwe-panel.react.js';
const exactSearchUserLoadingStatusSelector = createLoadingStatusSelector(
exactSearchUserActionTypes,
);
export type ConnectEthereumParams = {
+userSelections: {
+coolOrNerdMode: CoolOrNerdMode,
+keyserverUsername: string,
},
};
type PanelState = 'closed' | 'opening' | 'open' | 'closing';
type Props = {
+navigation: RegistrationNavigationProp<'ConnectEthereum'>,
+route: NavigationRoute<'ConnectEthereum'>,
};
function ConnectEthereum(props: Props): React.Node {
const { params } = props.route;
const { userSelections } = props.route.params;
const isNerdMode = userSelections.coolOrNerdMode === 'nerd';
const styles = useStyles(unboundStyles);
let body;
if (!isNerdMode) {
body = (
Connecting your Ethereum wallet allows you to use your ENS name and
avatar in the app. You’ll also be able to log in with your wallet
instead of a password.
);
} else {
body = (
<>
Connecting your Ethereum wallet has three benefits:
{'1. '}
Your peers will be able to cryptographically verify that your Comm
account is associated with your Ethereum wallet.
{'2. '}
You’ll be able to use your ENS name and avatar in the app.
{'3. '}
You can choose to skip setting a password, and to log in with your
Ethereum wallet instead.
>
);
}
const [panelState, setPanelState] = React.useState('closed');
const openPanel = React.useCallback(() => {
setPanelState('opening');
}, []);
const onPanelClosed = React.useCallback(() => {
setPanelState('closed');
}, []);
const onPanelClosing = React.useCallback(() => {
setPanelState('closing');
}, []);
const siwePanelSetLoading = React.useCallback(
(loading: boolean) => {
if (panelState === 'closing' || panelState === 'closed') {
return;
}
setPanelState(loading ? 'opening' : 'open');
},
[panelState],
);
const { navigate } = props.navigation;
const onSkip = React.useCallback(() => {
navigate<'UsernameSelection'>({
name: UsernameSelectionRouteName,
params,
});
}, [navigate, params]);
const exactSearchUserCall = useServerCall(exactSearchUser);
const dispatchActionPromise = useDispatchActionPromise();
+ const cacheContext = React.useContext(ENSCacheContext);
+ const { ensCache } = cacheContext;
+
const onSuccessfulWalletSignature = React.useCallback(
async (result: SIWEResult) => {
const searchPromise = exactSearchUserCall(result.address);
dispatchActionPromise(exactSearchUserActionTypes, searchPromise);
+
+ // We want to figure out if the user has an ENS avatar now
+ // so that we can default to the ENS avatar in AvatarSelection
+ const avatarURIPromise = (async () => {
+ if (!ensCache) {
+ return null;
+ }
+ return await ensCache.getAvatarURIForAddress(result.address);
+ })();
+
const { userInfo } = await searchPromise;
if (userInfo) {
navigate<'ExistingEthereumAccount'>({
name: ExistingEthereumAccountRouteName,
params: result,
});
return;
}
+ const avatarURI = await avatarURIPromise;
+
const newUserSelections = {
...userSelections,
accountSelections: {
accountType: 'ethereum',
...result,
+ avatarURI,
},
};
navigate<'AvatarSelection'>({
name: AvatarSelectionRouteName,
params: { userSelections: newUserSelections },
});
},
- [userSelections, exactSearchUserCall, dispatchActionPromise, navigate],
+ [
+ userSelections,
+ exactSearchUserCall,
+ dispatchActionPromise,
+ navigate,
+ ensCache,
+ ],
);
let siwePanel;
if (panelState !== 'closed') {
siwePanel = (
);
}
const exactSearchUserCallLoading = useSelector(
state => exactSearchUserLoadingStatusSelector(state) === 'loading',
);
const connectButtonVariant =
exactSearchUserCallLoading || panelState === 'opening'
? 'loading'
: 'enabled';
return (
<>
Do you want to connect an Ethereum wallet?
{body}
{siwePanel}
>
);
}
const unboundStyles = {
scrollViewContentContainer: {
flexGrow: 1,
},
header: {
fontSize: 24,
color: 'panelForegroundLabel',
paddingBottom: 16,
},
body: {
fontFamily: 'Arial',
fontSize: 15,
lineHeight: 20,
color: 'panelForegroundSecondaryLabel',
paddingBottom: 16,
},
ethereumLogoContainer: {
flexGrow: 1,
alignItems: 'center',
justifyContent: 'center',
},
list: {
paddingBottom: 16,
},
listItem: {
flexDirection: 'row',
},
listItemNumber: {
fontFamily: 'Arial',
fontWeight: 'bold',
fontSize: 15,
lineHeight: 20,
color: 'panelForegroundSecondaryLabel',
},
listItemContent: {
fontFamily: 'Arial',
flexShrink: 1,
fontSize: 15,
lineHeight: 20,
color: 'panelForegroundSecondaryLabel',
},
};
export default ConnectEthereum;
diff --git a/native/avatars/edit-user-avatar.react.js b/native/avatars/edit-user-avatar.react.js
index e2f535f69..52e009d86 100644
--- a/native/avatars/edit-user-avatar.react.js
+++ b/native/avatars/edit-user-avatar.react.js
@@ -1,147 +1,152 @@
// @flow
import { useNavigation } from '@react-navigation/native';
import invariant from 'invariant';
import * as React from 'react';
import { ActivityIndicator, TouchableOpacity, View } from 'react-native';
import { useENSAvatar } from 'lib/hooks/ens-cache.js';
import { getETHAddressForUserInfo } from 'lib/shared/account-utils.js';
import type { GenericUserInfoWithAvatar } from 'lib/types/avatar-types.js';
import { useShowAvatarActionSheet } from './avatar-hooks.js';
import EditAvatarBadge from './edit-avatar-badge.react.js';
import { EditUserAvatarContext } from './edit-user-avatar-provider.react.js';
import UserAvatar from './user-avatar.react.js';
import {
EmojiUserAvatarCreationRouteName,
UserAvatarCameraModalRouteName,
EmojiAvatarSelectionRouteName,
RegistrationUserAvatarCameraModalRouteName,
} from '../navigation/route-names.js';
import { useSelector } from '../redux/redux-utils.js';
import { useStyles } from '../themes/colors.js';
type Props =
| { +userID: ?string, +disabled?: boolean }
- | { +userInfo: ?GenericUserInfoWithAvatar, +disabled?: boolean };
+ | {
+ +userInfo: ?GenericUserInfoWithAvatar,
+ +disabled?: boolean,
+ +prefetchedAvatarURI: ?string,
+ };
function EditUserAvatar(props: Props): React.Node {
const editUserAvatarContext = React.useContext(EditUserAvatarContext);
invariant(editUserAvatarContext, 'editUserAvatarContext should be set');
const {
userAvatarSaveInProgress,
selectFromGalleryAndUpdateUserAvatar,
setUserAvatar,
registrationModeEnabled,
} = editUserAvatarContext;
const currentUserInfo = useSelector(state => state.currentUserInfo);
const userInfoProp = props.userInfo;
const userInfo: ?GenericUserInfoWithAvatar = userInfoProp ?? currentUserInfo;
const ethAddress = React.useMemo(
() => getETHAddressForUserInfo(userInfo),
[userInfo],
);
- const ensAvatarURI = useENSAvatar(ethAddress);
+ const fetchedENSAvatarURI = useENSAvatar(ethAddress);
+ const ensAvatarURI = fetchedENSAvatarURI ?? props.prefetchedAvatarURI;
const { navigate } = useNavigation();
const usernameOrEthAddress = userInfo?.username;
const navigateToEmojiSelection = React.useCallback(() => {
if (!registrationModeEnabled) {
navigate(EmojiUserAvatarCreationRouteName);
return;
}
navigate<'EmojiAvatarSelection'>({
name: EmojiAvatarSelectionRouteName,
params: { usernameOrEthAddress },
});
}, [navigate, registrationModeEnabled, usernameOrEthAddress]);
const navigateToCamera = React.useCallback(() => {
navigate(
registrationModeEnabled
? RegistrationUserAvatarCameraModalRouteName
: UserAvatarCameraModalRouteName,
);
}, [navigate, registrationModeEnabled]);
const setENSUserAvatar = React.useCallback(() => {
setUserAvatar({ type: 'ens' });
}, [setUserAvatar]);
const removeUserAvatar = React.useCallback(() => {
setUserAvatar({ type: 'remove' });
}, [setUserAvatar]);
const hasCurrentAvatar = !!userInfo?.avatar;
const actionSheetConfig = React.useMemo(() => {
const configOptions = [
{ id: 'emoji', onPress: navigateToEmojiSelection },
{ id: 'image', onPress: selectFromGalleryAndUpdateUserAvatar },
{ id: 'camera', onPress: navigateToCamera },
];
if (ensAvatarURI) {
configOptions.push({ id: 'ens', onPress: setENSUserAvatar });
}
if (hasCurrentAvatar) {
configOptions.push({ id: 'remove', onPress: removeUserAvatar });
}
return configOptions;
}, [
hasCurrentAvatar,
ensAvatarURI,
navigateToCamera,
navigateToEmojiSelection,
removeUserAvatar,
setENSUserAvatar,
selectFromGalleryAndUpdateUserAvatar,
]);
const showAvatarActionSheet = useShowAvatarActionSheet(actionSheetConfig);
const styles = useStyles(unboundStyles);
let spinner;
if (userAvatarSaveInProgress) {
spinner = (
);
}
const { userID } = props;
const userAvatar = userID ? (
) : (
);
const { disabled } = props;
return (
{userAvatar}
{spinner}
{!disabled ? : null}
);
}
const unboundStyles = {
spinnerContainer: {
position: 'absolute',
alignItems: 'center',
justifyContent: 'center',
top: 0,
bottom: 0,
left: 0,
right: 0,
},
};
export default EditUserAvatar;