diff --git a/src/ZulipMobile.js b/src/ZulipMobile.js index 2c85af4ada5..12eaaeb2860 100644 --- a/src/ZulipMobile.js +++ b/src/ZulipMobile.js @@ -16,7 +16,7 @@ import CompatibilityChecker from './boot/CompatibilityChecker'; import AppEventHandlers from './boot/AppEventHandlers'; import { initializeSentry } from './sentry'; import ZulipSafeAreaProvider from './boot/ZulipSafeAreaProvider'; -import TopicModalProvider from './boot/TopicModalProvider'; +import TopicEditModalProvider from './boot/TopicEditModalProvider'; initializeSentry(); @@ -56,11 +56,11 @@ export default function ZulipMobile(): Node { - + - + diff --git a/src/action-sheets/index.js b/src/action-sheets/index.js index 687bf8dd132..bb6993d09f3 100644 --- a/src/action-sheets/index.js +++ b/src/action-sheets/index.js @@ -77,12 +77,7 @@ type TopicArgs = { zulipFeatureLevel: number, dispatch: Dispatch, _: GetText, - startEditTopic: ( - streamId: number, - topic: string, - streamsById: Map, - _: GetText, - ) => Promise, + startEditTopic: (streamId: number, topic: string) => Promise, ... }; @@ -260,8 +255,8 @@ const toggleResolveTopic = async ({ auth, streamId, topic, _, streams, zulipFeat const editTopic = { title: 'Edit topic', errorMessage: 'Failed to resolve topic', - action: ({ streamId, topic, streams, _, startEditTopic }) => { - startEditTopic(streamId, topic, streams, _); + action: ({ streamId, topic, startEditTopic }) => { + startEditTopic(streamId, topic); }, }; @@ -516,10 +511,14 @@ export const constructTopicActionButtons = (args: {| const buttons = []; const unreadCount = getUnreadCountForTopic(unread, streamId, topic); + const isAdmin = roleIsAtLeast(ownUserRole, Role.Admin); if (unreadCount > 0) { buttons.push(markTopicAsRead); } - buttons.push(editTopic); + // Set back to isAdmin after testing feature + if (true) { + buttons.push(editTopic); + } if (isTopicMuted(streamId, topic, mute)) { buttons.push(unmuteTopic); } else { @@ -530,7 +529,7 @@ export const constructTopicActionButtons = (args: {| } else { buttons.push(unresolveTopic); } - if (roleIsAtLeast(ownUserRole, Role.Admin)) { + if (isAdmin) { buttons.push(deleteTopic); } const sub = subscriptions.get(streamId); @@ -681,12 +680,7 @@ export const showTopicActionSheet = (args: {| showActionSheetWithOptions: ShowActionSheetWithOptions, callbacks: {| dispatch: Dispatch, - startEditTopic: ( - streamId: number, - topic: string, - streamsById: Map, - _: GetText, - ) => Promise, + startEditTopic: (streamId: number, topic: string) => Promise, _: GetText, |}, backgroundData: $ReadOnly<{ diff --git a/src/boot/TopicEditModalProvider.js b/src/boot/TopicEditModalProvider.js new file mode 100644 index 00000000000..50f0e10c64b --- /dev/null +++ b/src/boot/TopicEditModalProvider.js @@ -0,0 +1,70 @@ +/* @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; + +// $FlowIssue[incompatible-type] +const TopicModal: Context = 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 startEditTopic = useCallback( + async (streamId: number, topic: string) => { + const { visible } = topicModalProviderState; + if (visible) { + return; + } + setTopicModalProviderState({ + visible: true, + streamId, + topic, + }); + }, [topicModalProviderState]); + + const closeEditTopicModal = useCallback(() => { + setTopicModalProviderState({ + visible: false, + streamId: -1, + topic: '', + }); + }, []); + + return ( + + + {children} + + ); +} diff --git a/src/boot/TopicModalProvider.js b/src/boot/TopicModalProvider.js deleted file mode 100644 index 91bb0b399a3..00000000000 --- a/src/boot/TopicModalProvider.js +++ /dev/null @@ -1,92 +0,0 @@ -/* @flow strict-local */ -import React, { createContext, useState, useMemo, useCallback, useContext } from 'react'; -import type { Context, Node } from 'react'; -import { useSelector } from '../react-redux'; -import TopicEditModal from '../topics/TopicEditModal'; -import type { Stream, GetText } from '../types'; -import { fetchSomeMessageIdForConversation } from '../message/fetchActions'; -import { getAuth, getZulipFeatureLevel } from '../selectors'; - -type Props = $ReadOnly<{| - children: Node, -|}>; - -type TopicModalContext = $ReadOnly<{| - startEditTopic: ( - streamId: number, - topic: string, - streamsById: Map, - _: GetText, - ) => Promise, - closeEditTopicModal: () => void, -|}>; - -// $FlowIssue[incompatible-type] -const TopicModal: Context = createContext(undefined); - -export const useTopicModalHandler = (): TopicModalContext => useContext(TopicModal); - -export default function TopicModalProvider(props: Props): Node { - const { children } = props; - const auth = useSelector(getAuth); - const zulipFeatureLevel = useSelector(getZulipFeatureLevel); - const [topicModalState, setTopicModalState] = useState({ - visible: false, - topic: '', - fetchArgs: { - auth: null, - messageId: null, - zulipFeatureLevel: null, - }, - }); - - const startEditTopic = useCallback( - async (streamId, topic, streamsById, _) => { - 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}`, - }), - ); - } - setTopicModalState({ - visible: true, - topic, - fetchArgs: { auth, messageId, zulipFeatureLevel }, - }); - }, - [auth, zulipFeatureLevel], - ); - - const closeEditTopicModal = useCallback(() => { - setTopicModalState({ - visible: false, - topic: null, - fetchArgs: { auth: null, messageId: null, zulipFeatureLevel: null }, - }); - }, []); - - const topicModalHandler = useMemo( - () => ({ - startEditTopic, - closeEditTopicModal, - }), - [startEditTopic, closeEditTopicModal], - ); - - return ( - - {topicModalState.visible && ( - - )} - {children} - - ); -} diff --git a/src/chat/ChatScreen.js b/src/chat/ChatScreen.js index 472e0fcd163..0dd1a74f23c 100644 --- a/src/chat/ChatScreen.js +++ b/src/chat/ChatScreen.js @@ -30,7 +30,7 @@ import { showErrorAlert } from '../utils/info'; import { TranslationContext } from '../boot/TranslationProvider'; import * as api from '../api'; import { useConditionalEffect } from '../reactUtils'; -import { useTopicModalHandler } from '../boot/TopicModalProvider'; +import { useStartEditTopic } from '../boot/TopicEditModalProvider'; type Props = $ReadOnly<{| navigation: AppNavigationProp<'chat'>, @@ -128,7 +128,7 @@ const useMessagesWithFetch = args => { export default function ChatScreen(props: Props): Node { const { route, navigation } = props; const { backgroundColor } = React.useContext(ThemeContext); - const { startEditTopic } = useTopicModalHandler(); + const startEditTopic = useStartEditTopic(); const { narrow, editMessage } = route.params; const setEditMessage = useCallback( diff --git a/src/search/SearchMessagesCard.js b/src/search/SearchMessagesCard.js index 4d00da798a2..ede07efd0d4 100644 --- a/src/search/SearchMessagesCard.js +++ b/src/search/SearchMessagesCard.js @@ -9,7 +9,7 @@ import { createStyleSheet } from '../styles'; import LoadingIndicator from '../common/LoadingIndicator'; import SearchEmptyState from '../common/SearchEmptyState'; import MessageList from '../webview/MessageList'; -import { useTopicModalHandler } from '../boot/TopicModalProvider'; +import { useStartEditTopic } from '../boot/TopicEditModalProvider'; const styles = createStyleSheet({ results: { @@ -25,7 +25,7 @@ type Props = $ReadOnly<{| export default function SearchMessagesCard(props: Props): Node { const { narrow, isFetching, messages } = props; - const { startEditTopic } = useTopicModalHandler(); + const startEditTopic = useStartEditTopic(); if (isFetching) { // Display loading indicator only if there are no messages to diff --git a/src/streams/TopicItem.js b/src/streams/TopicItem.js index c197e3e9abb..2d58dc2e3b6 100644 --- a/src/streams/TopicItem.js +++ b/src/streams/TopicItem.js @@ -25,7 +25,7 @@ import { import { getMute } from '../mute/muteModel'; import { getUnread } from '../unread/unreadModel'; import { getOwnUserRole } from '../permissionSelectors'; -import { useTopicModalHandler } from '../boot/TopicModalProvider'; +import { useStartEditTopic } from '../boot/TopicEditModalProvider'; const componentStyles = createStyleSheet({ selectedRow: { @@ -71,7 +71,7 @@ export default function TopicItem(props: Props): Node { useActionSheet().showActionSheetWithOptions; const _ = useContext(TranslationContext); const dispatch = useDispatch(); - const { startEditTopic } = useTopicModalHandler(); + const startEditTopic = useStartEditTopic(); const backgroundData = useSelector(state => ({ auth: getAuth(state), mute: getMute(state), diff --git a/src/title/TitleStream.js b/src/title/TitleStream.js index 5ffce4e9509..d1b927d5e8e 100644 --- a/src/title/TitleStream.js +++ b/src/title/TitleStream.js @@ -27,7 +27,7 @@ import { showStreamActionSheet, showTopicActionSheet } from '../action-sheets'; import type { ShowActionSheetWithOptions } from '../action-sheets'; import { getUnread } from '../unread/unreadModel'; import { getOwnUserRole } from '../permissionSelectors'; -import { useTopicModalHandler } from '../boot/TopicModalProvider'; +import { useStartEditTopic } from '../boot/TopicEditModalProvider'; type Props = $ReadOnly<{| narrow: Narrow, @@ -68,7 +68,7 @@ export default function TitleStream(props: Props): Node { const showActionSheetWithOptions: ShowActionSheetWithOptions = useActionSheet().showActionSheetWithOptions; const _ = useContext(TranslationContext); - const { startEditTopic } = useTopicModalHandler(); + const startEditTopic = useStartEditTopic(); return ( , - _: GetText, - ) => Promise, - closeEditTopicModal: () => void, + streamId: number, }, -}; + auth: Auth, + zulipFeatureLevel: number, + streamsById: Map, + _: GetText, + closeEditTopicModal: () => void, +|}>; + +export default function TopicEditModal(props: Props): Node { + const { + topicModalProviderState, + closeEditTopicModal, + auth, + zulipFeatureLevel, + streamsById, + _, + } = props; -export default function TopicEditModal({ - topicModalState, - topicModalHandler, -}: TopicEditModalArgs): JSX$Element { - const { topic, fetchArgs } = topicModalState; - const [topicState, onChangeTopic] = useState(topic); - const { closeEditTopicModal } = topicModalHandler; - const { auth, messageId, zulipFeatureLevel } = fetchArgs; + const { visible, topic, streamId } = topicModalProviderState; + + const [topicName, onChangeTopicName] = useState(); + + useEffect(() => { + onChangeTopicName(topic); + }, [topic]); const { backgroundColor } = useContext(ThemeContext); @@ -110,9 +113,23 @@ export default function TopicEditModal({ }); const handleSubmit = async () => { + 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: topicState, + subject: topicName, ...(zulipFeatureLevel >= 9 && { send_notification_to_old_thread: true, send_notification_to_new_thread: true, @@ -121,21 +138,14 @@ export default function TopicEditModal({ closeEditTopicModal(); }; return ( - { - closeEditTopicModal(); - }} - > + - Edit topic + @@ -147,22 +157,26 @@ export default function TopicEditModal({ borderColor: BRAND_COLOR, ...styles.button, }} - onPress={() => { - closeEditTopicModal(); - }} + onPress={closeEditTopicModal} > - Cancel + - Submit + diff --git a/src/webview/MessageList.js b/src/webview/MessageList.js index 74132f9fea9..68699bccd8d 100644 --- a/src/webview/MessageList.js +++ b/src/webview/MessageList.js @@ -14,7 +14,6 @@ import type { MessageListElement, UserOrBot, EditMessage, - Stream, } from '../types'; import { assumeSecretlyGlobalState } from '../reduxTypes'; import { connect } from '../react-redux'; @@ -46,12 +45,7 @@ type OuterProps = $ReadOnly<{| initialScrollMessageId: number | null, showMessagePlaceholders: boolean, startEditMessage: (editMessage: EditMessage) => void, - startEditTopic: ( - streamId: number, - topic: string, - streamsById: Map, - _: GetText, - ) => Promise, + startEditTopic: (streamId: number, topic: string) => Promise, |}>; type SelectorProps = {| diff --git a/src/webview/handleOutboundEvents.js b/src/webview/handleOutboundEvents.js index eae496f7cc6..cf8a7127bdb 100644 --- a/src/webview/handleOutboundEvents.js +++ b/src/webview/handleOutboundEvents.js @@ -4,16 +4,7 @@ import { Clipboard, Alert } from 'react-native'; import * as NavigationService from '../nav/NavigationService'; import * as api from '../api'; import config from '../config'; -import type { - Dispatch, - GetText, - Message, - Narrow, - Outbox, - EditMessage, - UserId, - Stream, -} from '../types'; +import type { Dispatch, GetText, Message, Narrow, Outbox, EditMessage, UserId } from '../types'; import type { BackgroundData } from './backgroundData'; import type { ShowActionSheetWithOptions } from '../action-sheets'; import type { JSONableDict } from '../utils/jsonable'; @@ -178,12 +169,7 @@ type Props = $ReadOnly<{ doNotMarkMessagesAsRead: boolean, showActionSheetWithOptions: ShowActionSheetWithOptions, startEditMessage: (editMessage: EditMessage) => void, - startEditTopic: ( - streamId: number, - topic: string, - streamsById: Map, - _: GetText, - ) => Promise, + startEditTopic: (streamId: number, topic: string) => Promise, ... }>; diff --git a/static/translations/messages_en.json b/static/translations/messages_en.json index a6dc550672e..65b74e2fb26 100644 --- a/static/translations/messages_en.json +++ b/static/translations/messages_en.json @@ -338,5 +338,6 @@ "Failed to copy stream link": "Failed to copy stream link", "A stream with this name already exists.": "A stream with this name already exists.", "Streams": "Streams", - "Edit topic": "Edit topic" + "Edit topic": "Edit topic", + "Submit": "Submit" }