Skip to content

Topic edit modal #5505

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
17 changes: 16 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 @@ -251,6 +252,14 @@ const toggleResolveTopic = async ({ auth, streamId, topic, _, streams, zulipFeat
});
};

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

const resolveTopic = {
title: 'Resolve topic',
errorMessage: 'Failed to resolve topic',
Expand Down Expand Up @@ -502,9 +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);
}
// Set back to isAdmin after testing feature
if (true) {
buttons.push(editTopic);
}
if (isTopicMuted(streamId, topic, mute)) {
buttons.push(unmuteTopic);
} else {
Expand All @@ -515,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);
Expand Down Expand Up @@ -666,6 +680,7 @@ export const showTopicActionSheet = (args: {|
showActionSheetWithOptions: ShowActionSheetWithOptions,
callbacks: {|
dispatch: Dispatch,
startEditTopic: (streamId: number, topic: string) => Promise<void>,
_: GetText,
|},
backgroundData: $ReadOnly<{
Expand Down
2 changes: 2 additions & 0 deletions src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import deleteMessage from './messages/deleteMessage';
import deleteTopic from './messages/deleteTopic';
import getRawMessageContent from './messages/getRawMessageContent';
import getMessages from './messages/getMessages';
import getSingleMessage from './messages/getSingleMessage';
import getMessageHistory from './messages/getMessageHistory';
import messagesFlags from './messages/messagesFlags';
import sendMessage from './messages/sendMessage';
Expand Down Expand Up @@ -78,6 +79,7 @@ export {
deleteTopic,
getRawMessageContent,
getMessages,
getSingleMessage,
getMessageHistory,
messagesFlags,
sendMessage,
Expand Down
55 changes: 55 additions & 0 deletions src/api/messages/getSingleMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* @flow strict-local */

import type { Auth, ApiResponseSuccess } from '../transportTypes';
import type { Message } from '../apiTypes';
import { transformFetchedMessage, type FetchedMessage } from '../rawModelTypes';
import { apiGet } from '../apiFetch';
import { identityOfAuth } from '../../account/accountMisc';

// The actual response from the server. We convert the message to a proper
// Message before returning it to application code.
type ServerApiResponseSingleMessage = {|
...$Exact<ApiResponseSuccess>,
-raw_content: string, // deprecated

// Until we narrow FetchedMessage into its FL 120+ form, FetchedMessage
// will be a bit less precise than we could be here. That's because we
// only get this field from servers FL 120+.
// TODO(server-5.0): Make this field required, and remove FL-120 comment.
+message?: FetchedMessage,
|};

/**
* See https://zulip.com/api/get-message
*
* Gives undefined if the `message` field is missing, which it will be for
* FL <120.
*/
// TODO(server-5.0): Simplify FL-120 condition in jsdoc and implementation.
export default async (
auth: Auth,
args: {|
+message_id: number,
|},

// TODO(#4659): Don't get this from callers.
zulipFeatureLevel: number,

// TODO(#4659): Don't get this from callers?
allowEditHistory: boolean,
): Promise<Message | void> => {
const { message_id } = args;
const response: ServerApiResponseSingleMessage = await apiGet(auth, `messages/${message_id}`, {
apply_markdown: true,
});

return (
response.message
&& transformFetchedMessage<Message>(
response.message,
identityOfAuth(auth),
zulipFeatureLevel,
allowEditHistory,
)
);
};
70 changes: 70 additions & 0 deletions src/boot/TopicEditModalProvider.js
Original file line number Diff line number Diff line change
@@ -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<void>;

// $FlowIssue[incompatible-type]
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 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 (
<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 @@ -127,6 +128,7 @@ const useMessagesWithFetch = args => {
export default function ChatScreen(props: Props): Node {
const { route, navigation } = props;
const { backgroundColor } = React.useContext(ThemeContext);
const startEditTopic = useStartEditTopic();

const { narrow, editMessage } = route.params;
const setEditMessage = useCallback(
Expand Down Expand Up @@ -221,6 +223,7 @@ export default function ChatScreen(props: Props): Node {
}
showMessagePlaceholders={showMessagePlaceholders}
startEditMessage={setEditMessage}
startEditTopic={startEditTopic}
/>
);
}
Expand Down
69 changes: 35 additions & 34 deletions src/search/SearchMessagesCard.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* @flow strict-local */

import React, { PureComponent } from 'react';
import React from 'react';
import type { Node } from 'react';
import { View } from 'react-native';

Expand All @@ -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 @@ -22,42 +23,42 @@ type Props = $ReadOnly<{|
isFetching: boolean,
|}>;

export default class SearchMessagesCard extends PureComponent<Props> {
render(): Node {
const { isFetching, messages } = this.props;
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
// display from a previous search.
if (!messages || messages.length === 0) {
return <LoadingIndicator size={40} />;
}
}

if (!messages) {
return null;
if (isFetching) {
// Display loading indicator only if there are no messages to
// display from a previous search.
if (!messages || messages.length === 0) {
return <LoadingIndicator size={40} />;
}
}

if (messages.length === 0) {
return <SearchEmptyState text="No results" />;
}
if (!messages) {
return null;
}

return (
<View style={styles.results}>
<MessageList
initialScrollMessageId={
// This access is OK only because of the `.length === 0` check
// above.
messages[messages.length - 1].id
}
messages={messages}
narrow={this.props.narrow}
showMessagePlaceholders={false}
// TODO: handle editing a message from the search results,
// or make this prop optional
startEditMessage={() => undefined}
/>
</View>
);
if (messages.length === 0) {
return <SearchEmptyState text="No results" />;
}

return (
<View style={styles.results}>
<MessageList
initialScrollMessageId={
// This access is OK only because of the `.length === 0` check
// above.
messages[messages.length - 1].id
}
messages={messages}
narrow={narrow}
showMessagePlaceholders={false}
// TODO: handle editing a message from the search results,
// or make this prop optional
startEditMessage={() => undefined}
startEditTopic={startEditTopic}
/>
</View>
);
}
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 Down Expand Up @@ -67,6 +68,7 @@ export default function TitleStream(props: Props): Node {
const showActionSheetWithOptions: ShowActionSheetWithOptions =
useActionSheet().showActionSheetWithOptions;
const _ = useContext(TranslationContext);
const startEditTopic = useStartEditTopic();

return (
<TouchableWithoutFeedback
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
Loading