Skip to content

Commit

Permalink
topic edit modal: Add new edit-topic UI.
Browse files Browse the repository at this point in the history
Fixes: #5365
  • Loading branch information
Leslie Ngo authored and Leslie Ngo committed Sep 29, 2022
1 parent 531e268 commit 52ad6a3
Show file tree
Hide file tree
Showing 12 changed files with 253 additions and 9 deletions.
9 changes: 6 additions & 3 deletions src/ZulipMobile.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import CompatibilityChecker from './boot/CompatibilityChecker';
import AppEventHandlers from './boot/AppEventHandlers';
import { initializeSentry } from './sentry';
import ZulipSafeAreaProvider from './boot/ZulipSafeAreaProvider';
import TopicEditModalProvider from './boot/TopicEditModalProvider';

initializeSentry();

Expand Down Expand Up @@ -55,9 +56,11 @@ export default function ZulipMobile(): Node {
<AppEventHandlers>
<TranslationProvider>
<ThemeProvider>
<ActionSheetProvider>
<ZulipNavigationContainer />
</ActionSheetProvider>
<TopicEditModalProvider>
<ActionSheetProvider>
<ZulipNavigationContainer />
</ActionSheetProvider>
</TopicEditModalProvider>
</ThemeProvider>
</TranslationProvider>
</AppEventHandlers>
Expand Down
19 changes: 18 additions & 1 deletion src/action-sheets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ type TopicArgs = {
zulipFeatureLevel: number,
dispatch: Dispatch,
_: GetText,
startEditTopic: (streamId: number, topic: string) => Promise<void>,
...
};

Expand Down Expand Up @@ -169,6 +170,14 @@ const deleteMessage = {
},
};

const editTopic = {
title: 'Edit topic',
errorMessage: 'Failed to resolve topic',
action: ({ streamId, topic, startEditTopic }) => {
startEditTopic(streamId, topic);
},
};

const markTopicAsRead = {
title: 'Mark topic as read',
errorMessage: 'Failed to mark topic as read',
Expand Down Expand Up @@ -502,9 +511,16 @@ export const constructTopicActionButtons = (args: {|

const buttons = [];
const unreadCount = getUnreadCountForTopic(unread, streamId, topic);
const isAdmin = roleIsAtLeast(ownUserRole, Role.Admin);
if (unreadCount > 0) {
buttons.push(markTopicAsRead);
}
/* At present, the permissions for editing the topic of a message are highly complex.
Until we move to a better set of policy options, we'll only display the edit topic
button to admins. Further information: #21739, #M5365 */
if (isAdmin) {
buttons.push(editTopic);
}
if (isTopicMuted(streamId, topic, mute)) {
buttons.push(unmuteTopic);
} else {
Expand All @@ -515,7 +531,7 @@ export const constructTopicActionButtons = (args: {|
} else {
buttons.push(unresolveTopic);
}
if (roleIsAtLeast(ownUserRole, Role.Admin)) {
if (isAdmin) {
buttons.push(deleteTopic);
}
const sub = subscriptions.get(streamId);
Expand Down Expand Up @@ -666,6 +682,7 @@ export const showTopicActionSheet = (args: {|
showActionSheetWithOptions: ShowActionSheetWithOptions,
callbacks: {|
dispatch: Dispatch,
startEditTopic: (streamId: number, topic: string) => Promise<void>,
_: GetText,
|},
backgroundData: $ReadOnly<{
Expand Down
72 changes: 72 additions & 0 deletions src/boot/TopicEditModalProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/* @flow strict-local */
import React, { createContext, useState, useCallback, useContext } from 'react';
import type { Context, Node } from 'react';
import { useSelector } from '../react-redux';
import TopicEditModal from '../topics/TopicEditModal';
import { getAuth, getZulipFeatureLevel, getStreamsById } from '../selectors';
import { TranslationContext } from './TranslationProvider';

type Props = $ReadOnly<{|
children: Node,
|}>;

type StartEditTopicContext = (streamId: number, topic: string) => Promise<void>;

/* $FlowIssue[incompatible-type] We can't provide an initial 'value' prop. We would
need to provide the 'startEditTopic' callback that's defined after this createContext
call, which is impossible. */
const TopicModal: Context<StartEditTopicContext> = createContext(undefined);

export const useStartEditTopic = (): StartEditTopicContext => useContext(TopicModal);

export default function TopicEditModalProvider(props: Props): Node {
const { children } = props;
const auth = useSelector(getAuth);
const zulipFeatureLevel = useSelector(getZulipFeatureLevel);
const streamsById = useSelector(getStreamsById);
const _ = useContext(TranslationContext);

const [topicModalProviderState, setTopicModalProviderState] = useState({
visible: false,
streamId: -1,
topic: '',
});

const { visible } = topicModalProviderState;

const startEditTopic = useCallback(
async (streamId: number, topic: string) => {
if (visible) {
return;
}
setTopicModalProviderState({
visible: true,
streamId,
topic,
});
},
[visible],
);

const closeEditTopicModal = useCallback(() => {
setTopicModalProviderState({
visible: false,
streamId: -1,
topic: '',
});
}, []);

return (
<TopicModal.Provider value={startEditTopic}>
<TopicEditModal
topicModalProviderState={topicModalProviderState}
closeEditTopicModal={closeEditTopicModal}
auth={auth}
zulipFeatureLevel={zulipFeatureLevel}
streamsById={streamsById}
_={_}
/>
{children}
</TopicModal.Provider>
);
}
3 changes: 3 additions & 0 deletions src/chat/ChatScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { showErrorAlert } from '../utils/info';
import { TranslationContext } from '../boot/TranslationProvider';
import * as api from '../api';
import { useConditionalEffect } from '../reactUtils';
import { useStartEditTopic } from '../boot/TopicEditModalProvider';

type Props = $ReadOnly<{|
navigation: AppNavigationProp<'chat'>,
Expand Down Expand Up @@ -133,6 +134,7 @@ export default function ChatScreen(props: Props): Node {
(value: EditMessage | null) => navigation.setParams({ editMessage: value }),
[navigation],
);
const startEditTopic = useStartEditTopic();

const isNarrowValid = useSelector(state => getIsNarrowValid(state, narrow));
const draft = useSelector(state => getDraftForNarrow(state, narrow));
Expand Down Expand Up @@ -221,6 +223,7 @@ export default function ChatScreen(props: Props): Node {
}
showMessagePlaceholders={showMessagePlaceholders}
startEditMessage={setEditMessage}
startEditTopic={startEditTopic}
/>
);
}
Expand Down
3 changes: 3 additions & 0 deletions src/search/SearchMessagesCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createStyleSheet } from '../styles';
import LoadingIndicator from '../common/LoadingIndicator';
import SearchEmptyState from '../common/SearchEmptyState';
import MessageList from '../webview/MessageList';
import { useStartEditTopic } from '../boot/TopicEditModalProvider';

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

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

if (isFetching) {
// Display loading indicator only if there are no messages to
Expand Down Expand Up @@ -55,6 +57,7 @@ export default function SearchMessagesCard(props: Props): Node {
// TODO: handle editing a message from the search results,
// or make this prop optional
startEditMessage={() => undefined}
startEditTopic={startEditTopic}
/>
</View>
);
Expand Down
4 changes: 3 additions & 1 deletion src/streams/TopicItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import { getMute } from '../mute/muteModel';
import { getUnread } from '../unread/unreadModel';
import { getOwnUserRole } from '../permissionSelectors';
import { useStartEditTopic } from '../boot/TopicEditModalProvider';

const componentStyles = createStyleSheet({
selectedRow: {
Expand Down Expand Up @@ -70,6 +71,7 @@ export default function TopicItem(props: Props): Node {
useActionSheet().showActionSheetWithOptions;
const _ = useContext(TranslationContext);
const dispatch = useDispatch();
const startEditTopic = useStartEditTopic();
const backgroundData = useSelector(state => ({
auth: getAuth(state),
mute: getMute(state),
Expand All @@ -88,7 +90,7 @@ export default function TopicItem(props: Props): Node {
onLongPress={() => {
showTopicActionSheet({
showActionSheetWithOptions,
callbacks: { dispatch, _ },
callbacks: { dispatch, startEditTopic, _ },
backgroundData,
streamId,
topic: name,
Expand Down
4 changes: 3 additions & 1 deletion src/title/TitleStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { showStreamActionSheet, showTopicActionSheet } from '../action-sheets';
import type { ShowActionSheetWithOptions } from '../action-sheets';
import { getUnread } from '../unread/unreadModel';
import { getOwnUserRole } from '../permissionSelectors';
import { useStartEditTopic } from '../boot/TopicEditModalProvider';

type Props = $ReadOnly<{|
narrow: Narrow,
Expand All @@ -51,6 +52,7 @@ export default function TitleStream(props: Props): Node {
const { narrow, color } = props;
const dispatch = useDispatch();
const stream = useSelector(state => getStreamInNarrow(state, narrow));
const startEditTopic = useStartEditTopic();
const backgroundData = useSelector(state => ({
auth: getAuth(state),
mute: getMute(state),
Expand All @@ -75,7 +77,7 @@ export default function TitleStream(props: Props): Node {
? () => {
showTopicActionSheet({
showActionSheetWithOptions,
callbacks: { dispatch, _ },
callbacks: { dispatch, startEditTopic, _ },
backgroundData,
streamId: stream.stream_id,
topic: topicOfNarrow(narrow),
Expand Down
129 changes: 129 additions & 0 deletions src/topics/TopicEditModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// @flow strict-local
import React, { useState, useContext, useEffect } from 'react';
import { Modal, View } from 'react-native';
import type { Node } from 'react';
import styles, { ThemeContext, BRAND_COLOR, createStyleSheet } from '../styles';
import { updateMessage } from '../api';
import type { Auth, GetText, Stream } from '../types';
import { fetchSomeMessageIdForConversation } from '../message/fetchActions';
import ZulipTextIntl from '../common/ZulipTextIntl';
import ZulipTextButton from '../common/ZulipTextButton';
import Input from '../common/Input';

type Props = $ReadOnly<{|
topicModalProviderState: {
visible: boolean,
topic: string,
streamId: number,
},
auth: Auth,
zulipFeatureLevel: number,
streamsById: Map<number, Stream>,
_: GetText,
closeEditTopicModal: () => void,
|}>;

export default function TopicEditModal(props: Props): Node {
const { topicModalProviderState, closeEditTopicModal, auth, zulipFeatureLevel, streamsById, _ } =
props;

const { visible, topic, streamId } = topicModalProviderState;

const [topicName, onChangeTopicName] = useState();

useEffect(() => {
onChangeTopicName(topic);
}, [topic]);

const { backgroundColor } = useContext(ThemeContext);

const modalStyles = createStyleSheet({
wrapper: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
modal: {
justifyContent: 'flex-start',
backgroundColor,
padding: 15,
shadowOpacity: 0.5,
shadowColor: 'gray',
shadowOffset: {
height: 5,
width: 5,
},
shadowRadius: 5,
borderRadius: 5,
width: '90%',
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'flex-end',
},
titleText: {
fontSize: 18,
lineHeight: 21,
color: BRAND_COLOR,
marginBottom: 10,
fontWeight: 'bold',
},
});

const handleSubmit = async () => {
if (topicName === '') {
return;
}
const messageId = await fetchSomeMessageIdForConversation(
auth,
streamId,
topic,
streamsById,
zulipFeatureLevel,
);
if (messageId == null) {
throw new Error(
_('No messages in topic: {streamAndTopic}', {
streamAndTopic: `#${streamsById.get(streamId)?.name ?? 'unknown'} > ${topic}`,
}),
);
}
await updateMessage(auth, messageId, {
propagate_mode: 'change_all',
subject: topicName,
...(zulipFeatureLevel >= 9 && {
send_notification_to_old_thread: true,
send_notification_to_new_thread: true,
}),
});
closeEditTopicModal();
};
return (
<Modal
transparent
visible={visible}
animationType="slide"
onRequestClose={closeEditTopicModal}
supportedOrientations={['portrait', 'landscape', 'landscape-left', 'landscape-right']}
>
<View style={modalStyles.wrapper}>
<View style={modalStyles.modal}>
<ZulipTextIntl style={modalStyles.titleText} text="Edit topic" />
<Input
style={styles.marginBottom}
defaultValue={topicName}
placeholder="Please enter a new topic name."
onChangeText={onChangeTopicName}
maxLength={60}
autoFocus
selectTextOnFocus
/>
<View style={modalStyles.buttonContainer}>
<ZulipTextButton label="Cancel" onPress={closeEditTopicModal} />
<ZulipTextButton label="Submit" onPress={handleSubmit} disabled={topicName === ''} />
</View>
</View>
</View>
</Modal>
);
}
1 change: 1 addition & 0 deletions src/webview/MessageList.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type OuterProps = $ReadOnly<{|
initialScrollMessageId: number | null,
showMessagePlaceholders: boolean,
startEditMessage: (editMessage: EditMessage) => void,
startEditTopic: (streamId: number, topic: string) => Promise<void>,
|}>;

type SelectorProps = {|
Expand Down
1 change: 1 addition & 0 deletions src/webview/__tests__/generateInboundEvents-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('generateInboundEvents', () => {
narrow: HOME_NARROW,
showMessagePlaceholders: false,
startEditMessage: jest.fn(),
startEditTopic: jest.fn(),
dispatch: jest.fn(),
...baseSelectorProps,
showActionSheetWithOptions: jest.fn(),
Expand Down
Loading

0 comments on commit 52ad6a3

Please sign in to comment.