Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show custom profile fields in users' profiles #5398

Merged
merged 14 commits into from
Jun 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/__tests__/lib/exampleData.js
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,9 @@ export const action = Object.freeze({
// InitialDataAlertWords
alert_words: [],

// InitialDataCustomProfileFields
custom_profile_fields: [],

// InitialDataMessage
max_message_id: 100,

Expand Down Expand Up @@ -762,7 +765,14 @@ export const action = Object.freeze({
realm_create_public_stream_policy: 3,
realm_create_web_public_stream_policy: CreateWebPublicStreamPolicy.ModeratorOrAbove,
realm_default_code_block_language: 'python',
realm_default_external_accounts: {},
realm_default_external_accounts: {
github: {
name: 'GitHub',
text: 'GitHub',
hint: 'Enter your GitHub username',
url_pattern: 'https://github.com/%(username)s',
},
},
realm_default_language: 'en',
realm_delete_own_message_policy: 3,
realm_description: 'description',
Expand Down
27 changes: 0 additions & 27 deletions src/account-info/AccountDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,8 @@ import { useSelector } from '../react-redux';
import UserAvatar from '../common/UserAvatar';
import ComponentList from '../common/ComponentList';
import ZulipText from '../common/ZulipText';
import { getOwnUser } from '../selectors';
import { getUserStatus } from '../user-statuses/userStatusesModel';
import PresenceStatusIndicator from '../common/PresenceStatusIndicator';
import ActivityText from '../title/ActivityText';
import { nowInTimeZone } from '../utils/date';

const componentStyles = createStyleSheet({
componentListItem: {
Expand Down Expand Up @@ -43,25 +40,11 @@ type Props = $ReadOnly<{|
export default function AccountDetails(props: Props): Node {
const { user } = props;

const ownUser = useSelector(getOwnUser);
const userStatusText = useSelector(state => getUserStatus(state, props.user.user_id).status_text);
const userStatusEmoji = useSelector(
state => getUserStatus(state, props.user.user_id).status_emoji,
);

const isSelf = user.user_id === ownUser.user_id;

let localTime: string | null = null;
// See comments at CrossRealmBot and User at src/api/modelTypes.js.
if (user.timezone !== '' && user.timezone !== undefined) {
try {
localTime = `${nowInTimeZone(user.timezone)} Local time`;
} catch {
// The set of timezone names in the tz database is subject to change over
// time. Handle unrecognized timezones by quietly discarding them.
}
}

return (
<ComponentList outerSpacing itemStyle={componentStyles.componentListItem}>
<View>
Expand Down Expand Up @@ -96,16 +79,6 @@ export default function AccountDetails(props: Props): Node {
/>
)}
</View>
{!isSelf && (
<View>
<ActivityText style={styles.largerText} user={user} />
</View>
)}
{!isSelf && localTime !== null && (
<View>
<ZulipText style={styles.largerText} text={localTime} />
</View>
)}
</ComponentList>
);
}
34 changes: 33 additions & 1 deletion src/account-info/AccountDetailsScreen.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
/* @flow strict-local */
import React, { useCallback } from 'react';
import type { Node } from 'react';
import { View } from 'react-native';

import type { RouteProp } from '../react-navigation';
import type { AppNavigationProp } from '../nav/AppNavigator';
import type { UserId } from '../types';
import { createStyleSheet } from '../styles';
import globalStyles, { createStyleSheet } from '../styles';
import { useSelector, useDispatch } from '../react-redux';
import Screen from '../common/Screen';
import ZulipButton from '../common/ZulipButton';
import ZulipTextIntl from '../common/ZulipTextIntl';
import { IconPrivateChat } from '../common/Icons';
import { pm1to1NarrowFromUser } from '../utils/narrow';
import AccountDetails from './AccountDetails';
import ZulipText from '../common/ZulipText';
import ActivityText from '../title/ActivityText';
import { doNarrow } from '../actions';
import { getUserIsActive, getUserForId } from '../users/userSelectors';
import { nowInTimeZone } from '../utils/date';
import CustomProfileFields from './CustomProfileFields';

const styles = createStyleSheet({
pmButton: {
Expand All @@ -26,6 +31,11 @@ const styles = createStyleSheet({
fontStyle: 'italic',
fontSize: 18,
},
itemWrapper: {
alignItems: 'center',
marginBottom: 16,
marginHorizontal: 16,
},
});

type Props = $ReadOnly<{|
Expand All @@ -42,6 +52,17 @@ export default function AccountDetailsScreen(props: Props): Node {
dispatch(doNarrow(pm1to1NarrowFromUser(user)));
}, [user, dispatch]);

let localTime: string | null = null;
// See comments at CrossRealmBot and User at src/api/modelTypes.js.
if (user.timezone !== '' && user.timezone !== undefined) {
try {
localTime = `${nowInTimeZone(user.timezone)} Local time`;
} catch {
// The set of timezone names in the tz database is subject to change over
// time. Handle unrecognized timezones by quietly discarding them.
}
}

const title = {
text: '{_}',
values: {
Expand All @@ -53,6 +74,17 @@ export default function AccountDetailsScreen(props: Props): Node {
return (
<Screen title={title}>
<AccountDetails user={user} />
<View style={styles.itemWrapper}>
<ActivityText style={globalStyles.largerText} user={user} />
</View>
{localTime !== null && (
<View style={styles.itemWrapper}>
<ZulipText style={globalStyles.largerText} text={localTime} />
</View>
)}
<View style={styles.itemWrapper}>
<CustomProfileFields user={user} />
</View>
{!isActive && (
<ZulipTextIntl style={styles.deactivatedText} text="(This user has been deactivated)" />
)}
Expand Down
133 changes: 133 additions & 0 deletions src/account-info/CustomProfileFields.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// @flow strict-local
import * as React from 'react';
import { View } from 'react-native';

import { type UserOrBot, type UserId } from '../api/modelTypes';
import WebLink from '../common/WebLink';
import ZulipText from '../common/ZulipText';
import ZulipTextIntl from '../common/ZulipTextIntl';
import { ensureUnreachable } from '../generics';
import { useSelector } from '../react-redux';
import { tryGetUserForId } from '../selectors';
import {
type CustomProfileFieldValue,
getCustomProfileFieldsForUser,
} from '../users/userSelectors';
import UserItem from '../users/UserItem';
import { useNavigation } from '../react-navigation';
import { navigateToAccountDetails } from '../nav/navActions';

/* eslint-disable no-shadow */

type Props = {|
+user: UserOrBot,
|};

function CustomProfileFieldUser(props: {| +userId: UserId |}): React.Node {
const { userId } = props;
const user = useSelector(state => tryGetUserForId(state, userId));

const navigation = useNavigation();
const onPress = React.useCallback(
(user: UserOrBot) => {
navigation.dispatch(navigateToAccountDetails(user.user_id));
},
[navigation],
);

if (!user) {
return <ZulipTextIntl text="(unknown user)" />;
}

return <UserItem userId={userId} onPress={onPress} size="medium" />;
}

function CustomProfileFieldRow(props: {|
+name: string,
+value: CustomProfileFieldValue,
+first: boolean,
|}): React.Node {
const { first, name, value } = props;

const styles = React.useMemo(
() => ({
row: { marginTop: first ? 0 : 8, flexDirection: 'row' },
label: { width: 96, fontWeight: 'bold' },
valueView: { flex: 1, paddingStart: 8 },
valueText: { flex: 1, paddingStart: 8 },
// The padding difference compensates for the paddingHorizontal in UserItem.
valueUnpadded: { flex: 1 },
}),
[first],
);

let valueElement = undefined;
switch (value.displayType) {
case 'text':
valueElement = <ZulipText style={styles.valueText} text={value.text} />;
break;

case 'link':
valueElement = (
<View style={styles.valueView}>
{value.url ? (
<WebLink url={value.url} label={{ text: '{_}', values: { _: value.text } }} />
) : (
<ZulipText text={value.text} />
)}
</View>
);
break;

case 'users':
valueElement = (
<View style={styles.valueUnpadded}>
{value.userIds.map(userId => (
<CustomProfileFieldUser key={userId} userId={userId} />
))}
</View>
);
break;

default:
ensureUnreachable(value.displayType);
return null;
}

return (
<View style={styles.row}>
<ZulipText style={styles.label} text={name} />
{valueElement}
</View>
);
}

export default function CustomProfileFields(props: Props): React.Node {
const { user } = props;
const realm = useSelector(state => state.realm);

const fields = React.useMemo(() => getCustomProfileFieldsForUser(realm, user), [realm, user]);

const styles = React.useMemo(
() => ({
outer: { flexDirection: 'row', justifyContent: 'center' },
inner: { flexBasis: 400, flexShrink: 1 },
}),
[],
);

return (
<View style={styles.outer}>
<View style={styles.inner}>
{fields.map((field, i) => (
<CustomProfileFieldRow
key={field.fieldId}
name={field.name}
value={field.value}
first={i === 0}
/>
))}
</View>
</View>
);
}
23 changes: 19 additions & 4 deletions src/account-info/ProfileScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { useContext } from 'react';
import type { Node } from 'react';
import { ScrollView, View, Alert } from 'react-native';

import { type UserId } from '../api/idTypes';
import { TranslationContext } from '../boot/TranslationProvider';
import type { RouteProp } from '../react-navigation';
import type { MainTabsNavigationProp } from '../main/MainTabsScreen';
Expand All @@ -21,6 +22,7 @@ import AccountDetails from './AccountDetails';
import AwayStatusSwitch from './AwayStatusSwitch';
import { getOwnUser } from '../users/userSelectors';
import { getIdentity } from '../account/accountsSelectors';
import { navigateToAccountDetails } from '../nav/navActions';

const styles = createStyleSheet({
buttonRow: {
Expand All @@ -46,6 +48,19 @@ function SetStatusButton(props: {||}) {
);
}

function ProfileButton(props: {| +ownUserId: UserId |}) {
return (
<ZulipButton
style={styles.button}
secondary
text="Full profile"
onPress={() => {
NavigationService.dispatch(navigateToAccountDetails(props.ownUserId));
}}
/>
);
}

function SettingsButton(props: {||}) {
return (
<ZulipButton
Expand Down Expand Up @@ -112,10 +127,7 @@ type Props = $ReadOnly<{|
|}>;

/**
* This is similar to `AccountDetails` but used to show the current users account.
* It does not have a "Send private message" but has "Switch account" and "Log out" buttons.
*
* The user can still open `AccountDetails` on themselves via the (i) icon in a chat screen.
* The profile/settings/account screen we offer among the main tabs of the app.
*/
export default function ProfileScreen(props: Props): Node {
const ownUser = useSelector(getOwnUser);
Expand All @@ -127,6 +139,9 @@ export default function ProfileScreen(props: Props): Node {
<View style={styles.buttonRow}>
<SetStatusButton />
</View>
<View style={styles.buttonRow}>
<ProfileButton ownUserId={ownUser.user_id} />
</View>
<View style={styles.buttonRow}>
<SettingsButton />
</View>
Expand Down
2 changes: 2 additions & 0 deletions src/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import {
} from './actionConstants';

import type {
CustomProfileFieldsEvent,
MessageEvent,
MutedUsersEvent,
PresenceEvent,
Expand Down Expand Up @@ -328,6 +329,7 @@ type EventSubscriptionPeerRemoveAction = $ReadOnly<{|
type GenericEventAction = $ReadOnly<{|
type: typeof EVENT,
event:
| CustomProfileFieldsEvent
| StreamEvent
| RestartEvent
| RealmUpdateEvent
Expand Down
8 changes: 8 additions & 0 deletions src/api/eventTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import type { SubsetProperties } from '../generics';
import { keyMirror } from '../utils/keyMirror';
import type {
CustomProfileField,
Message,
UserOrBot,
MutedUser,
Expand Down Expand Up @@ -88,6 +89,13 @@ export type HeartbeatEvent = $ReadOnly<{|
type: typeof EventTypes.heartbeat,
|}>;

/** https://zulip.com/api/get-events#custom_profile_fields */
export type CustomProfileFieldsEvent = {|
...EventCommon,
type: typeof EventTypes.custom_profile_fields,
fields: $ReadOnlyArray<CustomProfileField>,
|};

export type MessageEvent = $ReadOnly<{|
...EventCommon,
type: typeof EventTypes.message,
Expand Down
Loading