Skip to content
Merged
3 changes: 3 additions & 0 deletions src/common/Icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ const makeIcon = <Glyphs: string>(
export const IconInbox: SpecificIconType = makeIcon(Feather, 'inbox');
export const IconMention: SpecificIconType = makeIcon(Feather, 'at-sign');
export const IconSearch: SpecificIconType = makeIcon(Feather, 'search');

// SelectableOptionRow depends on this being square.
export const IconDone: SpecificIconType = makeIcon(Feather, 'check');

export const IconCancel: SpecificIconType = makeIcon(Feather, 'slash');
export const IconTrash: SpecificIconType = makeIcon(Feather, 'trash-2');
export const IconSend: SpecificIconType = makeIcon(MaterialIcon, 'send');
Expand Down
67 changes: 44 additions & 23 deletions src/common/SelectableOptionRow.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* @flow strict-local */
import React from 'react';
import React, { useMemo } from 'react';
import type { Node } from 'react';
import { View } from 'react-native';

Expand All @@ -8,25 +8,6 @@ import Touchable from './Touchable';
import { BRAND_COLOR, createStyleSheet } from '../styles';
import { IconDone } from './Icons';

const styles = createStyleSheet({
wrapper: {
flex: 1,
flexDirection: 'column',
},
subtitle: {
fontWeight: '300',
fontSize: 13,
},
listItem: {
flexDirection: 'row',
alignItems: 'center',
paddingTop: 12,
paddingBottom: 12,
paddingLeft: 16,
paddingRight: 16,
},
});

type Props<TItemKey: string | number> = $ReadOnly<{|
// A key to uniquely identify the item. The function passed as
// `onRequestSelectionChange` gets passed this. We would have just
Expand All @@ -47,6 +28,10 @@ type Props<TItemKey: string | number> = $ReadOnly<{|
onRequestSelectionChange: (itemKey: TItemKey, requestedValue: boolean) => void,
|}>;

// The desired height of the checkmark icon, which we'll pass for its `size`
// prop. It'll also be its width, since it's a square.
const kCheckmarkSize = 24;

/**
* A labeled row for an item among related items; shows a checkmark
* when selected.
Expand All @@ -63,14 +48,50 @@ export default function SelectableOptionRow<TItemKey: string | number>(
): Node {
const { itemKey, title, subtitle, selected, onRequestSelectionChange } = props;

const styles = useMemo(
() =>
createStyleSheet({
textWrapper: {
flex: 1,
flexDirection: 'column',
},

// Reserve a space for the checkmark so the layout (e.g., word
// wrapping of the subtitle) doesn't change when `selected` changes.
checkmarkWrapper: {
height: kCheckmarkSize,

// The checkmark icon is a square, so width equals height and this
// is the right amount of width to reserve.
width: kCheckmarkSize,
},

subtitle: {
fontWeight: '300',
fontSize: 13,
},
wrapper: {
flexDirection: 'row',
alignItems: 'center',
paddingTop: 12,
paddingBottom: 12,
paddingLeft: 16,
paddingRight: 16,
},
}),
[],
);

return (
<Touchable onPress={() => onRequestSelectionChange(itemKey, !selected)}>
<View style={styles.listItem}>
<View style={styles.wrapper}>
<View style={styles.wrapper}>
<View style={styles.textWrapper}>
<ZulipText text={title} />
{subtitle !== undefined && <ZulipText text={subtitle} style={styles.subtitle} />}
</View>
<View>{selected && <IconDone size={16} color={BRAND_COLOR} />}</View>
<View style={styles.checkmarkWrapper}>
{selected && <IconDone size={kCheckmarkSize} color={BRAND_COLOR} />}
</View>
</View>
</Touchable>
);
Expand Down
2 changes: 1 addition & 1 deletion src/emoji/EmojiPickerScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type Props = $ReadOnly<{|
// persist navigation state for this screen or set up deep linking to
// it, hence the LogBox suppression below.
//
// React Navigation doesn't offer a more sensible way to do have us
// React Navigation doesn't offer a more sensible way to have us
// pass the emoji data to the calling screen. …We could store the
// emoji data as a route param on the calling screen, or in Redux. But
// from this screen's perspective, that's basically just setting a
Expand Down
6 changes: 3 additions & 3 deletions src/streams/CreateStreamScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default function CreateStreamScreen(props: Props): Node {
const streamsByName = useSelector(getStreamsByName);

const handleComplete = useCallback(
async (name: string, description: string, invite_only: boolean) => {
async (name, description, privacy) => {
// This will miss existing streams that the client can't know about;
// for example, a private stream the user can't access. See comment
// where we catch an `ApiError`, below.
Expand All @@ -38,7 +38,7 @@ export default function CreateStreamScreen(props: Props): Node {
}

try {
await api.createStream(auth, { name, description, invite_only });
await api.createStream(auth, { name, description, invite_only: privacy === 'private' });
NavigationService.dispatch(navigateBack());
} catch (error) {
// If the stream already exists but you can't access it (e.g., it's
Expand All @@ -63,7 +63,7 @@ export default function CreateStreamScreen(props: Props): Node {
<Screen title="Create new stream" padding>
<EditStreamCard
isNewStream
initialValues={{ name: '', description: '', invite_only: false }}
initialValues={{ name: '', description: '', privacy: 'public' }}
onComplete={handleComplete}
/>
</Screen>
Expand Down
121 changes: 56 additions & 65 deletions src/streams/EditStreamCard.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* @flow strict-local */
import React, { PureComponent } from 'react';
import React, { useState, useCallback } from 'react';
import type { Node } from 'react';
import { View } from 'react-native';

Expand All @@ -10,89 +10,80 @@ import ZulipButton from '../common/ZulipButton';
import styles, { createStyleSheet } from '../styles';
import { IconPrivate } from '../common/Icons';

/* eslint-disable no-shadow */

const componentStyles = createStyleSheet({
switchRow: {
paddingLeft: 8,
paddingRight: 8,
},
});

type Privacy = 'public' | 'private';

type Props = $ReadOnly<{|
isNewStream: boolean,
initialValues: {|
name: string,
description: string,
invite_only: boolean,
privacy: Privacy,
|},
onComplete: (name: string, description: string, invite_only: boolean) => void | Promise<void>,
onComplete: (name: string, description: string, privacy: Privacy) => void | Promise<void>,
|}>;

type State = {|
name: string,
description: string,
invite_only: boolean,
|};

export default class EditStreamCard extends PureComponent<Props, State> {
state: State = {
name: this.props.initialValues.name,
description: this.props.initialValues.description,
invite_only: this.props.initialValues.invite_only,
};
export default function EditStreamCard(props: Props): Node {
const { onComplete, initialValues, isNewStream } = props;

handlePerformAction: () => void = () => {
const { onComplete } = this.props;
const { name, description, invite_only } = this.state;
onComplete(name, description, invite_only);
};
const [name, setName] = useState<string>(props.initialValues.name);
const [description, setDescription] = useState<string>(props.initialValues.description);
const [privacy, setPrivacy] = useState<Privacy>(props.initialValues.privacy);

handleNameChange: string => void = name => {
this.setState({ name });
};
const handlePerformAction = useCallback(() => {
onComplete(name, description, privacy);
}, [onComplete, name, description, privacy]);

handleDescriptionChange: string => void = description => {
this.setState({ description });
};
const handleNameChange = useCallback(name => {
setName(name);
}, []);

handleInviteOnlyChange: boolean => void = invite_only => {
this.setState({ invite_only });
};
const handleDescriptionChange = useCallback(description => {
setDescription(description);
}, []);

render(): Node {
const { initialValues, isNewStream } = this.props;
const { name } = this.state;
const handlePrivacyChange = useCallback(isPrivate => {
setPrivacy(isPrivate ? 'private' : 'public');
}, []);

return (
<View>
<ZulipTextIntl text="Name" />
<Input
style={styles.marginBottom}
placeholder="Name"
autoFocus
defaultValue={initialValues.name}
onChangeText={this.handleNameChange}
/>
<ZulipTextIntl text="Description" />
<Input
style={styles.marginBottom}
placeholder="Description"
defaultValue={initialValues.description}
onChangeText={this.handleDescriptionChange}
/>
<SwitchRow
style={componentStyles.switchRow}
Icon={IconPrivate}
label="Private"
value={this.state.invite_only}
onValueChange={this.handleInviteOnlyChange}
/>
<ZulipButton
style={styles.marginTop}
text={isNewStream ? 'Create' : 'Update'}
disabled={name.length === 0}
onPress={this.handlePerformAction}
/>
</View>
);
}
return (
<View>
<ZulipTextIntl text="Name" />
<Input
style={styles.marginBottom}
placeholder="Name"
autoFocus
defaultValue={initialValues.name}
onChangeText={handleNameChange}
/>
<ZulipTextIntl text="Description" />
<Input
style={styles.marginBottom}
placeholder="Description"
defaultValue={initialValues.description}
onChangeText={handleDescriptionChange}
/>
<SwitchRow
style={componentStyles.switchRow}
Icon={IconPrivate}
label="Private"
value={privacy === 'private'}
onValueChange={handlePrivacyChange}
/>
<ZulipButton
style={styles.marginTop}
text={isNewStream ? 'Create' : 'Update'}
disabled={name.length === 0}
onPress={handlePerformAction}
/>
</View>
);
}
12 changes: 9 additions & 3 deletions src/streams/EditStreamScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@ export default function EditStreamScreen(props: Props): Node {
const stream = useSelector(state => getStreamForId(state, props.route.params.streamId));

const handleComplete = useCallback(
(name: string, description: string, invite_only: boolean) => {
dispatch(updateExistingStream(stream.stream_id, stream, { name, description, invite_only }));
(name, description, privacy) => {
dispatch(
updateExistingStream(stream.stream_id, stream, {
name,
description,
invite_only: privacy === 'private',
}),
);
NavigationService.dispatch(navigateBack());
},
[stream, dispatch],
Expand All @@ -35,7 +41,7 @@ export default function EditStreamScreen(props: Props): Node {
initialValues={{
name: stream.name,
description: stream.description,
invite_only: stream.invite_only,
privacy: stream.invite_only ? 'private' : 'public',
}}
onComplete={handleComplete}
/>
Expand Down
10 changes: 9 additions & 1 deletion src/streams/StreamSettingsScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,18 @@ export default function StreamSettingsScreen(props: Props): Node {
)}
<View style={styles.padding}>
{isAdmin && (
// TODO: Group all the stream's attributes together (name,
// description, policies, etc.), with an associated "Edit"
// button that gives a UI for changing those attributes. For the
// grouping, try react-native-paper's `Card`, with a
// ZulipTextButton in its `Card.Actions`. See
// https://callstack.github.io/react-native-paper/card-actions.html
// Or their `Surface`:
// https://callstack.github.io/react-native-paper/surface.html
Comment on lines +102 to +109
Copy link
Copy Markdown
Contributor Author

@chrisbobbe chrisbobbe Apr 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seemed like it might take a while to get react-native-paper set up just right, so this stuff isn't in my plan for getting #5250 out the door. But I wanted to write it down.

<ZulipButton
style={styles.marginTop}
Icon={IconEdit}
text="Edit"
text="Edit stream"
secondary
onPress={() => delay(handlePressEdit)}
/>
Expand Down
1 change: 0 additions & 1 deletion static/translations/messages_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,6 @@
"Notifications": "Notifications",
"Stream": "Stream",
"Private Message": "Private Message",
"Edit": "Edit",
"Muted topics": "Muted topics",
"Edit stream": "Edit stream",
"OK": "OK",
Expand Down