diff --git a/native/avatars/edit-avatar.react.js b/native/avatars/edit-thread-avatar.react.js similarity index 98% copy from native/avatars/edit-avatar.react.js copy to native/avatars/edit-thread-avatar.react.js index 814e93b5a..afa7b2f12 100644 --- a/native/avatars/edit-avatar.react.js +++ b/native/avatars/edit-thread-avatar.react.js @@ -1,234 +1,234 @@ // @flow import { useActionSheet } from '@expo/react-native-action-sheet'; import * as ImagePicker from 'expo-image-picker'; import * as React from 'react'; import { View, TouchableOpacity, Platform } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { uploadMultimedia } from 'lib/actions/upload-actions.js'; import { extensionFromFilename, filenameFromPathOrURI, } from 'lib/media/file-utils.js'; import type { MediaLibrarySelection } from 'lib/types/media-types.js'; import { useServerCall } from 'lib/utils/action-utils.js'; import CommIcon from '../components/comm-icon.react.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import { getCompatibleMediaURI } from '../media/identifier-utils.js'; import { processMedia } from '../media/media-utils.js'; import type { MediaResult } from '../media/media-utils.js'; import { useSelector } from '../redux/redux-utils.js'; import { useColors, useStyles } from '../themes/colors.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; type Props = { +children: React.Node, +onPressEmojiAvatarFlow: () => mixed, +disabled?: boolean, }; -function EditAvatar(props: Props): React.Node { +function EditThreadAvatar(props: Props): React.Node { const { onPressEmojiAvatarFlow, children, disabled } = props; const { showActionSheetWithOptions } = useActionSheet(); const colors = useColors(); const styles = useStyles(unboundStyles); const hasWiFi = useSelector(state => state.connectivity.hasWiFi); const staffCanSee = useStaffCanSee(); const callUploadMultimedia = useServerCall(uploadMultimedia); const uploadProcessedMedia = React.useCallback( (processedMedia: MediaResult) => { const { uploadURI, filename, mime, dimensions } = processedMedia; return callUploadMultimedia( { uri: uploadURI, name: filename, type: mime, }, dimensions, ); }, [callUploadMultimedia], ); const processSelectedMedia = React.useCallback( async (selection: MediaLibrarySelection) => { const { resultPromise } = processMedia(selection, { hasWiFi, finalFileHeaderCheck: staffCanSee, }); return await resultPromise; }, [hasWiFi, staffCanSee], ); // eslint-disable-next-line no-unused-vars const openPhotoGallery = React.useCallback(async () => { try { const { assets, canceled } = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, allowsMultipleSelection: false, quality: 1, }); if (canceled || assets.length === 0) { return; } const asset = assets.pop(); const { width, height, assetId: mediaNativeID } = asset; const assetFilename = asset.fileName || filenameFromPathOrURI(asset.uri) || ''; const uri = getCompatibleMediaURI( asset.uri, extensionFromFilename(assetFilename), ); const currentTime = Date.now(); const selection: MediaLibrarySelection = { step: 'photo_library', dimensions: { height, width }, uri, filename: assetFilename, mediaNativeID, selectTime: currentTime, sendTime: currentTime, retries: 0, }; const processedMedia = await processSelectedMedia(selection); if (!processedMedia.success) { return; } await uploadProcessedMedia(processedMedia); } catch (e) { console.log(e); return; } }, [processSelectedMedia, uploadProcessedMedia]); const editAvatarOptions = React.useMemo(() => { const options = [ { id: 'emoji', text: 'Use Emoji', onPress: onPressEmojiAvatarFlow, icon: ( ), }, ]; if (Platform.OS === 'ios') { options.push({ id: 'cancel', text: 'Cancel', isCancel: true, }); } return options; }, [onPressEmojiAvatarFlow, styles.bottomSheetIcon]); const insets = useSafeAreaInsets(); const onPressEditAvatar = React.useCallback(() => { const texts = editAvatarOptions.map(option => option.text); const cancelButtonIndex = editAvatarOptions.findIndex( option => option.isCancel, ); const containerStyle = { paddingBottom: insets.bottom, }; const icons = editAvatarOptions.map(option => option.icon); const onPressAction = (selectedIndex: ?number) => { if ( selectedIndex === null || selectedIndex === undefined || selectedIndex < 0 ) { return; } const option = editAvatarOptions[selectedIndex]; if (option.onPress) { option.onPress(); } }; showActionSheetWithOptions( { options: texts, cancelButtonIndex, containerStyle, icons, }, onPressAction, ); }, [editAvatarOptions, insets.bottom, showActionSheetWithOptions]); 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', }, bottomSheetIcon: { color: '#000000', }, }; -export default EditAvatar; +export default EditThreadAvatar; diff --git a/native/avatars/edit-avatar.react.js b/native/avatars/edit-user-avatar.react.js similarity index 98% rename from native/avatars/edit-avatar.react.js rename to native/avatars/edit-user-avatar.react.js index 814e93b5a..83d41127c 100644 --- a/native/avatars/edit-avatar.react.js +++ b/native/avatars/edit-user-avatar.react.js @@ -1,234 +1,234 @@ // @flow import { useActionSheet } from '@expo/react-native-action-sheet'; import * as ImagePicker from 'expo-image-picker'; import * as React from 'react'; import { View, TouchableOpacity, Platform } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { uploadMultimedia } from 'lib/actions/upload-actions.js'; import { extensionFromFilename, filenameFromPathOrURI, } from 'lib/media/file-utils.js'; import type { MediaLibrarySelection } from 'lib/types/media-types.js'; import { useServerCall } from 'lib/utils/action-utils.js'; import CommIcon from '../components/comm-icon.react.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import { getCompatibleMediaURI } from '../media/identifier-utils.js'; import { processMedia } from '../media/media-utils.js'; import type { MediaResult } from '../media/media-utils.js'; import { useSelector } from '../redux/redux-utils.js'; import { useColors, useStyles } from '../themes/colors.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; type Props = { +children: React.Node, +onPressEmojiAvatarFlow: () => mixed, +disabled?: boolean, }; -function EditAvatar(props: Props): React.Node { +function EditUserAvatar(props: Props): React.Node { const { onPressEmojiAvatarFlow, children, disabled } = props; const { showActionSheetWithOptions } = useActionSheet(); const colors = useColors(); const styles = useStyles(unboundStyles); const hasWiFi = useSelector(state => state.connectivity.hasWiFi); const staffCanSee = useStaffCanSee(); const callUploadMultimedia = useServerCall(uploadMultimedia); const uploadProcessedMedia = React.useCallback( (processedMedia: MediaResult) => { const { uploadURI, filename, mime, dimensions } = processedMedia; return callUploadMultimedia( { uri: uploadURI, name: filename, type: mime, }, dimensions, ); }, [callUploadMultimedia], ); const processSelectedMedia = React.useCallback( async (selection: MediaLibrarySelection) => { const { resultPromise } = processMedia(selection, { hasWiFi, finalFileHeaderCheck: staffCanSee, }); return await resultPromise; }, [hasWiFi, staffCanSee], ); // eslint-disable-next-line no-unused-vars const openPhotoGallery = React.useCallback(async () => { try { const { assets, canceled } = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, allowsMultipleSelection: false, quality: 1, }); if (canceled || assets.length === 0) { return; } const asset = assets.pop(); const { width, height, assetId: mediaNativeID } = asset; const assetFilename = asset.fileName || filenameFromPathOrURI(asset.uri) || ''; const uri = getCompatibleMediaURI( asset.uri, extensionFromFilename(assetFilename), ); const currentTime = Date.now(); const selection: MediaLibrarySelection = { step: 'photo_library', dimensions: { height, width }, uri, filename: assetFilename, mediaNativeID, selectTime: currentTime, sendTime: currentTime, retries: 0, }; const processedMedia = await processSelectedMedia(selection); if (!processedMedia.success) { return; } await uploadProcessedMedia(processedMedia); } catch (e) { console.log(e); return; } }, [processSelectedMedia, uploadProcessedMedia]); const editAvatarOptions = React.useMemo(() => { const options = [ { id: 'emoji', text: 'Use Emoji', onPress: onPressEmojiAvatarFlow, icon: ( ), }, ]; if (Platform.OS === 'ios') { options.push({ id: 'cancel', text: 'Cancel', isCancel: true, }); } return options; }, [onPressEmojiAvatarFlow, styles.bottomSheetIcon]); const insets = useSafeAreaInsets(); const onPressEditAvatar = React.useCallback(() => { const texts = editAvatarOptions.map(option => option.text); const cancelButtonIndex = editAvatarOptions.findIndex( option => option.isCancel, ); const containerStyle = { paddingBottom: insets.bottom, }; const icons = editAvatarOptions.map(option => option.icon); const onPressAction = (selectedIndex: ?number) => { if ( selectedIndex === null || selectedIndex === undefined || selectedIndex < 0 ) { return; } const option = editAvatarOptions[selectedIndex]; if (option.onPress) { option.onPress(); } }; showActionSheetWithOptions( { options: texts, cancelButtonIndex, containerStyle, icons, }, onPressAction, ); }, [editAvatarOptions, insets.bottom, showActionSheetWithOptions]); 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', }, bottomSheetIcon: { color: '#000000', }, }; -export default EditAvatar; +export default EditUserAvatar; diff --git a/native/chat/settings/thread-settings-avatar.react.js b/native/chat/settings/thread-settings-avatar.react.js index ffa02aa9f..9ecf1cf30 100644 --- a/native/chat/settings/thread-settings-avatar.react.js +++ b/native/chat/settings/thread-settings-avatar.react.js @@ -1,59 +1,59 @@ // @flow import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { View } from 'react-native'; import { type ResolvedThreadInfo } from 'lib/types/thread-types.js'; -import EditAvatar from '../../avatars/edit-avatar.react.js'; +import EditThreadAvatar from '../../avatars/edit-thread-avatar.react.js'; import ThreadAvatar from '../../avatars/thread-avatar.react.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 { navigate } = useNavigation(); const styles = useStyles(unboundStyles); const onPressEmojiAvatarFlow = React.useCallback(() => { navigate<'EmojiAvatarCreation'>({ name: EmojiAvatarCreationRouteName, params: { threadID: threadInfo.id, containingThreadID: threadInfo.containingThreadID, }, }); }, [navigate, threadInfo.containingThreadID, threadInfo.id]); return ( - - + ); } const unboundStyles = { container: { alignItems: 'center', backgroundColor: 'panelForeground', flex: 1, paddingVertical: 16, }, }; const MemoizedThreadSettingsAvatar: React.ComponentType = React.memo(ThreadSettingsAvatar); export default MemoizedThreadSettingsAvatar; diff --git a/native/profile/profile-screen.react.js b/native/profile/profile-screen.react.js index bc5f8c1b6..db9d7100e 100644 --- a/native/profile/profile-screen.react.js +++ b/native/profile/profile-screen.react.js @@ -1,438 +1,440 @@ // @flow import * as React from 'react'; 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 EditAvatar from '../avatars/edit-avatar.react.js'; +import EditUserAvatar from '../avatars/edit-user-avatar.react.js'; import UserAvatar from '../avatars/user-avatar.react.js'; import Action from '../components/action-row.react.js'; import Button from '../components/button.react.js'; import EditSettingButton from '../components/edit-setting-button.react.js'; import { SingleLine } from '../components/single-line.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} ); } 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, }, 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;