Skip to content

Commit 6df6dab

Browse files
committed
LegalScreen: Turn into a global, all-accounts screen
The user should be equally aware of the terms for all realms the app is interacting with, and that includes realms other than the active account's realm. (There would be even more interaction in a future with better multi-account support.) Rather than make the user traverse all realms with the "active account" pointer to find all the relevant terms, it seems nicer to show them together on this one page.
1 parent 4cbb4e7 commit 6df6dab

File tree

2 files changed

+98
-28
lines changed

2 files changed

+98
-28
lines changed

src/nav/AppNavigator.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,6 @@ export default function AppNavigator(props: Props): Node {
184184
name="notif-troubleshooting"
185185
component={useHaveServerDataGate(NotifTroubleshootingScreen)}
186186
/>
187-
<Stack.Screen name="legal" component={useHaveServerDataGate(LegalScreen)} />
188187
<Stack.Screen name="user-status" component={useHaveServerDataGate(UserStatusScreen)} />
189188
<Stack.Screen name="settings" component={useHaveServerDataGate(SettingsScreen)} />
190189
<Stack.Screen name="read-receipts" component={useHaveServerDataGate(ReadReceiptsScreen)} />
@@ -205,6 +204,7 @@ export default function AppNavigator(props: Props): Node {
205204
}
206205
/>
207206
<Stack.Screen name="sharing" component={SharingScreen} />
207+
<Stack.Screen name="legal" component={LegalScreen} />
208208
<Stack.Screen name="selectable-options" component={SelectableOptionsScreen} />
209209
</Stack.Navigator>
210210
);

src/settings/LegalScreen.js

Lines changed: 97 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,56 +3,126 @@
33
import React, { useCallback } from 'react';
44
import type { Node } from 'react';
55

6+
import { createSelector } from 'reselect';
67
import type { RouteProp } from '../react-navigation';
78
import type { AppNavigationProp } from '../nav/AppNavigator';
8-
import { useGlobalSelector, useSelector } from '../react-redux';
9+
import { useGlobalSelector } from '../react-redux';
910
import Screen from '../common/Screen';
1011
import NavRow from '../common/NavRow';
1112
import ZulipText from '../common/ZulipText';
1213
import { openLinkWithUserPreference } from '../utils/openLink';
13-
import { getRealmUrl, getRealmName, getGlobalSettings } from '../selectors';
14+
import { getRealmName, getGlobalSettings } from '../selectors';
15+
import { getAccounts } from '../directSelectors';
16+
import type { GlobalSelector } from '../reduxTypes';
17+
import { getAccount, tryGetActiveAccountState } from '../account/accountsSelectors';
18+
import { identityOfAccount, keyOfIdentity } from '../account/accountMisc';
19+
import { getHaveServerData } from '../haveServerDataSelectors';
20+
21+
/**
22+
* Data for all realms represented in `state.accounts`, logged-in or not,
23+
* unique by URL.
24+
*
25+
* The realm name will be missing when we don't have server data for any
26+
* account on the realm.
27+
*/
28+
type ViewModel = $ReadOnlyArray<{|
29+
+realm: URL,
30+
+name: string | null,
31+
+policiesUrl: URL,
32+
|}>;
33+
34+
const getViewModel: GlobalSelector<ViewModel> = createSelector(
35+
getAccounts,
36+
tryGetActiveAccountState,
37+
(accounts, activeAccountState) => {
38+
const result = new Map(accounts.map(a => [a.realm.toString(), null]));
39+
40+
accounts.forEach(account => {
41+
const realmStr = account.realm.toString();
42+
43+
if (result.get(realmStr) != null) {
44+
return;
45+
}
46+
47+
// TODO(#5006): Add realm name for any account we have server data for,
48+
// not just the active account.
49+
if (
50+
activeAccountState
51+
&& keyOfIdentity(identityOfAccount(getAccount(activeAccountState)))
52+
=== keyOfIdentity(identityOfAccount(account))
53+
&& getHaveServerData(activeAccountState)
54+
) {
55+
result.set(realmStr, getRealmName(activeAccountState));
56+
}
57+
});
58+
59+
return [...result.entries()].map(([realmStr, name]) => {
60+
const realm = new URL(realmStr);
61+
return {
62+
realm,
63+
name,
64+
policiesUrl: new URL('/policies/?nav=no', realm),
65+
};
66+
});
67+
},
68+
);
1469

1570
type Props = $ReadOnly<{|
1671
navigation: AppNavigationProp<'legal'>,
1772
route: RouteProp<'legal', void>,
1873
|}>;
1974

20-
/** (NB this is a per-account screen: it leads to this realm's policies.) */
75+
/**
76+
* A global, all-accounts screen linking to terms for all realms we know about.
77+
*/
2178
export default function LegalScreen(props: Props): Node {
22-
const realm = useSelector(getRealmUrl);
23-
const realmName = useSelector(getRealmName);
79+
const viewModel = useGlobalSelector(getViewModel);
2480

2581
const globalSettings = useGlobalSelector(getGlobalSettings);
2682

2783
const openZulipPolicies = useCallback(() => {
2884
openLinkWithUserPreference(new URL('https://zulip.com/policies/?nav=no'), globalSettings);
2985
}, [globalSettings]);
3086

31-
const openRealmPolicies = useCallback(() => {
32-
openLinkWithUserPreference(new URL('/policies/?nav=no', realm), globalSettings);
33-
}, [realm, globalSettings]);
34-
3587
return (
3688
<Screen title="Legal">
3789
<NavRow title="Zulip terms" onPress={openZulipPolicies} type="external" />
38-
<NavRow
39-
// These are really terms set by the server admin responsible for
40-
// hosting the org, and that server admin may or may not represent
41-
// the org itself, as this text might be read to imply. (E.g.,
42-
// on Zulip Cloud they don't.) But:
43-
// - We don't want to complicate the wording. Not everyone knows
44-
// what a server is.
45-
// - These terms will often differ from Zulip's own terms (the ones
46-
// at the other link).
47-
// - These terms will apply to all users in the org, in all cases.
48-
// We should link to them.
49-
title={{
50-
text: 'Terms for {realmName}',
51-
values: { realmName: <ZulipText style={{ fontWeight: 'bold' }} text={realmName} /> },
52-
}}
53-
onPress={openRealmPolicies}
54-
type="external"
55-
/>
90+
{viewModel.map(({ realm, name, policiesUrl }) => (
91+
<NavRow
92+
key={realm.toString()}
93+
// These are really terms set by the server admin responsible for
94+
// hosting the org, and that server admin may or may not represent
95+
// the org itself, as this text might be read to imply. (E.g.,
96+
// on Zulip Cloud they don't.) But:
97+
// - We don't want to complicate the wording. Not everyone knows
98+
// what a server is.
99+
// - These terms will often differ from Zulip's own terms (the ones
100+
// at the "Zulip terms" link).
101+
// - These terms will apply to all users in the org, in all cases.
102+
// We should link to them.
103+
title={{
104+
text: 'Terms for {realmName}',
105+
values: {
106+
realmName: (
107+
// The realm name comes from server data. If we don't
108+
// have server data, fall back on the realm URL.
109+
<ZulipText style={{ fontWeight: 'bold' }} text={name ?? realm.toString()} />
110+
),
111+
},
112+
}}
113+
subtitle={
114+
// It's nice to be explicit about where the policies live,
115+
// though the "?nav=no" is a bit annoying. But also, this line
116+
// disambiguates multiple realms with the same name; the name is
117+
// shown (when we have it) in `title`.
118+
{ text: '{_}', values: { _: policiesUrl.toString() } }
119+
}
120+
onPress={() => {
121+
openLinkWithUserPreference(policiesUrl, globalSettings);
122+
}}
123+
type="external"
124+
/>
125+
))}
56126
</Screen>
57127
);
58128
}

0 commit comments

Comments
 (0)