Skip to content

Commit 75bd9cf

Browse files
Leslie NgoLeslie Ngo
Leslie Ngo
authored and
Leslie Ngo
committed
topic edit modal: Add new edit-topic UI.
Fixes: zulip#5365
1 parent 7122b2d commit 75bd9cf

13 files changed

+301
-12
lines changed

src/ZulipMobile.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import AppEventHandlers from './boot/AppEventHandlers';
1717
import { initializeSentry } from './sentry';
1818
import ZulipSafeAreaProvider from './boot/ZulipSafeAreaProvider';
1919
import { OfflineNoticeProvider } from './boot/OfflineNoticeProvider';
20+
import TopicEditModalProvider from './boot/TopicEditModalProvider';
2021

2122
initializeSentry();
2223

@@ -79,9 +80,11 @@ export default function ZulipMobile(): Node {
7980
<TranslationProvider>
8081
<ThemeProvider>
8182
<OfflineNoticeProvider>
82-
<ActionSheetProvider>
83-
<ZulipNavigationContainer />
84-
</ActionSheetProvider>
83+
<TopicEditModalProvider>
84+
<ActionSheetProvider>
85+
<ZulipNavigationContainer />
86+
</ActionSheetProvider>
87+
</TopicEditModalProvider>
8588
</OfflineNoticeProvider>
8689
</ThemeProvider>
8790
</TranslationProvider>

src/action-sheets/index.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ type TopicArgs = {
7979
zulipFeatureLevel: number,
8080
dispatch: Dispatch,
8181
_: GetText,
82+
startEditTopic: (streamId: number, topic: string) => void,
8283
...
8384
};
8485

@@ -171,6 +172,14 @@ const deleteMessage = {
171172
},
172173
};
173174

175+
const editTopic = {
176+
title: 'Edit topic',
177+
errorMessage: 'Failed to edit topic',
178+
action: ({ streamId, topic, startEditTopic }) => {
179+
startEditTopic(streamId, topic);
180+
},
181+
};
182+
174183
const markTopicAsRead = {
175184
title: 'Mark topic as read',
176185
errorMessage: 'Failed to mark topic as read',
@@ -532,9 +541,18 @@ export const constructTopicActionButtons = (args: {|
532541

533542
const buttons = [];
534543
const unreadCount = getUnreadCountForTopic(unread, streamId, topic);
544+
const isAdmin = roleIsAtLeast(ownUserRole, Role.Admin);
535545
if (unreadCount > 0) {
536546
buttons.push(markTopicAsRead);
537547
}
548+
// At present, the permissions for editing the topic of a message are highly complex.
549+
// Until we move to a better set of policy options, we'll only display the edit topic
550+
// button to admins.
551+
// Issue: https://github.com/zulip/zulip/issues/21739
552+
// Relevant comment: https://github.com/zulip/zulip-mobile/issues/5365#issuecomment-1197093294
553+
if (isAdmin) {
554+
buttons.push(editTopic);
555+
}
538556
if (isTopicMuted(streamId, topic, mute)) {
539557
buttons.push(unmuteTopic);
540558
} else {
@@ -545,7 +563,7 @@ export const constructTopicActionButtons = (args: {|
545563
} else {
546564
buttons.push(unresolveTopic);
547565
}
548-
if (roleIsAtLeast(ownUserRole, Role.Admin)) {
566+
if (isAdmin) {
549567
buttons.push(deleteTopic);
550568
}
551569
const sub = subscriptions.get(streamId);
@@ -705,6 +723,7 @@ export const showTopicActionSheet = (args: {|
705723
showActionSheetWithOptions: ShowActionSheetWithOptions,
706724
callbacks: {|
707725
dispatch: Dispatch,
726+
startEditTopic: (streamId: number, topic: string) => void,
708727
_: GetText,
709728
|},
710729
backgroundData: $ReadOnly<{

src/boot/TopicEditModalProvider.js

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/* @flow strict-local */
2+
import React, { createContext, useState, useCallback, useContext } from 'react';
3+
import type { Context, Node } from 'react';
4+
5+
import TopicEditModal from '../topics/TopicEditModal';
6+
7+
type Props = $ReadOnly<{|
8+
children: Node,
9+
|}>;
10+
11+
type TopicEditModalProviderState = {
12+
streamId: number,
13+
oldTopic: string,
14+
} | null;
15+
16+
type StartEditTopicContext = (streamId: number, oldTopic: string) => void;
17+
18+
const TopicEditModalContext: Context<StartEditTopicContext> = createContext(() => {
19+
throw new Error(
20+
'Tried to open the edit-topic UI from a component without TopicEditModalProvider above it in the tree.',
21+
);
22+
});
23+
24+
export const useStartEditTopic = (): StartEditTopicContext => useContext(TopicEditModalContext);
25+
26+
export default function TopicEditModalProvider(props: Props): Node {
27+
const { children } = props;
28+
29+
const [topicModalProviderState, setTopicModalProviderState] =
30+
useState<TopicEditModalProviderState>(null);
31+
32+
const startEditTopic = useCallback(
33+
(streamIdArg, oldTopicArg) => {
34+
if (!topicModalProviderState) {
35+
setTopicModalProviderState({
36+
streamId: streamIdArg,
37+
oldTopic: oldTopicArg,
38+
});
39+
}
40+
},
41+
[topicModalProviderState],
42+
);
43+
44+
const closeEditTopicModal = () => {
45+
setTopicModalProviderState(null);
46+
};
47+
48+
return (
49+
<TopicEditModalContext.Provider value={startEditTopic}>
50+
<TopicEditModal
51+
topicModalProviderState={topicModalProviderState}
52+
closeEditTopicModal={closeEditTopicModal}
53+
/>
54+
{children}
55+
</TopicEditModalContext.Provider>
56+
);
57+
}

src/chat/ChatScreen.js

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { showErrorAlert } from '../utils/info';
2929
import { TranslationContext } from '../boot/TranslationProvider';
3030
import * as api from '../api';
3131
import { useConditionalEffect } from '../reactUtils';
32+
import { useStartEditTopic } from '../boot/TopicEditModalProvider';
3233

3334
type Props = $ReadOnly<{|
3435
navigation: AppNavigationProp<'chat'>,
@@ -132,6 +133,7 @@ export default function ChatScreen(props: Props): Node {
132133
(value: EditMessage | null) => navigation.setParams({ editMessage: value }),
133134
[navigation],
134135
);
136+
const startEditTopic = useStartEditTopic();
135137

136138
const isNarrowValid = useSelector(state => getIsNarrowValid(state, narrow));
137139
const draft = useSelector(state => getDraftForNarrow(state, narrow));
@@ -219,6 +221,7 @@ export default function ChatScreen(props: Props): Node {
219221
}
220222
showMessagePlaceholders={showMessagePlaceholders}
221223
startEditMessage={setEditMessage}
224+
startEditTopic={startEditTopic}
222225
/>
223226
);
224227
}

src/common/ZulipTextButton.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,13 @@ type Props = $ReadOnly<{|
7878

7979
/**
8080
* True just if the button is in the disabled state.
81-
* https://material.io/design/interaction/states.html#disabled
81+
*
82+
* https://material.io/design/interaction/states.html#disabled
8283
*/
8384
disabled?: boolean,
8485

8586
/**
86-
* Whether `onPress` is used even when
87-
* `disabled` is true.
87+
* Whether `onPress` is used even when `disabled` is true.
8888
*/
8989
isPressHandledWhenDisabled?: boolean,
9090

src/search/SearchMessagesCard.js

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createStyleSheet } from '../styles';
99
import LoadingIndicator from '../common/LoadingIndicator';
1010
import SearchEmptyState from '../common/SearchEmptyState';
1111
import MessageList from '../webview/MessageList';
12+
import { useStartEditTopic } from '../boot/TopicEditModalProvider';
1213

1314
const styles = createStyleSheet({
1415
results: {
@@ -24,6 +25,7 @@ type Props = $ReadOnly<{|
2425

2526
export default function SearchMessagesCard(props: Props): Node {
2627
const { narrow, isFetching, messages } = props;
28+
const startEditTopic = useStartEditTopic();
2729

2830
if (isFetching) {
2931
// Display loading indicator only if there are no messages to
@@ -55,6 +57,7 @@ export default function SearchMessagesCard(props: Props): Node {
5557
// TODO: handle editing a message from the search results,
5658
// or make this prop optional
5759
startEditMessage={() => undefined}
60+
startEditTopic={startEditTopic}
5861
/>
5962
</View>
6063
);

src/streams/TopicItem.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import { getMute } from '../mute/muteModel';
2626
import { getUnread } from '../unread/unreadModel';
2727
import { getOwnUserRole } from '../permissionSelectors';
28+
import { useStartEditTopic } from '../boot/TopicEditModalProvider';
2829

2930
const componentStyles = createStyleSheet({
3031
selectedRow: {
@@ -70,6 +71,7 @@ export default function TopicItem(props: Props): Node {
7071
useActionSheet().showActionSheetWithOptions;
7172
const _ = useContext(TranslationContext);
7273
const dispatch = useDispatch();
74+
const startEditTopic = useStartEditTopic();
7375
const backgroundData = useSelector(state => ({
7476
auth: getAuth(state),
7577
mute: getMute(state),
@@ -88,7 +90,7 @@ export default function TopicItem(props: Props): Node {
8890
onLongPress={() => {
8991
showTopicActionSheet({
9092
showActionSheetWithOptions,
91-
callbacks: { dispatch, _ },
93+
callbacks: { dispatch, startEditTopic, _ },
9294
backgroundData,
9395
streamId,
9496
topic: name,

src/title/TitleStream.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { showStreamActionSheet, showTopicActionSheet } from '../action-sheets';
2727
import type { ShowActionSheetWithOptions } from '../action-sheets';
2828
import { getUnread } from '../unread/unreadModel';
2929
import { getOwnUserRole } from '../permissionSelectors';
30+
import { useStartEditTopic } from '../boot/TopicEditModalProvider';
3031

3132
type Props = $ReadOnly<{|
3233
narrow: Narrow,
@@ -51,6 +52,7 @@ export default function TitleStream(props: Props): Node {
5152
const { narrow, color } = props;
5253
const dispatch = useDispatch();
5354
const stream = useSelector(state => getStreamInNarrow(state, narrow));
55+
const startEditTopic = useStartEditTopic();
5456
const backgroundData = useSelector(state => ({
5557
auth: getAuth(state),
5658
mute: getMute(state),
@@ -75,7 +77,7 @@ export default function TitleStream(props: Props): Node {
7577
? () => {
7678
showTopicActionSheet({
7779
showActionSheetWithOptions,
78-
callbacks: { dispatch, _ },
80+
callbacks: { dispatch, startEditTopic, _ },
7981
backgroundData,
8082
streamId: stream.stream_id,
8183
topic: topicOfNarrow(narrow),

0 commit comments

Comments
 (0)