Skip to content

Commit

Permalink
ui [nfc]: Centralize how we choose display text for a user's name
Browse files Browse the repository at this point in the history
Making sure that it chooses '(unknown user)' or 'Muted user', as
appropriate. We're about to add another of that kind of thing --
'Firstname Lastname (guest)' -- for zulip#5804.
  • Loading branch information
chrisbobbe committed Jan 5, 2024
1 parent 889827b commit 1c0266d
Show file tree
Hide file tree
Showing 16 changed files with 149 additions and 65 deletions.
5 changes: 3 additions & 2 deletions src/account-info/AccountDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import PresenceStatusIndicator from '../common/PresenceStatusIndicator';
import { getDisplayEmailForUser } from '../selectors';
import { Role } from '../api/permissionsTypes';
import ZulipTextIntl from '../common/ZulipTextIntl';
import { getFullNameText } from '../users/userSelectors';

const componentStyles = createStyleSheet({
componentListItem: {
Expand Down Expand Up @@ -82,10 +83,10 @@ export default function AccountDetails(props: Props): Node {
hideIfOffline={false}
useOpaqueBackground={false}
/>
<ZulipText
<ZulipTextIntl
selectable
style={[styles.largerText, componentStyles.boldText]}
text={user.full_name}
text={getFullNameText({ user })}
/>
</View>
{displayEmail !== null && showEmail && (
Expand Down
12 changes: 2 additions & 10 deletions src/account-info/AccountDetailsScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import AccountDetails from './AccountDetails';
import ZulipText from '../common/ZulipText';
import ActivityText from '../title/ActivityText';
import { doNarrow } from '../actions';
import { getUserIsActive, tryGetUserForId } from '../users/userSelectors';
import { getFullNameText, getUserIsActive, tryGetUserForId } from '../users/userSelectors';
import { nowInTimeZone } from '../utils/date';
import CustomProfileFields from './CustomProfileFields';

Expand Down Expand Up @@ -79,16 +79,8 @@ export default function AccountDetailsScreen(props: Props): Node {
}
}

const title = {
text: '{_}',
values: {
// This causes the name not to get translated.
_: user.full_name,
},
};

return (
<Screen title={title}>
<Screen title={getFullNameText({ user })}>
<AccountDetails user={user} showEmail showStatus />
<View style={styles.itemWrapper}>
<ActivityText style={globalStyles.largerText} user={user} />
Expand Down
3 changes: 2 additions & 1 deletion src/action-sheets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { roleIsAtLeast } from '../permissionSelectors';
import { kNotificationBotEmail } from '../api/constants';
import type { AppNavigationMethods } from '../nav/AppNavigator';
import { type ImperativeHandle as ComposeBoxImperativeHandle } from '../compose/ComposeBox';
import { getFullNameText } from '../users/userSelectors';

// TODO really this belongs in a libdef.
export type ShowActionSheetWithOptions = (
Expand Down Expand Up @@ -1011,7 +1012,7 @@ export const showPmConversationActionSheet = (args: {|
.map(userId => {
const user = backgroundData.allUsersById.get(userId);
invariant(user, 'allUsersById incomplete; could not show PM action sheet');
return user.full_name;
return callbacks._(getFullNameText({ user }));
})
.sort()
.join(', '),
Expand Down
2 changes: 1 addition & 1 deletion src/compose/ComposeBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,7 @@ const ComposeBox: React$AbstractComponent<Props, ImperativeHandle> = forwardRef(
return <AnnouncementOnly />;
}

const placeholder = getComposeInputPlaceholder(narrow, ownUserId, allUsersById, streamsById);
const placeholder = getComposeInputPlaceholder(narrow, ownUserId, allUsersById, streamsById, _);

const SubmitButtonIcon = isEditing ? IconDone : IconSend;

Expand Down
3 changes: 2 additions & 1 deletion src/compose/MentionWarnings.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { showToast } from '../utils/info';

import MentionedUserNotSubscribed from '../message/MentionedUserNotSubscribed';
import { makeUserId } from '../api/idTypes';
import { getFullNameText } from '../users/userSelectors';

type Props = $ReadOnly<{|
narrow: Narrow,
Expand Down Expand Up @@ -86,7 +87,7 @@ function MentionWarningsInner(props: Props, ref): Node {
(mentionedUser: UserOrBot) => {
showToast(
_('Couldn’t load information about {fullName}', {
fullName: mentionedUser.full_name,
fullName: _(getFullNameText({ user: mentionedUser })),
}),
);
},
Expand Down
41 changes: 36 additions & 5 deletions src/compose/__tests__/getComposeInputPlaceholder-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '../../utils/narrow';
import * as eg from '../../__tests__/lib/exampleData';
import { getStreamsById } from '../../selectors';
import { mock_ } from '../../__tests__/lib/intl';

describe('getComposeInputPlaceholder', () => {
const usersById = new Map([eg.selfUser, eg.otherUser, eg.thirdUser].map(u => [u.user_id, u]));
Expand All @@ -18,7 +19,13 @@ describe('getComposeInputPlaceholder', () => {

test('returns "Message This Person" object for person narrow', () => {
const narrow = deepFreeze(pm1to1NarrowFromUser(eg.otherUser));
const placeholder = getComposeInputPlaceholder(narrow, ownUserId, usersById, streamsById);
const placeholder = getComposeInputPlaceholder(
narrow,
ownUserId,
usersById,
streamsById,
mock_,
);
expect(placeholder).toEqual({
text: 'Message {recipient}',
values: { recipient: eg.otherUser.full_name },
Expand All @@ -27,13 +34,25 @@ describe('getComposeInputPlaceholder', () => {

test('returns "Jot down something" object for self narrow', () => {
const narrow = deepFreeze(pm1to1NarrowFromUser(eg.selfUser));
const placeholder = getComposeInputPlaceholder(narrow, ownUserId, usersById, streamsById);
const placeholder = getComposeInputPlaceholder(
narrow,
ownUserId,
usersById,
streamsById,
mock_,
);
expect(placeholder).toEqual({ text: 'Jot down something' });
});

test('returns "Message #streamName" for stream narrow', () => {
const narrow = deepFreeze(streamNarrow(eg.stream.stream_id));
const placeholder = getComposeInputPlaceholder(narrow, ownUserId, usersById, streamsById);
const placeholder = getComposeInputPlaceholder(
narrow,
ownUserId,
usersById,
streamsById,
mock_,
);
expect(placeholder).toEqual({
text: 'Message {recipient}',
values: { recipient: `#${eg.stream.name}` },
Expand All @@ -42,13 +61,25 @@ describe('getComposeInputPlaceholder', () => {

test('returns properly for topic narrow', () => {
const narrow = deepFreeze(topicNarrow(eg.stream.stream_id, 'Copenhagen'));
const placeholder = getComposeInputPlaceholder(narrow, ownUserId, usersById, streamsById);
const placeholder = getComposeInputPlaceholder(
narrow,
ownUserId,
usersById,
streamsById,
mock_,
);
expect(placeholder).toEqual({ text: 'Reply' });
});

test('returns "Message group" object for group narrow', () => {
const narrow = deepFreeze(pmNarrowFromUsersUnsafe([eg.otherUser, eg.thirdUser]));
const placeholder = getComposeInputPlaceholder(narrow, ownUserId, usersById, streamsById);
const placeholder = getComposeInputPlaceholder(
narrow,
ownUserId,
usersById,
streamsById,
mock_,
);
expect(placeholder).toEqual({ text: 'Message group' });
});
});
6 changes: 4 additions & 2 deletions src/compose/getComposeInputPlaceholder.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
/* @flow strict-local */
import type { Narrow, Stream, UserId, UserOrBot, LocalizableText } from '../types';
import type { Narrow, Stream, UserId, UserOrBot, LocalizableText, GetText } from '../types';
import { getFullNameText } from '../users/userSelectors';
import { caseNarrowDefault } from '../utils/narrow';

export default (
narrow: Narrow,
ownUserId: UserId,
allUsersById: Map<UserId, UserOrBot>,
streamsById: Map<number, Stream>,
_: GetText,
): LocalizableText =>
caseNarrowDefault(
narrow,
Expand All @@ -28,7 +30,7 @@ export default (

return {
text: 'Message {recipient}',
values: { recipient: user.full_name },
values: { recipient: _(getFullNameText({ user })) },
};
},
stream: streamId => {
Expand Down
15 changes: 11 additions & 4 deletions src/lightbox/LightboxHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import UserAvatarWithPresence from '../common/UserAvatarWithPresence';
import { Icon } from '../common/Icons';
import { OfflineNoticePlaceholder } from '../boot/OfflineNoticeProvider';
import { useSelector } from '../react-redux';
import { tryGetUserForId } from '../users/userSelectors';
import { getFullNameText, tryGetUserForId } from '../users/userSelectors';
import type { Message } from '../api/modelTypes';
import ZulipText from '../common/ZulipText';
import ZulipTextIntl from '../common/ZulipTextIntl';

const styles = createStyleSheet({
text: {
Expand Down Expand Up @@ -64,9 +65,15 @@ export default function LightboxHeader(props: Props): Node {
<SafeAreaView mode="padding" edges={['right', 'left']} style={styles.contentArea}>
<UserAvatarWithPresence size={36} userId={senderId} />
<View style={styles.text}>
<ZulipText style={styles.name} numberOfLines={1}>
{sender?.full_name ?? message.sender_full_name}
</ZulipText>
{sender != null ? (
<ZulipTextIntl
style={styles.name}
numberOfLines={1}
text={getFullNameText({ user: sender })}
/>
) : (
<ZulipText style={styles.name} numberOfLines={1} text={message.sender_full_name} />
)}
<ZulipText style={styles.subheader} numberOfLines={1}>
{subheader}
</ZulipText>
Expand Down
7 changes: 6 additions & 1 deletion src/message/MentionedUserNotSubscribed.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import ZulipButton from '../common/ZulipButton';
import ZulipTextIntl from '../common/ZulipTextIntl';
import { getAuth } from '../selectors';
import { kWarningColor } from '../styles/constants';
import { getFullNameText } from '../users/userSelectors';

type Props = $ReadOnly<{|
user: UserOrBot,
Expand Down Expand Up @@ -72,7 +73,11 @@ export default function MentionedUserNotSubscribed(props: Props): Node {
<ZulipTextIntl
text={{
text: '{username} will not be notified unless you subscribe them to this stream.',
values: { username: user.full_name },
values: {
username: (
<ZulipTextIntl inheritColor inheritFontSize text={getFullNameText({ user })} />
),
},
}}
style={styles.text}
/>
Expand Down
5 changes: 2 additions & 3 deletions src/pm-conversations/GroupPmConversationItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Touchable from '../common/Touchable';
import UnreadCount from '../common/UnreadCount';
import { getMutedUsers } from '../selectors';
import { TranslationContext } from '../boot/TranslationProvider';
import { getFullNameOrMutedUserText } from '../users/userSelectors';

const componentStyles = createStyleSheet({
text: {
Expand Down Expand Up @@ -41,9 +42,7 @@ export default function GroupPmConversationItem<U: $ReadOnlyArray<UserOrBot>>(

const _ = useContext(TranslationContext);
const mutedUsers = useSelector(getMutedUsers);
const names = users.map(user =>
mutedUsers.has(user.user_id) ? _('Muted user') : user.full_name,
);
const names = users.map(user => _(getFullNameOrMutedUserText({ user, mutedUsers })));

return (
<Touchable onPress={handlePress}>
Expand Down
14 changes: 9 additions & 5 deletions src/title/TitlePrivate.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import React from 'react';
import type { Node } from 'react';
import { Text, View } from 'react-native';
import { View } from 'react-native';

import type { UserId } from '../types';
import styles, { createStyleSheet } from '../styles';
Expand All @@ -11,8 +11,9 @@ import Touchable from '../common/Touchable';
import ViewPlaceholder from '../common/ViewPlaceholder';
import UserAvatarWithPresence from '../common/UserAvatarWithPresence';
import ActivityText from './ActivityText';
import { tryGetUserForId } from '../users/userSelectors';
import { getFullNameText, tryGetUserForId } from '../users/userSelectors';
import { useNavigation } from '../react-navigation';
import ZulipTextIntl from '../common/ZulipTextIntl';

type Props = $ReadOnly<{|
userId: UserId,
Expand Down Expand Up @@ -47,9 +48,12 @@ export default function TitlePrivate(props: Props): Node {
<UserAvatarWithPresence size={32} userId={user.user_id} />
<ViewPlaceholder width={8} />
<View style={componentStyles.textWrapper}>
<Text style={[styles.navTitle, { color }]} numberOfLines={1} ellipsizeMode="tail">
{user.full_name}
</Text>
<ZulipTextIntl
style={[styles.navTitle, { color }]}
numberOfLines={1}
ellipsizeMode="tail"
text={getFullNameText({ user })}
/>
<ActivityText style={[styles.navSubtitle, { color }]} user={user} />
</View>
</View>
Expand Down
5 changes: 3 additions & 2 deletions src/user-picker/AvatarItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import type AnimatedValue from 'react-native/Libraries/Animated/nodes/AnimatedVa
import type { UserId, UserOrBot } from '../types';
import UserAvatarWithPresence from '../common/UserAvatarWithPresence';
import ComponentWithOverlay from '../common/ComponentWithOverlay';
import ZulipText from '../common/ZulipText';
import Touchable from '../common/Touchable';
import { createStyleSheet } from '../styles';
import { IconCancel } from '../common/Icons';
import { getFullNameText } from '../users/userSelectors';
import ZulipTextIntl from '../common/ZulipTextIntl';

const styles = createStyleSheet({
wrapper: {
Expand Down Expand Up @@ -79,7 +80,7 @@ export default class AvatarItem extends PureComponent<Props> {
</ComponentWithOverlay>
</Touchable>
<View style={styles.textFrame}>
<ZulipText style={styles.text} text={user.full_name} />
<ZulipTextIntl style={styles.text} text={getFullNameText({ user })} />
</View>
</Animated.View>
);
Expand Down
27 changes: 10 additions & 17 deletions src/users/UserItem.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
/* @flow strict-local */
import invariant from 'invariant';
import React, { useCallback, useContext } from 'react';
import React, { useCallback } from 'react';
import type { Node } from 'react';
import { View } from 'react-native';

import { TranslationContext } from '../boot/TranslationProvider';
import type { UserId, UserOrBot } from '../types';
import ZulipText from '../common/ZulipText';
import Touchable from '../common/Touchable';
import UnreadCount from '../common/UnreadCount';
import UserAvatarWithPresence from '../common/UserAvatarWithPresence';
import { createStyleSheet, BRAND_COLOR } from '../styles';
import { useSelector } from '../react-redux';
import { tryGetUserForId } from './userSelectors';
import { getFullNameOrMutedUserText, tryGetUserForId } from './userSelectors';
import { getMutedUsers } from '../selectors';
import { getUserStatus } from '../user-statuses/userStatusesModel';
import { emojiTypeFromReactionType } from '../emoji/data';
import Emoji from '../emoji/Emoji';
import ZulipTextIntl from '../common/ZulipTextIntl';

type Props = $ReadOnly<{|
userId: UserId,
Expand All @@ -39,11 +39,11 @@ export default function UserItem(props: Props): Node {
showEmail = false,
size = 'large',
} = props;
const _ = useContext(TranslationContext);

const user = useSelector(state => tryGetUserForId(state, userId));

const isMuted = useSelector(getMutedUsers).has(userId);
const mutedUsers = useSelector(getMutedUsers);
const isMuted = mutedUsers.has(userId);
const userStatusEmoji = useSelector(
state => user && getUserStatus(state, user.user_id),
)?.status_emoji;
Expand All @@ -56,17 +56,10 @@ export default function UserItem(props: Props): Node {
}, [onPress, user]);
const handlePress = onPress && user ? _handlePress : undefined;

let displayName;
let displayEmail;
if (!user) {
displayName = _('(unknown user)');
displayEmail = null;
} else if (isMuted) {
displayName = _('Muted user');
displayEmail = null;
} else {
displayName = user.full_name;
displayEmail = showEmail ? user.email : null;
const displayName = getFullNameOrMutedUserText({ user, mutedUsers });
let displayEmail = null;
if (user != null && !isMuted && showEmail) {
displayEmail = user.email;
}

const styles = React.useMemo(
Expand Down Expand Up @@ -116,7 +109,7 @@ export default function UserItem(props: Props): Node {
onPress={handlePress}
/>
<View style={styles.textWrapper}>
<ZulipText
<ZulipTextIntl
style={[styles.text, isSelected && styles.selectedText]}
text={displayName}
numberOfLines={1}
Expand Down
Loading

0 comments on commit 1c0266d

Please sign in to comment.