Skip to content

Commit

Permalink
EditStreamCard: Offer all four stream-privacy settings
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbobbe committed May 24, 2022
1 parent 9376144 commit 7fe7d78
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 14 deletions.
159 changes: 152 additions & 7 deletions src/streams/EditStreamCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import type { Node } from 'react';
import { View } from 'react-native';

import type { Privacy } from './streamsActions';
import { ensureUnreachable } from '../types';
import type { LocalizableText } from '../types';
import { useSelector } from '../react-redux';
import { getRealm, getOwnUser, getRealmUrl } from '../selectors';
import { Role } from '../api/permissionsTypes';
import { getCanCreateWebPublicStreams } from '../permissionSelectors';
import type { AppNavigationProp } from '../nav/AppNavigator';
import Input from '../common/Input';
import InputRowRadioButtons from '../common/InputRowRadioButtons';
Expand All @@ -25,6 +31,151 @@ type Props = $ReadOnly<{|
onComplete: (name: string, description: string, privacy: Privacy) => void | Promise<void>,
|}>;

function useStreamPrivacyOptions(initialValue: Privacy) {
const {
webPublicStreamsEnabled,
enableSpectatorAccess,
createWebPublicStreamPolicy,
name: realmName,
} = useSelector(getRealm);
const canCreateWebPublicStreams = useSelector(getCanCreateWebPublicStreams);
const { role } = useSelector(getOwnUser);
const realmUrl = useSelector(getRealmUrl);

return useMemo(
() =>
[
webPublicStreamsEnabled && enableSpectatorAccess
? {
key: 'web-public',
title: 'Web-public',
subtitle:
'Organization members can join (guests must be invited by a subscriber); anyone on the Internet can view complete message history without creating an account',

// See comment where we use this.
disabledIfNotInitialValue: !canCreateWebPublicStreams
? {
title: 'Insufficient permission',
message: (() => {
switch (createWebPublicStreamPolicy) {
// FlowIssue: sad that we end up having to write numeric literals here :-/
// But the most important thing to get from the type-checker here is
// that the ensureUnreachable works -- that ensures that when we add a
// new possible value, we'll add a case for it here. Couldn't find a
// cleaner way to write this that still accomplished that. Discussion:
// https://github.com/zulip/zulip-mobile/pull/5384#discussion_r875147220
case 6: // CreateWebPublicStreamPolicy.Nobody
return {
text:
role === Role.Admin || role === Role.Owner
? '{realmName} does not allow anybody to make web-public streams. To change this setting, please open {realmUrl} in your browser.'
: '{realmName} does not allow anybody to make web-public streams.',
values: { realmName, realmUrl: realmUrl.toString() },
};
case 7: // CreateWebPublicStreamPolicy.OwnerOnly
return {
text:
role === Role.Admin
? '{realmName} only allows organization owners to make web-public streams. To change this setting, please open {realmUrl} in your browser.'
: '{realmName} only allows organization owners to make web-public streams.',
values: { realmName, realmUrl: realmUrl.toString() },
};
case 2: // CreateWebPublicStreamPolicy.AdminOrAbove
return {
text:
'{realmName} only allows organization admins or owners to make web-public streams.',
values: { realmName },
};
case 4: // CreateWebPublicStreamPolicy.ModeratorOrAbove
return {
text:
'{realmName} only allows organization moderators, admins, or owners to make web-public streams.',
values: { realmName },
};
default: {
ensureUnreachable(createWebPublicStreamPolicy);

// (Unreachable as long as the cases are exhaustive.)
return '';
}
}
})(),
learnMoreUrl:
role === Role.Admin || role === Role.Owner
? new URL('/help/configure-who-can-create-streams', realmUrl)
: undefined,
}
: false,
}
: undefined,
{
key: 'public',
title: 'Public',
subtitle:
'Organization members can join (guests must be invited by a subscriber); organization members can view complete message history without joining',
},
{
key: 'invite-only-public-history',
title: 'Private, shared history',
subtitle:
'Must be invited by a subscriber; new subscribers can view complete message history; hidden from non-administrator users',
},
{
key: 'invite-only',
title: 'Private, protected history',
subtitle:
'Must be invited by a subscriber; new subscribers can only see messages sent after they join; hidden from non-administrator users',
},
]
.filter(Boolean)
.map(
(x: {|
key: Privacy,
title: LocalizableText,
subtitle?: LocalizableText,
disabledIfNotInitialValue?:
| {|
+title: LocalizableText,
+message?: LocalizableText,
+learnMoreUrl?: URL,
|}
| false,
|}) => {
const { disabledIfNotInitialValue, ...rest } = x; // eslint-disable-line no-unused-vars

return {
...rest,

// E.g., if editing an existing stream that's already
// web-public, let the user keep it that way, even if they
// wouldn't have permission to switch to web-public from
// something else.
//
// As implemented, we check against initialValue, the value
// when the form was initialized. That could differ from the
// current value. This makes it less likely for the form to
// jump out from under the user while they're looking at it,
// but it slightly raises the chance of sending a disallowed
// request to make a stream web-public. That's fine; the
// server should give an informative error, which we should
// show.
disabled: x.key !== initialValue && disabledIfNotInitialValue,
};
},
),
[
initialValue,
webPublicStreamsEnabled,
canCreateWebPublicStreams,
createWebPublicStreamPolicy,
enableSpectatorAccess,
realmName,
realmUrl,
role,
],
);
}

export default function EditStreamCard(props: Props): Node {
const { navigation, onComplete, initialValues, isNewStream } = props;

Expand All @@ -36,13 +187,7 @@ export default function EditStreamCard(props: Props): Node {
onComplete(name, description, privacy);
}, [onComplete, name, description, privacy]);

const privacyOptions = useMemo(
() => [
{ key: 'public', title: 'Public' },
{ key: 'private', title: 'Private' },
],
[],
);
const privacyOptions = useStreamPrivacyOptions(props.initialValues.privacy);

return (
<View>
Expand Down
38 changes: 31 additions & 7 deletions src/streams/streamsActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,35 @@ import { getAuth, getZulipFeatureLevel } from '../selectors';
import { ensureUnreachable } from '../generics';
import type { SubsetProperties } from '../generics';

export type Privacy = 'public' | 'private';
export type Privacy = 'web-public' | 'public' | 'invite-only-public-history' | 'invite-only';

type StreamPrivacyProps = SubsetProperties<Stream, { +invite_only: mixed, ... }>;
type StreamPrivacyProps = SubsetProperties<
Stream,
{ +is_web_public: mixed, +invite_only: mixed, +history_public_to_subscribers: mixed, ... },
>;

export const streamPropsToPrivacy = (streamProps: StreamPrivacyProps): Privacy =>
streamProps.invite_only ? 'private' : 'public';
export const streamPropsToPrivacy = (streamProps: StreamPrivacyProps): Privacy => {
if (streamProps.is_web_public === true) {
return 'web-public';
} else if (streamProps.invite_only === false) {
return 'public';
} else if (streamProps.history_public_to_subscribers === true) {
return 'invite-only-public-history';
} else {
return 'invite-only';
}
};

export const privacyToStreamProps = (privacy: Privacy): $Exact<StreamPrivacyProps> => {
switch (privacy) {
case 'private':
return { invite_only: true };
case 'web-public':
return { is_web_public: true, invite_only: false, history_public_to_subscribers: true };
case 'public':
return { invite_only: false };
return { is_web_public: false, invite_only: false, history_public_to_subscribers: true };
case 'invite-only-public-history':
return { is_web_public: false, invite_only: true, history_public_to_subscribers: true };
case 'invite-only':
return { is_web_public: false, invite_only: true, history_public_to_subscribers: false };
default:
ensureUnreachable(privacy);

Expand Down Expand Up @@ -53,7 +69,15 @@ export const updateExistingStream = (
if (initialPrivacy !== newData.privacy) {
const streamProps = privacyToStreamProps(newData.privacy);

// Only send is_web_public if the server will recognize it. Will it? It
// should, if we've let the user switch a stream from web-public to not
// or vice versa.
// TODO(server-2.1): Remove conditional.
if (initialPrivacy === 'web-public' || newData.privacy === 'web-public') {
updates.is_web_public = streamProps.is_web_public;
}
updates.is_private = streamProps.invite_only;
updates.history_public_to_subscribers = streamProps.history_public_to_subscribers;
}

if (Object.keys(updates).length === 0) {
Expand Down
14 changes: 14 additions & 0 deletions static/translations/messages_en.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
{
"Cannot apply requested settings": "Cannot apply requested settings",
"Insufficient permission": "Insufficient permission",
"Web-public": "Web-public",
"Organization members can join (guests must be invited by a subscriber); anyone on the Internet can view complete message history without creating an account": "Organization members can join (guests must be invited by a subscriber); anyone on the Internet can view complete message history without creating an account",
"{realmName} does not allow anybody to make web-public streams. To change this setting, please open {realmUrl} in your browser.": "{realmName} does not allow anybody to make web-public streams. To change this setting, please open {realmUrl} in your browser.",
"{realmName} does not allow anybody to make web-public streams.": "{realmName} does not allow anybody to make web-public streams.",
"{realmName} only allows organization owners to make web-public streams. To change this setting, please open {realmUrl} in your browser.": "{realmName} only allows organization owners to make web-public streams. To change this setting, please open {realmUrl} in your browser.",
"{realmName} only allows organization owners to make web-public streams.": "{realmName} only allows organization owners to make web-public streams.",
"{realmName} only allows organization admins or owners to make web-public streams.": "{realmName} only allows organization admins or owners to make web-public streams.",
"{realmName} only allows organization moderators, admins, or owners to make web-public streams.": "{realmName} only allows organization moderators, admins, or owners to make web-public streams.",
"Organization members can join (guests must be invited by a subscriber); organization members can view complete message history without joining": "Organization members can join (guests must be invited by a subscriber); organization members can view complete message history without joining",
"Private, shared history": "Private, shared history",
"Must be invited by a subscriber; new subscribers can view complete message history; hidden from non-administrator users": "Must be invited by a subscriber; new subscribers can view complete message history; hidden from non-administrator users",
"Private, protected history": "Private, protected history",
"Must be invited by a subscriber; new subscribers can only see messages sent after they join; hidden from non-administrator users": "Must be invited by a subscriber; new subscribers can only see messages sent after they join; hidden from non-administrator users",
"Cannot subscribe to stream": "Cannot subscribe to stream",
"Stream #{name} is private.": "Stream #{name} is private.",
"See details": "See details",
Expand Down

0 comments on commit 7fe7d78

Please sign in to comment.