Skip to content

Commit 0a10831

Browse files
committed
user: Show custom profile fields in a user's profile
Fixes: zulip#2900
1 parent a2113af commit 0a10831

File tree

3 files changed

+138
-0
lines changed

3 files changed

+138
-0
lines changed

src/account-info/AccountDetailsScreen.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import ActivityText from '../title/ActivityText';
1919
import { doNarrow } from '../actions';
2020
import { getUserIsActive, getUserForId } from '../users/userSelectors';
2121
import { nowInTimeZone } from '../utils/date';
22+
import CustomProfileFields from './CustomProfileFields';
2223

2324
const styles = createStyleSheet({
2425
pmButton: {
@@ -81,6 +82,9 @@ export default function AccountDetailsScreen(props: Props): Node {
8182
<ZulipText style={globalStyles.largerText} text={localTime} />
8283
</View>
8384
)}
85+
<View style={styles.itemWrapper}>
86+
<CustomProfileFields user={user} />
87+
</View>
8488
{!isActive && (
8589
<ZulipTextIntl style={styles.deactivatedText} text="(This user has been deactivated)" />
8690
)}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// @flow strict-local
2+
import * as React from 'react';
3+
import { View } from 'react-native';
4+
5+
import { type UserOrBot, type UserId } from '../api/modelTypes';
6+
import WebLink from '../common/WebLink';
7+
import ZulipText from '../common/ZulipText';
8+
import ZulipTextIntl from '../common/ZulipTextIntl';
9+
import { ensureUnreachable } from '../generics';
10+
import { useSelector } from '../react-redux';
11+
import { tryGetUserForId } from '../selectors';
12+
import {
13+
type CustomProfileFieldValue,
14+
getCustomProfileFieldsForUser,
15+
} from '../users/userSelectors';
16+
import UserItem from '../users/UserItem';
17+
import { useNavigation } from '../react-navigation';
18+
import { navigateToAccountDetails } from '../nav/navActions';
19+
20+
/* eslint-disable no-shadow */
21+
22+
type Props = {|
23+
+user: UserOrBot,
24+
|};
25+
26+
function CustomProfileFieldUser(props: {| +userId: UserId |}): React.Node {
27+
const { userId } = props;
28+
const user = useSelector(state => tryGetUserForId(state, userId));
29+
30+
const navigation = useNavigation();
31+
const onPress = React.useCallback(
32+
(user: UserOrBot) => {
33+
navigation.dispatch(navigateToAccountDetails(user.user_id));
34+
},
35+
[navigation],
36+
);
37+
38+
if (!user) {
39+
return <ZulipTextIntl text="(unknown user)" />;
40+
}
41+
42+
return <UserItem userId={userId} onPress={onPress} size="medium" />;
43+
}
44+
45+
function CustomProfileFieldRow(props: {|
46+
+name: string,
47+
+value: CustomProfileFieldValue,
48+
+first: boolean,
49+
|}): React.Node {
50+
const { first, name, value } = props;
51+
52+
const styles = React.useMemo(
53+
() => ({
54+
row: { marginTop: first ? 0 : 8, flexDirection: 'row' },
55+
label: { width: 96, fontWeight: 'bold' },
56+
valueView: { flex: 1, paddingStart: 8 },
57+
valueText: { flex: 1, paddingStart: 8 },
58+
// The padding difference compensates for the paddingHorizontal in UserItem.
59+
valueUnpadded: { flex: 1 },
60+
}),
61+
[first],
62+
);
63+
64+
let valueElement = undefined;
65+
switch (value.displayType) {
66+
case 'text':
67+
valueElement = <ZulipText style={styles.valueText} text={value.text} />;
68+
break;
69+
70+
case 'link':
71+
valueElement = (
72+
<View style={styles.valueView}>
73+
{value.url ? (
74+
<WebLink url={value.url} label={{ text: '{_}', values: { _: value.text } }} />
75+
) : (
76+
<ZulipText text={value.text} />
77+
)}
78+
</View>
79+
);
80+
break;
81+
82+
case 'users':
83+
valueElement = (
84+
<View style={styles.valueUnpadded}>
85+
{value.userIds.map(userId => (
86+
<CustomProfileFieldUser key={userId} userId={userId} />
87+
))}
88+
</View>
89+
);
90+
break;
91+
92+
default:
93+
ensureUnreachable(value.displayType);
94+
return null;
95+
}
96+
97+
return (
98+
<View style={styles.row}>
99+
<ZulipText style={styles.label} text={name} />
100+
{valueElement}
101+
</View>
102+
);
103+
}
104+
105+
export default function CustomProfileFields(props: Props): React.Node {
106+
const { user } = props;
107+
const realm = useSelector(state => state.realm);
108+
109+
const fields = React.useMemo(() => getCustomProfileFieldsForUser(realm, user), [realm, user]);
110+
111+
const styles = React.useMemo(
112+
() => ({
113+
outer: { flexDirection: 'row', justifyContent: 'center' },
114+
inner: { flexBasis: 400, flexShrink: 1 },
115+
}),
116+
[],
117+
);
118+
119+
return (
120+
<View style={styles.outer}>
121+
<View style={styles.inner}>
122+
{fields.map((field, i) => (
123+
<CustomProfileFieldRow
124+
key={field.fieldId}
125+
name={field.name}
126+
value={field.value}
127+
first={i === 0}
128+
/>
129+
))}
130+
</View>
131+
</View>
132+
);
133+
}

static/translations/messages_en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@
186186
"Send private message": "Send private message",
187187
"View private messages": "View private messages",
188188
"(This user has been deactivated)": "(This user has been deactivated)",
189+
"(unknown user)": "(unknown user)",
189190
"Forgot password?": "Forgot password?",
190191
"Members": "Members",
191192
"Recipients": "Recipients",

0 commit comments

Comments
 (0)