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;