diff --git a/native/chat/settings/thread-settings-avatar.react.js b/native/chat/settings/thread-settings-avatar.react.js index 855feff60..08f49ab00 100644 --- a/native/chat/settings/thread-settings-avatar.react.js +++ b/native/chat/settings/thread-settings-avatar.react.js @@ -1,91 +1,59 @@ // @flow +import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; -import { View, TouchableWithoutFeedback } from 'react-native'; +import { View } from 'react-native'; import { type ResolvedThreadInfo } from 'lib/types/thread-types.js'; -import SWMansionIcon from '../../components/swmansion-icon.react.js'; +import EditAvatar from '../../components/edit-avatar.react.js'; import ThreadAvatar from '../../components/thread-avatar.react.js'; -import { useColors, useStyles } from '../../themes/colors.js'; +import { EmojiAvatarCreationRouteName } from '../../navigation/route-names.js'; +import { useStyles } from '../../themes/colors.js'; type Props = { +threadInfo: ResolvedThreadInfo, +canChangeSettings: boolean, }; function ThreadSettingsAvatar(props: Props): React.Node { const { threadInfo, canChangeSettings } = props; - const colors = useColors(); - const styles = useStyles(unboundStyles); - - const onPressEditAvatar = React.useCallback(() => { - // TODO: - // Display action sheet with all the different avatar creation options - }, []); + const { navigate } = useNavigation(); - const editBadge = React.useMemo(() => { - if (!canChangeSettings) { - return null; - } + const styles = useStyles(unboundStyles); - return ( - - - - ); - }, [ - canChangeSettings, - colors.floatingButtonLabel, - styles.editAvatarIcon, - styles.editAvatarIconContainer, - ]); + const onPressEmojiAvatarFlow = React.useCallback(() => { + navigate<'EmojiAvatarCreation'>({ + name: EmojiAvatarCreationRouteName, + params: { + threadID: threadInfo.id, + containingThreadID: threadInfo.containingThreadID, + }, + }); + }, [navigate, threadInfo.containingThreadID, threadInfo.id]); return ( - - - - {editBadge} - - + + ); } const unboundStyles = { container: { alignItems: 'center', backgroundColor: 'panelForeground', flex: 1, paddingVertical: 16, }, - editAvatarIconContainer: { - position: 'absolute', - bottom: 0, - right: 0, - borderWidth: 2, - borderColor: 'panelForeground', - borderRadius: 18, - width: 36, - height: 36, - backgroundColor: 'purpleButton', - justifyContent: 'center', - }, - editAvatarIcon: { - textAlign: 'center', - }, }; const MemoizedThreadSettingsAvatar: React.ComponentType = React.memo(ThreadSettingsAvatar); export default MemoizedThreadSettingsAvatar; diff --git a/native/components/edit-avatar.react.js b/native/components/edit-avatar.react.js new file mode 100644 index 000000000..fa469aef5 --- /dev/null +++ b/native/components/edit-avatar.react.js @@ -0,0 +1,68 @@ +// @flow + +import * as React from 'react'; +import { View, TouchableOpacity } from 'react-native'; + +import SWMansionIcon from '../components/swmansion-icon.react.js'; +import { useColors, useStyles } from '../themes/colors.js'; + +type Props = { + +children: React.Node, + +onPressEmojiAvatarFlow: () => mixed, + +disabled?: boolean, +}; +function EditAvatar(props: Props): React.Node { + const { onPressEmojiAvatarFlow, children, disabled } = props; + + const colors = useColors(); + const styles = useStyles(unboundStyles); + + const editBadge = React.useMemo(() => { + if (disabled) { + return null; + } + + return ( + + + + ); + }, [ + colors.floatingButtonLabel, + disabled, + styles.editAvatarIcon, + styles.editAvatarIconContainer, + ]); + + return ( + + {children} + {editBadge} + + ); +} + +const unboundStyles = { + editAvatarIconContainer: { + position: 'absolute', + bottom: 0, + right: 0, + borderWidth: 2, + borderColor: 'panelForeground', + borderRadius: 18, + width: 36, + height: 36, + backgroundColor: 'purpleButton', + justifyContent: 'center', + }, + editAvatarIcon: { + textAlign: 'center', + }, +}; + +export default EditAvatar; diff --git a/native/profile/profile-screen.react.js b/native/profile/profile-screen.react.js index 0dcade58c..819669a20 100644 --- a/native/profile/profile-screen.react.js +++ b/native/profile/profile-screen.react.js @@ -1,467 +1,438 @@ // @flow import * as React from 'react'; -import { - View, - Text, - Alert, - Platform, - ScrollView, - TouchableWithoutFeedback, -} from 'react-native'; +import { View, Text, Alert, Platform, ScrollView } from 'react-native'; import { logOutActionTypes, logOut } from 'lib/actions/user-actions.js'; import { useStringForUser } from 'lib/hooks/ens-cache.js'; import { preRequestUserStateSelector } from 'lib/selectors/account-selectors.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { accountHasPassword } from 'lib/shared/account-utils.js'; import type { LogOutResult } from 'lib/types/account-types.js'; import { type PreRequestUserState } from 'lib/types/session-types.js'; import { type CurrentUserInfo } from 'lib/types/user-types.js'; import { type DispatchActionPromise, useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import { deleteNativeCredentialsFor } from '../account/native-credentials.js'; import Action from '../components/action-row.react.js'; import Button from '../components/button.react.js'; +import EditAvatar from '../components/edit-avatar.react.js'; import EditSettingButton from '../components/edit-setting-button.react.js'; import { SingleLine } from '../components/single-line.react.js'; -import SWMansionIcon from '../components/swmansion-icon.react.js'; import UserAvatar from '../components/user-avatar.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { EditPasswordRouteName, + EmojiAvatarCreationRouteName, DeleteAccountRouteName, BuildInfoRouteName, DevToolsRouteName, AppearancePreferencesRouteName, FriendListRouteName, BlockListRouteName, PrivacyPreferencesRouteName, DefaultNotificationsPreferencesRouteName, } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { type Colors, useColors, useStyles } from '../themes/colors.js'; import { useShouldRenderAvatars } from '../utils/avatar-utils.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; type ProfileRowProps = { +content: string, +onPress: () => void, +danger?: boolean, }; function ProfileRow(props: ProfileRowProps): React.Node { const { content, onPress, danger } = props; return ( ); } type BaseProps = { +navigation: ProfileNavigationProp<'ProfileScreen'>, +route: NavigationRoute<'ProfileScreen'>, }; type Props = { ...BaseProps, +currentUserInfo: ?CurrentUserInfo, +preRequestUserState: PreRequestUserState, +logOutLoading: boolean, +colors: Colors, +styles: typeof unboundStyles, +dispatchActionPromise: DispatchActionPromise, +logOut: (preRequestUserState: PreRequestUserState) => Promise, +staffCanSee: boolean, +stringForUser: ?string, +isAccountWithPassword: boolean, +shouldRenderAvatars: boolean, }; class ProfileScreen extends React.PureComponent { get loggedOutOrLoggingOut() { return ( !this.props.currentUserInfo || this.props.currentUserInfo.anonymous || this.props.logOutLoading ); } render() { let developerTools, defaultNotifications; const { staffCanSee } = this.props; if (staffCanSee) { developerTools = ( ); defaultNotifications = ( ); } let passwordEditionUI; if (accountHasPassword(this.props.currentUserInfo)) { passwordEditionUI = ( Password •••••••••••••••• ); } let avatarSection; if (this.props.shouldRenderAvatars) { avatarSection = ( <> USER AVATAR - - - - - - - - + + + ); } return ( {avatarSection} ACCOUNT Logged in as {this.props.stringForUser} {passwordEditionUI} PREFERENCES {defaultNotifications} {developerTools} ); } - onPressEditAvatar = () => { - // TODO: - // Display action sheet with all the different avatar creation options + onPressEmojiAvatarFlow = () => { + this.props.navigation.navigate<'EmojiAvatarCreation'>({ + name: EmojiAvatarCreationRouteName, + params: {}, + }); }; onPressLogOut = () => { if (this.loggedOutOrLoggingOut) { return; } if (!this.props.isAccountWithPassword) { Alert.alert( 'Log out', 'Are you sure you want to log out?', [ { text: 'No', style: 'cancel' }, { text: 'Yes', onPress: this.logOutWithoutDeletingNativeCredentialsWrapper, style: 'destructive', }, ], { cancelable: true }, ); return; } const alertTitle = Platform.OS === 'ios' ? 'Keep Login Info in Keychain' : 'Keep Login Info'; const alertDescription = 'We will automatically fill out log-in forms with your credentials ' + 'in the app.'; Alert.alert( alertTitle, alertDescription, [ { text: 'Cancel', style: 'cancel' }, { text: 'Keep', onPress: this.logOutWithoutDeletingNativeCredentialsWrapper, }, { text: 'Remove', onPress: this.logOutAndDeleteNativeCredentialsWrapper, style: 'destructive', }, ], { cancelable: true }, ); }; logOutWithoutDeletingNativeCredentialsWrapper = () => { if (this.loggedOutOrLoggingOut) { return; } this.logOut(); }; logOutAndDeleteNativeCredentialsWrapper = async () => { if (this.loggedOutOrLoggingOut) { return; } await this.deleteNativeCredentials(); this.logOut(); }; logOut() { this.props.dispatchActionPromise( logOutActionTypes, this.props.logOut(this.props.preRequestUserState), ); } async deleteNativeCredentials() { await deleteNativeCredentialsFor(); } navigateIfActive(name) { this.props.navigation.navigate({ name }); } onPressEditPassword = () => { this.navigateIfActive(EditPasswordRouteName); }; onPressDeleteAccount = () => { this.navigateIfActive(DeleteAccountRouteName); }; onPressBuildInfo = () => { this.navigateIfActive(BuildInfoRouteName); }; onPressDevTools = () => { this.navigateIfActive(DevToolsRouteName); }; onPressAppearance = () => { this.navigateIfActive(AppearancePreferencesRouteName); }; onPressPrivacy = () => { this.navigateIfActive(PrivacyPreferencesRouteName); }; onPressDefaultNotifications = () => { this.navigateIfActive(DefaultNotificationsPreferencesRouteName); }; onPressFriendList = () => { this.navigateIfActive(FriendListRouteName); }; onPressBlockList = () => { this.navigateIfActive(BlockListRouteName); }; } const unboundStyles = { avatarSection: { alignItems: 'center', paddingVertical: 16, }, - editAvatarIconContainer: { - position: 'absolute', - bottom: 0, - right: 0, - borderWidth: 2, - borderColor: 'panelForeground', - borderRadius: 18, - width: 36, - height: 36, - backgroundColor: '#6D49AB', - justifyContent: 'center', - }, - editAvatarIcon: { - textAlign: 'center', - }, container: { flex: 1, }, content: { flex: 1, }, deleteAccountButton: { paddingHorizontal: 24, paddingVertical: 12, }, editPasswordButton: { paddingTop: Platform.OS === 'android' ? 3 : 2, }, header: { color: 'panelBackgroundLabel', fontSize: 12, fontWeight: '400', paddingBottom: 3, paddingHorizontal: 24, }, label: { color: 'panelForegroundTertiaryLabel', fontSize: 16, paddingRight: 12, }, loggedInLabel: { color: 'panelForegroundTertiaryLabel', fontSize: 16, }, logOutText: { color: 'link', fontSize: 16, paddingLeft: 6, }, row: { flex: 1, flexDirection: 'row', justifyContent: 'space-between', }, scrollView: { backgroundColor: 'panelBackground', }, scrollViewContentContainer: { paddingTop: 24, }, paddedRow: { flex: 1, flexDirection: 'row', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 10, }, section: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, paddingVertical: 1, }, unpaddedSection: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, }, username: { color: 'panelForegroundLabel', flex: 1, }, value: { color: 'panelForegroundLabel', fontSize: 16, textAlign: 'right', }, }; const logOutLoadingStatusSelector = createLoadingStatusSelector(logOutActionTypes); const ConnectedProfileScreen: React.ComponentType = React.memo(function ConnectedProfileScreen(props: BaseProps) { const currentUserInfo = useSelector(state => state.currentUserInfo); const preRequestUserState = useSelector(preRequestUserStateSelector); const logOutLoading = useSelector(logOutLoadingStatusSelector) === 'loading'; const colors = useColors(); const styles = useStyles(unboundStyles); const callLogOut = useServerCall(logOut); const dispatchActionPromise = useDispatchActionPromise(); const staffCanSee = useStaffCanSee(); const stringForUser = useStringForUser(currentUserInfo); const isAccountWithPassword = useSelector(state => accountHasPassword(state.currentUserInfo), ); const shouldRenderAvatars = useShouldRenderAvatars(); return ( ); }); export default ConnectedProfileScreen;