diff --git a/native/chat/message-reactions-modal.react.js b/native/chat/message-reactions-modal.react.js
index 096fecac5..8d3bfa9ae 100644
--- a/native/chat/message-reactions-modal.react.js
+++ b/native/chat/message-reactions-modal.react.js
@@ -1,158 +1,167 @@
// @flow
import Icon from '@expo/vector-icons/FontAwesome.js';
import { useNavigation } from '@react-navigation/native';
import * as React from 'react';
import { View, Text, FlatList, TouchableHighlight } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import type { ReactionInfo } from 'lib/selectors/chat-selectors.js';
import { getAvatarForUser } from 'lib/shared/avatar-utils.js';
import { useMessageReactionsList } from 'lib/shared/reaction-utils.js';
import Avatar from '../components/avatar.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 { useColors, useStyles } from '../themes/colors.js';
+import { useShouldRenderAvatars } from '../utils/avatar-utils.js';
export type MessageReactionsModalParams = {
+reactions: ReactionInfo,
};
type Props = {
+navigation: RootNavigationProp<'MessageReactionsModal'>,
+route: NavigationRoute<'MessageReactionsModal'>,
};
function MessageReactionsModal(props: Props): React.Node {
const { reactions } = props.route.params;
const styles = useStyles(unboundStyles);
const colors = useColors();
const navigation = useNavigation();
const modalSafeAreaEdges = React.useMemo(() => ['top'], []);
const modalContainerSafeAreaEdges = React.useMemo(() => ['bottom'], []);
const close = React.useCallback(() => navigation.goBack(), [navigation]);
const reactionsListData = useMessageReactionsList(reactions);
+ const shouldRenderAvatars = useShouldRenderAvatars();
+ const marginLeftStyle = React.useMemo(
+ () => ({
+ marginLeft: shouldRenderAvatars ? 8 : 0,
+ }),
+ [shouldRenderAvatars],
+ );
+
const renderItem = React.useCallback(
({ item }) => {
const avatarInfo = getAvatarForUser(item);
return (
-
+
{item.username}
{item.reaction}
);
},
[
+ marginLeftStyle,
styles.reactionsListReactionText,
styles.reactionsListRowContainer,
styles.reactionsListUserInfoContainer,
styles.reactionsListUsernameText,
],
);
const itemSeperator = React.useCallback(() => {
return ;
}, [styles.reactionsListItemSeperator]);
return (
Reactions
);
}
const unboundStyles = {
modalStyle: {
// we need to set each margin property explicitly to override
marginLeft: 0,
marginRight: 0,
marginBottom: 0,
marginTop: 0,
justifyContent: 'flex-end',
flex: 0,
borderWidth: 0,
borderTopLeftRadius: 10,
borderTopRightRadius: 10,
},
modalContainerStyle: {
justifyContent: 'flex-end',
},
modalContentContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 24,
marginTop: 8,
},
reactionsListContentContainer: {
paddingBottom: 16,
},
reactionsListTitleText: {
color: 'modalForegroundLabel',
fontSize: 18,
},
reactionsListRowContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
},
reactionsListUserInfoContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
},
reactionsListUsernameText: {
color: 'modalForegroundLabel',
fontSize: 18,
- marginLeft: 8,
},
reactionsListReactionText: {
fontSize: 18,
},
reactionsListItemSeperator: {
height: 16,
},
closeButton: {
borderRadius: 4,
width: 18,
height: 18,
alignItems: 'center',
},
closeIcon: {
color: 'modalBackgroundSecondaryLabel',
},
};
export default MessageReactionsModal;
diff --git a/native/chat/settings/thread-settings-member.react.js b/native/chat/settings/thread-settings-member.react.js
index 84970442b..37d7bf8eb 100644
--- a/native/chat/settings/thread-settings-member.react.js
+++ b/native/chat/settings/thread-settings-member.react.js
@@ -1,302 +1,317 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import {
View,
Text,
Platform,
ActivityIndicator,
TouchableOpacity,
} from 'react-native';
import {
removeUsersFromThreadActionTypes,
changeThreadMemberRolesActionTypes,
} from 'lib/actions/thread-actions.js';
import { useENSNames } from 'lib/hooks/ens-cache.js';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
import { getAvatarForUser } from 'lib/shared/avatar-utils.js';
import {
memberIsAdmin,
memberHasAdminPowers,
getAvailableThreadMemberActions,
} from 'lib/shared/thread-utils.js';
import { stringForUser } from 'lib/shared/user-utils.js';
import type { LoadingStatus } from 'lib/types/loading-types.js';
import {
type ThreadInfo,
type RelativeMemberInfo,
} from 'lib/types/thread-types.js';
import type { ThreadSettingsNavigate } from './thread-settings.react.js';
import Avatar from '../../components/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 { ThreadSettingsMemberTooltipModalRouteName } 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 { useShouldRenderAvatars } from '../../utils/avatar-utils.js';
type BaseProps = {
+memberInfo: RelativeMemberInfo,
+threadInfo: ThreadInfo,
+canEdit: boolean,
+navigate: ThreadSettingsNavigate,
+firstListItem: boolean,
+lastListItem: boolean,
+verticalBounds: ?VerticalBounds,
+threadSettingsRouteKey: string,
};
type Props = {
...BaseProps,
// Redux state
+removeUserLoadingStatus: LoadingStatus,
+changeRoleLoadingStatus: LoadingStatus,
+colors: Colors,
+styles: typeof unboundStyles,
// withKeyboardState
+keyboardState: ?KeyboardState,
// withOverlayContext
+overlayContext: ?OverlayContextType,
+ +shouldRenderAvatars: boolean,
};
class ThreadSettingsMember extends React.PureComponent {
editButton: ?React.ElementRef;
render() {
const userText = stringForUser(this.props.memberInfo);
const avatarInfo = getAvatarForUser(this.props.memberInfo);
+
+ const marginLeftStyle = {
+ marginLeft: this.props.shouldRenderAvatars ? 8 : 0,
+ };
+
let usernameInfo = null;
if (this.props.memberInfo.username) {
usernameInfo = (
- {userText}
+
+ {userText}
+
);
} else {
usernameInfo = (
{userText}
);
}
let editButton = null;
if (
this.props.removeUserLoadingStatus === 'loading' ||
this.props.changeRoleLoadingStatus === 'loading'
) {
editButton = (
);
} else if (
getAvailableThreadMemberActions(
this.props.memberInfo,
this.props.threadInfo,
this.props.canEdit,
).length !== 0
) {
editButton = (
);
}
let roleInfo = null;
if (memberIsAdmin(this.props.memberInfo, this.props.threadInfo)) {
roleInfo = (
admin
);
} else if (memberHasAdminPowers(this.props.memberInfo)) {
roleInfo = (
parent admin
);
}
const firstItem = this.props.firstListItem
? null
: this.props.styles.topBorder;
const lastItem = this.props.lastListItem
? this.props.styles.lastInnerContainer
: null;
return (
{usernameInfo}
{editButton}
{roleInfo}
);
}
editButtonRef = (editButton: ?React.ElementRef) => {
this.editButton = editButton;
};
onEditButtonLayout = () => {};
onPressEdit = () => {
if (this.dismissKeyboardIfShowing()) {
return;
}
const {
editButton,
props: { verticalBounds },
} = this;
if (!editButton || !verticalBounds) {
return;
}
const { overlayContext } = this.props;
invariant(
overlayContext,
'ThreadSettingsMember should have OverlayContext',
);
overlayContext.setScrollBlockingModalStatus('open');
editButton.measure((x, y, width, height, pageX, pageY) => {
const coordinates = { x: pageX, y: pageY, width, height };
this.props.navigate<'ThreadSettingsMemberTooltipModal'>({
name: ThreadSettingsMemberTooltipModalRouteName,
params: {
presentedFrom: this.props.threadSettingsRouteKey,
initialCoordinates: coordinates,
verticalBounds,
visibleEntryIDs: getAvailableThreadMemberActions(
this.props.memberInfo,
this.props.threadInfo,
this.props.canEdit,
),
memberInfo: this.props.memberInfo,
threadInfo: this.props.threadInfo,
},
});
});
};
dismissKeyboardIfShowing = () => {
const { keyboardState } = this.props;
return !!(keyboardState && keyboardState.dismissKeyboardIfShowing());
};
}
const unboundStyles = {
anonymous: {
color: 'panelForegroundTertiaryLabel',
fontStyle: 'italic',
},
container: {
backgroundColor: 'panelForeground',
flex: 1,
paddingHorizontal: 12,
},
editButton: {
paddingLeft: 10,
},
topBorder: {
borderColor: 'panelForegroundBorder',
borderTopWidth: 1,
},
innerContainer: {
flex: 1,
paddingHorizontal: 12,
paddingVertical: 8,
},
lastInnerContainer: {
paddingBottom: Platform.OS === 'ios' ? 12 : 10,
},
role: {
color: 'panelForegroundTertiaryLabel',
flex: 1,
fontSize: 14,
paddingTop: 4,
},
row: {
flex: 1,
flexDirection: 'row',
},
userInfoContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
},
username: {
color: 'panelForegroundSecondaryLabel',
flex: 1,
fontSize: 16,
lineHeight: 20,
- marginLeft: 8,
},
};
const ConnectedThreadSettingsMember: React.ComponentType =
React.memo(function ConnectedThreadSettingsMember(
props: BaseProps,
) {
const memberID = props.memberInfo.id;
const removeUserLoadingStatus = useSelector(state =>
createLoadingStatusSelector(
removeUsersFromThreadActionTypes,
`${removeUsersFromThreadActionTypes.started}:${memberID}`,
)(state),
);
const changeRoleLoadingStatus = useSelector(state =>
createLoadingStatusSelector(
changeThreadMemberRolesActionTypes,
`${changeThreadMemberRolesActionTypes.started}:${memberID}`,
)(state),
);
const [memberInfo] = useENSNames([props.memberInfo]);
const colors = useColors();
const styles = useStyles(unboundStyles);
const keyboardState = React.useContext(KeyboardContext);
const overlayContext = React.useContext(OverlayContext);
+ const shouldRenderAvatars = useShouldRenderAvatars();
+
return (
);
});
export default ConnectedThreadSettingsMember;
diff --git a/native/chat/typeahead-tooltip.react.js b/native/chat/typeahead-tooltip.react.js
index ea7669e77..14d2b876a 100644
--- a/native/chat/typeahead-tooltip.react.js
+++ b/native/chat/typeahead-tooltip.react.js
@@ -1,155 +1,165 @@
// @flow
import * as React from 'react';
import { Platform, Text } from 'react-native';
import { PanGestureHandler, FlatList } from 'react-native-gesture-handler';
import { getAvatarForUser } from 'lib/shared/avatar-utils.js';
import {
type TypeaheadMatchedStrings,
type Selection,
getNewTextAndSelection,
} from 'lib/shared/mention-utils.js';
import type { RelativeMemberInfo } from 'lib/types/thread-types.js';
import Avatar from '../components/avatar.react.js';
import Button from '../components/button.react.js';
import { useStyles } from '../themes/colors.js';
+import { useShouldRenderAvatars } from '../utils/avatar-utils.js';
export type TypeaheadTooltipProps = {
+text: string,
+matchedStrings: TypeaheadMatchedStrings,
+suggestedUsers: $ReadOnlyArray,
+focusAndUpdateTextAndSelection: (text: string, selection: Selection) => void,
};
function TypeaheadTooltip(props: TypeaheadTooltipProps): React.Node {
const {
text,
matchedStrings,
suggestedUsers,
focusAndUpdateTextAndSelection,
} = props;
+ const shouldRenderAvatars = useShouldRenderAvatars();
+
const { textBeforeAtSymbol, usernamePrefix } = matchedStrings;
const styles = useStyles(unboundStyles);
+ const marginLeftStyle = React.useMemo(
+ () => ({
+ marginLeft: shouldRenderAvatars ? 8 : 0,
+ }),
+ [shouldRenderAvatars],
+ );
+
const renderTypeaheadButton = React.useCallback(
({ item }: { item: RelativeMemberInfo, ... }) => {
const onPress = () => {
const { newText, newSelectionStart } = getNewTextAndSelection(
textBeforeAtSymbol,
text,
usernamePrefix,
item,
);
focusAndUpdateTextAndSelection(newText, {
start: newSelectionStart,
end: newSelectionStart,
});
};
const avatarInfo = getAvatarForUser(item);
return (
);
},
[
- focusAndUpdateTextAndSelection,
styles.button,
styles.buttonLabel,
- text,
+ marginLeftStyle,
textBeforeAtSymbol,
+ text,
usernamePrefix,
+ focusAndUpdateTextAndSelection,
],
);
// This is a hack that was introduced due to a buggy behavior of a
// absolutely positioned FlatList on Android.
// There was a bug that was present when there were too few items in a
// FlatList and it wasn't scrollable. It was only present on Android as
// iOS has a default "bounce" animation, even if the list is too short.
// The bug manifested itself when we tried to scroll the FlatList.
// Because it was unscrollable we were really scrolling FlatList
// below it (in the ChatList) as FlatList here has "position: absolute"
// and is positioned over the other FlatList.
// The hack here solves it by using a PanGestureHandler. This way Pan events
// on TypeaheadTooltip FlatList are always caught by handler.
// When the FlatList is scrollable it scrolls normally, because handler
// passes those events down to it.
// If it's not scrollable, the PanGestureHandler "swallows" them.
// Normally it would trigger onGestureEvent callback, but we don't need to
// handle those events. We just want them to be ignored
// and that's what's actually happening.
const flatList = React.useMemo(
() => (
),
[
renderTypeaheadButton,
styles.container,
styles.contentContainer,
suggestedUsers,
],
);
const listWithConditionalHandler = React.useMemo(() => {
if (Platform.OS === 'android') {
return {flatList};
}
return flatList;
}, [flatList]);
return listWithConditionalHandler;
}
const unboundStyles = {
container: {
position: 'absolute',
maxHeight: 200,
left: 0,
right: 0,
bottom: '100%',
backgroundColor: 'typeaheadTooltipBackground',
borderBottomWidth: 1,
borderTopWidth: 1,
borderColor: 'typeaheadTooltipBorder',
borderStyle: 'solid',
},
contentContainer: {
padding: 8,
},
button: {
alignItems: 'center',
flexDirection: 'row',
innerHeight: 24,
padding: 8,
color: 'typeaheadTooltipText',
},
buttonLabel: {
color: 'white',
fontSize: 16,
fontWeight: '400',
- marginLeft: 8,
},
};
export default TypeaheadTooltip;
diff --git a/native/components/avatar.react.js b/native/components/avatar.react.js
index 6b50aa755..6dc448db1 100644
--- a/native/components/avatar.react.js
+++ b/native/components/avatar.react.js
@@ -1,98 +1,106 @@
// @flow
import * as React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import type { ClientAvatar } from 'lib/types/avatar-types.js';
+import { useShouldRenderAvatars } from '../utils/avatar-utils.js';
+
type Props = {
+avatarInfo: ClientAvatar,
+size?: 'large' | 'small' | 'profile' | 'micro',
};
function Avatar(props: Props): React.Node {
const { avatarInfo, size } = props;
+ const shouldRenderAvatars = useShouldRenderAvatars();
+
const containerSizeStyle = React.useMemo(() => {
if (size === 'profile') {
return styles.profile;
} else if (size === 'small') {
return styles.small;
} else if (size === 'micro') {
return styles.micro;
}
return styles.large;
}, [size]);
const emojiContainerStyle = React.useMemo(() => {
const containerStyles = [styles.emojiContainer, containerSizeStyle];
if (avatarInfo.type === 'emoji') {
const backgroundColor = { backgroundColor: `#${avatarInfo.color}` };
containerStyles.push(backgroundColor);
}
return containerStyles;
}, [avatarInfo, containerSizeStyle]);
const emojiSizeStyle = React.useMemo(() => {
if (size === 'profile') {
return styles.emojiProfile;
} else if (size === 'small') {
return styles.emojiSmall;
} else if (size === 'micro') {
return styles.emojiMicro;
}
return styles.emojiLarge;
}, [size]);
+ if (!shouldRenderAvatars) {
+ return null;
+ }
+
return (
{avatarInfo.emoji}
);
}
const styles = StyleSheet.create({
emojiContainer: {
alignItems: 'center',
justifyContent: 'center',
},
emojiLarge: {
fontSize: 28,
textAlign: 'center',
},
emojiMicro: {
fontSize: 9,
textAlign: 'center',
},
emojiProfile: {
fontSize: 80,
textAlign: 'center',
},
emojiSmall: {
fontSize: 14,
textAlign: 'center',
},
large: {
borderRadius: 20,
height: 40,
width: 40,
},
micro: {
borderRadius: 8,
height: 16,
width: 16,
},
profile: {
borderRadius: 56,
height: 112,
width: 112,
},
small: {
borderRadius: 12,
height: 24,
width: 24,
},
});
export default Avatar;
diff --git a/native/profile/profile-screen.react.js b/native/profile/profile-screen.react.js
index decc9d754..a409bddbd 100644
--- a/native/profile/profile-screen.react.js
+++ b/native/profile/profile-screen.react.js
@@ -1,416 +1,429 @@
// @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 { getAvatarForUser } from 'lib/shared/avatar-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 Avatar from '../components/avatar.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,
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
••••••••••••••••
);
}
const avatarInfo = getAvatarForUser(this.props.currentUserInfo);
const avatar = (
);
+ let avatarSection;
+ if (this.props.shouldRenderAvatars) {
+ avatarSection = (
+ <>
+ USER AVATAR
+ {avatar}
+ >
+ );
+ }
+
return (
- USER AVATAR
- {avatar}
+ {avatarSection}
ACCOUNT
Logged in as
{this.props.stringForUser}
{passwordEditionUI}
PREFERENCES
{defaultNotifications}
{developerTools}
);
}
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;
diff --git a/native/profile/relationship-list-item.react.js b/native/profile/relationship-list-item.react.js
index 067bf3807..7c3d4026b 100644
--- a/native/profile/relationship-list-item.react.js
+++ b/native/profile/relationship-list-item.react.js
@@ -1,351 +1,360 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import {
Alert,
View,
Text,
TouchableOpacity,
ActivityIndicator,
} from 'react-native';
import {
updateRelationshipsActionTypes,
updateRelationships,
} from 'lib/actions/relationship-actions.js';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
import { getAvatarForUser } from 'lib/shared/avatar-utils.js';
import type { LoadingStatus } from 'lib/types/loading-types.js';
import {
type RelationshipRequest,
type RelationshipAction,
type RelationshipErrors,
userRelationshipStatus,
relationshipActions,
} from 'lib/types/relationship-types.js';
import type {
AccountUserInfo,
GlobalAccountUserInfo,
} from 'lib/types/user-types.js';
import {
type DispatchActionPromise,
useServerCall,
useDispatchActionPromise,
} from 'lib/utils/action-utils.js';
import type { RelationshipListNavigate } from './relationship-list.react.js';
import Avatar from '../components/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 {
RelationshipListItemTooltipModalRouteName,
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 { useShouldRenderAvatars } from '../utils/avatar-utils.js';
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: typeof unboundStyles,
// Redux dispatch functions
+dispatchActionPromise: DispatchActionPromise,
// async functions that hit server APIs
+updateRelationships: (
request: RelationshipRequest,
) => Promise,
// withOverlayContext
+overlayContext: ?OverlayContextType,
// withKeyboardState
+keyboardState: ?KeyboardState,
+ +shouldRenderAvatars: boolean,
};
class RelationshipListItem extends React.PureComponent {
editButton = React.createRef>();
render() {
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
);
}
+ const marginLeftStyle = {
+ marginLeft: this.props.shouldRenderAvatars ? 8 : 0,
+ };
+
const avatarInfo = getAvatarForUser(this.props.userInfo);
return (
-
+
{this.props.userInfo.username}
{editButton}
);
}
onSelect = () => {
const { id, username } = this.props.userInfo;
this.props.onSelect({ id, username });
};
visibleEntryIDs() {
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<'RelationshipListItemTooltipModal'>({
name: RelationshipListItemTooltipModalRouteName,
params: {
presentedFrom: this.props.relationshipListRoute.key,
initialCoordinates: coordinates,
verticalBounds,
visibleEntryIDs: this.visibleEntryIDs(),
relativeUserInfo,
},
});
});
};
// 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: RelationshipAction) {
const { id } = this.props.userInfo;
const customKeyName = `${updateRelationshipsActionTypes.started}:${id}`;
this.props.dispatchActionPromise(
updateRelationshipsActionTypes,
this.updateFriendship(action),
{ customKeyName },
);
}
async updateFriendship(action: RelationshipAction) {
try {
return await this.props.updateRelationships({
action,
userIDs: [this.props.userInfo.id],
});
} catch (e) {
Alert.alert('Unknown error', 'Uhh... try again?', [{ text: 'OK' }], {
cancelable: true,
});
throw e;
}
}
}
const unboundStyles = {
editButton: {
paddingLeft: 10,
},
container: {
flex: 1,
paddingHorizontal: 12,
backgroundColor: 'panelForeground',
},
innerContainer: {
paddingVertical: 10,
paddingHorizontal: 12,
borderColor: 'panelForegroundBorder',
flexDirection: 'row',
},
borderBottom: {
borderBottomWidth: 1,
},
buttonContainer: {
flexDirection: 'row',
},
editButtonWithMargin: {
marginLeft: 15,
},
username: {
color: 'panelForegroundSecondaryLabel',
flex: 1,
fontSize: 16,
lineHeight: 20,
marginLeft: 8,
},
blueAction: {
color: 'link',
fontSize: 16,
paddingLeft: 6,
},
redAction: {
color: 'redText',
fontSize: 16,
paddingLeft: 6,
},
};
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 = useServerCall(updateRelationships);
const overlayContext = React.useContext(OverlayContext);
const keyboardState = React.useContext(KeyboardContext);
+ const shouldRenderAvatars = useShouldRenderAvatars();
+
return (
);
});
export default ConnectedRelationshipListItem;
diff --git a/native/utils/avatar-utils.js b/native/utils/avatar-utils.js
new file mode 100644
index 000000000..e8a4651c6
--- /dev/null
+++ b/native/utils/avatar-utils.js
@@ -0,0 +1,14 @@
+// @flow
+
+import * as React from 'react';
+
+import { FeatureFlagsContext } from '../components/feature-flags-provider.react.js';
+
+function useShouldRenderAvatars(): boolean {
+ const { configuration: featureFlagConfig } =
+ React.useContext(FeatureFlagsContext);
+
+ return !!featureFlagConfig['AVATARS_DISPLAY'];
+}
+
+export { useShouldRenderAvatars };