Skip to content
Merged
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
4 changes: 4 additions & 0 deletions src/__tests__/lib/exampleData.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import rootReducer from '../../boot/reducers';
import { authOfAccount } from '../../account/accountMisc';
import { HOME_NARROW } from '../../utils/narrow';
import type { BackgroundData } from '../../webview/MessageList';
import { getStreamsById, getStreamsByName } from '../../selectors';

/* ========================================================================
* Utilities
Expand Down Expand Up @@ -755,7 +756,10 @@ export const backgroundData: BackgroundData = deepFreeze({
mute: [],
mutedUsers: Immutable.Map(),
ownUser: selfUser,
streams: getStreamsById(baseReduxState),
streamsByName: getStreamsByName(baseReduxState),
subscriptions: [],
unread: baseReduxState.unread,
theme: 'default',
twentyFourHourTime: false,
});
93 changes: 63 additions & 30 deletions src/message/__tests__/messageActionSheet-test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// @flow strict-local
import deepFreeze from 'deep-freeze';
import { HOME_NARROW } from '../../utils/narrow';
import { streamNameOfStreamMessage } from '../../utils/recipient';

import * as eg from '../../__tests__/lib/exampleData';
import { constructMessageActionButtons, constructHeaderActionButtons } from '../messageActionSheet';
import { constructMessageActionButtons, constructTopicActionButtons } from '../messageActionSheet';
import { reducer } from '../../unread/unreadModel';
import { initialState } from '../../unread/__tests__/unread-testlib';

const buttonTitles = buttons => buttons.map(button => button.title);

Expand Down Expand Up @@ -43,61 +44,93 @@ describe('constructActionButtons', () => {
});
});

describe('constructHeaderActionButtons', () => {
describe('constructTopicActionButtons', () => {
const stream = eg.makeStream();
const streamMessage = eg.streamMessage({ stream });
const topic = streamMessage.subject;
const streamId = streamMessage.stream_id;
const streams = deepFreeze(new Map([[stream.stream_id, stream]]));

const baseState = (() => {
const r = (state, action) => reducer(state, action, eg.plusReduxState);
let state = initialState;
state = r(state, eg.mkActionEventNewMessage(streamMessage));
return state;
})();

test('show mark as read if topic is unread', () => {
Comment thread
WesleyAC marked this conversation as resolved.
const unread = baseState;
const buttons = constructTopicActionButtons({
backgroundData: { ...eg.backgroundData, streams, unread },
streamId,
topic,
});
expect(buttonTitles(buttons)).toContain('Mark topic as read');
});

test('do not show mark as read if topic is read', () => {
const buttons = constructTopicActionButtons({
backgroundData: { ...eg.backgroundData, streams },
streamId,
topic,
});
expect(buttonTitles(buttons)).not.toContain('Mark topic as read');
});

test('show Unmute topic option if topic is muted', () => {
const mute = deepFreeze([['electron issues', 'issue #556']]);
const buttons = constructHeaderActionButtons({
backgroundData: { ...eg.backgroundData, mute },
stream: 'electron issues',
topic: 'issue #556',
const mute = deepFreeze([[stream.name, topic]]);
const buttons = constructTopicActionButtons({
backgroundData: { ...eg.backgroundData, streams, mute },
streamId,
topic,
});
expect(buttonTitles(buttons)).toContain('Unmute topic');
});

test('show mute topic option if topic is not muted', () => {
const buttons = constructHeaderActionButtons({
backgroundData: { ...eg.backgroundData, mute: [] },
stream: streamNameOfStreamMessage(eg.streamMessage()),
topic: eg.streamMessage().subject,
const buttons = constructTopicActionButtons({
backgroundData: { ...eg.backgroundData, streams, mute: [] },
streamId,
topic,
});
expect(buttonTitles(buttons)).toContain('Mute topic');
});

test('show Unmute stream option if stream is not in home view', () => {
const subscriptions = [{ ...eg.subscription, in_home_view: false }];
const buttons = constructHeaderActionButtons({
backgroundData: { ...eg.backgroundData, subscriptions },
stream: streamNameOfStreamMessage(eg.streamMessage()),
topic: eg.streamMessage().subject,
const subscriptions = [{ ...eg.subscription, in_home_view: false, ...stream }];
const buttons = constructTopicActionButtons({
backgroundData: { ...eg.backgroundData, subscriptions, streams },
streamId,
topic,
});
expect(buttonTitles(buttons)).toContain('Unmute stream');
});

test('show mute stream option if stream is in home view', () => {
const subscriptions = [{ ...eg.subscription, in_home_view: true }];
const buttons = constructHeaderActionButtons({
backgroundData: { ...eg.backgroundData, subscriptions },
stream: streamNameOfStreamMessage(eg.streamMessage()),
topic: eg.streamMessage().subject,
const subscriptions = [{ ...eg.subscription, in_home_view: true, ...stream }];
const buttons = constructTopicActionButtons({
backgroundData: { ...eg.backgroundData, subscriptions, streams },
streamId,
topic,
});
expect(buttonTitles(buttons)).toContain('Mute stream');
});

test('show delete topic option if current user is an admin', () => {
const ownUser = { ...eg.selfUser, is_admin: true };
const buttons = constructHeaderActionButtons({
backgroundData: { ...eg.backgroundData, ownUser },
stream: streamNameOfStreamMessage(eg.streamMessage()),
topic: eg.streamMessage().subject,
const buttons = constructTopicActionButtons({
backgroundData: { ...eg.backgroundData, ownUser, streams },
streamId,
topic,
});
expect(buttonTitles(buttons)).toContain('Delete topic');
});

test('do not show delete topic option if current user is not an admin', () => {
const buttons = constructHeaderActionButtons({
backgroundData: eg.backgroundData,
stream: streamNameOfStreamMessage(eg.streamMessage()),
topic: eg.streamMessage().subject,
const buttons = constructTopicActionButtons({
backgroundData: { ...eg.backgroundData, streams },
streamId,
topic,
});
expect(buttonTitles(buttons)).not.toContain('Delete topic');
});
Expand Down
99 changes: 57 additions & 42 deletions src/message/messageActionSheet.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* @flow strict-local */
import { Clipboard, Share, Alert } from 'react-native';
import invariant from 'invariant';

import * as NavigationService from '../nav/NavigationService';
import type {
Expand All @@ -14,7 +15,9 @@ import type {
Subscription,
User,
EditMessage,
Stream,
} from '../types';
import type { UnreadState } from '../unread/unreadModelTypes';
import {
getNarrowForReply,
isPmNarrow,
Expand All @@ -28,18 +31,20 @@ import { doNarrow, deleteOutboxMessage, navigateToEmojiPicker, navigateToStream
import { navigateToMessageReactionScreen } from '../nav/navActions';
import { deleteMessagesForTopic } from '../topics/topicActions';
import * as logging from '../utils/logging';
import { getUnreadCountForTopic } from '../unread/unreadModel';

// TODO really this belongs in a libdef.
export type ShowActionSheetWithOptions = (
{ options: string[], cancelButtonIndex: number, ... },
(number) => void,
) => void;

type HeaderArgs = {
type TopicArgs = {
auth: Auth,
stream: string,
streamId: number,
topic: string,
subscriptions: Subscription[],
streams: Map<number, Stream>,
dispatch: Dispatch,
_: GetText,
...
Expand All @@ -55,7 +60,7 @@ type MessageArgs = {
...
};

type Button<Args: HeaderArgs | MessageArgs> = {|
type Button<Args: TopicArgs | MessageArgs> = {|
(Args): void | Promise<void>,

/** The label for the button. */
Expand Down Expand Up @@ -118,19 +123,29 @@ const deleteMessage = async ({ auth, message, dispatch }) => {
deleteMessage.title = 'Delete message';
deleteMessage.errorMessage = 'Failed to delete message';

const unmuteTopic = async ({ auth, stream, topic }) => {
await api.setTopicMute(auth, stream, topic, false);
const markTopicAsRead = async ({ auth, streamId, topic }) => {
await api.markTopicAsRead(auth, streamId, topic);
};
markTopicAsRead.title = 'Mark topic as read';
Copy link
Copy Markdown
Contributor

@chrisbobbe chrisbobbe May 13, 2021

Choose a reason for hiding this comment

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

Ah, I meant to post in my previous review but forgot:

The title and error message are translated via _ (the function with type GetText), so we should make sure these user-facing strings have entries in static/translations/messages_en.json; looks like 'Mark topic as read' is already there but the error message is not.

markTopicAsRead.errorMessage = 'Failed to mark topic as read';

const unmuteTopic = async ({ auth, streamId, topic, streams }) => {
const stream = streams.get(streamId);
invariant(stream !== undefined, 'Stream with provided streamId must exist.');
await api.setTopicMute(auth, stream.name, topic, false);
};
unmuteTopic.title = 'Unmute topic';
unmuteTopic.errorMessage = 'Failed to unmute topic';

const muteTopic = async ({ auth, stream, topic }) => {
await api.setTopicMute(auth, stream, topic, true);
const muteTopic = async ({ auth, streamId, topic, streams }) => {
const stream = streams.get(streamId);
invariant(stream !== undefined, 'Stream with provided streamId must exist.');
await api.setTopicMute(auth, stream.name, topic, true);
};
muteTopic.title = 'Mute topic';
muteTopic.errorMessage = 'Failed to mute topic';

const deleteTopic = async ({ auth, stream, topic, dispatch, _ }) => {
const deleteTopic = async ({ auth, streamId, topic, dispatch, _ }) => {
const alertTitle = _('Are you sure you want to delete the topic “{topic}”?', { topic });
const AsyncAlert = async (): Promise<boolean> =>
new Promise((resolve, reject) => {
Expand All @@ -157,35 +172,26 @@ const deleteTopic = async ({ auth, stream, topic, dispatch, _ }) => {
);
});
if (await AsyncAlert()) {
await dispatch(deleteMessagesForTopic(stream, topic));
await dispatch(deleteMessagesForTopic(streamId, topic));
}
};
deleteTopic.title = 'Delete topic';
deleteTopic.errorMessage = 'Failed to delete topic';

const unmuteStream = async ({ auth, stream, subscriptions }) => {
const sub = subscriptions.find(x => x.name === stream);
if (sub) {
await api.setSubscriptionProperty(auth, sub.stream_id, 'is_muted', false);
}
const unmuteStream = async ({ auth, streamId, subscriptions }) => {
await api.setSubscriptionProperty(auth, streamId, 'is_muted', false);
};
unmuteStream.title = 'Unmute stream';
unmuteStream.errorMessage = 'Failed to unmute stream';

const muteStream = async ({ auth, stream, subscriptions }) => {
const sub = subscriptions.find(x => x.name === stream);
if (sub) {
await api.setSubscriptionProperty(auth, sub.stream_id, 'is_muted', true);
}
const muteStream = async ({ auth, streamId, subscriptions }) => {
await api.setSubscriptionProperty(auth, streamId, 'is_muted', true);
};
muteStream.title = 'Mute stream';
muteStream.errorMessage = 'Failed to mute stream';

const showStreamSettings = ({ stream, subscriptions }) => {
const sub = subscriptions.find(x => x.name === stream);
if (sub) {
NavigationService.dispatch(navigateToStream(sub.stream_id));
}
const showStreamSettings = ({ streamId, subscriptions }) => {
NavigationService.dispatch(navigateToStream(streamId));
};
showStreamSettings.title = 'Stream settings';
showStreamSettings.errorMessage = 'Failed to show stream settings';
Expand Down Expand Up @@ -226,30 +232,38 @@ const cancel = params => {};
cancel.title = 'Cancel';
cancel.errorMessage = 'Failed to hide menu';

export const constructHeaderActionButtons = ({
backgroundData: { mute, subscriptions, ownUser },
stream,
export const constructTopicActionButtons = ({
backgroundData: { mute, ownUser, streams, subscriptions, unread },
streamId,
topic,
}: {|
backgroundData: $ReadOnly<{
mute: MuteState,
streams: Map<number, Stream>,
subscriptions: Subscription[],
unread: UnreadState,
ownUser: User,
...
}>,
stream: string,
streamId: number,
topic: string,
|}): Button<HeaderArgs>[] => {
|}): Button<TopicArgs>[] => {
const buttons = [];
if (ownUser.is_admin) {
buttons.push(deleteTopic);
}
if (isTopicMuted(stream, topic, mute)) {
const unreadCount = getUnreadCountForTopic(unread, streamId, topic);
if (unreadCount > 0) {
buttons.push(markTopicAsRead);
}
const stream = streams.get(streamId);
invariant(stream !== undefined, 'Stream with provided streamId not found.');
if (isTopicMuted(stream.name, topic, mute)) {
buttons.push(unmuteTopic);
} else {
buttons.push(muteTopic);
}
const sub = subscriptions.find(x => x.name === stream);
const sub = subscriptions.find(x => x.stream_id === streamId);
if (sub && !sub.in_home_view) {
buttons.push(unmuteStream);
} else {
Expand Down Expand Up @@ -338,10 +352,7 @@ export const constructNonHeaderActionButtons = ({
}
};

function makeButtonCallback<Args: HeaderArgs | MessageArgs>(
buttonList: Button<Args>[],
args: Args,
) {
function makeButtonCallback<Args: TopicArgs | MessageArgs>(buttonList: Button<Args>[], args: Args) {
return buttonIndex => {
(async () => {
const pressedButton: Button<Args> = buttonList[buttonIndex];
Expand Down Expand Up @@ -392,12 +403,12 @@ export const showMessageActionSheet = ({
);
};

export const showHeaderActionSheet = ({
export const showTopicActionSheet = ({
showActionSheetWithOptions,
callbacks,
backgroundData,
topic,
stream,
streamId,
}: {|
showActionSheetWithOptions: ShowActionSheetWithOptions,
callbacks: {|
Expand All @@ -407,29 +418,33 @@ export const showHeaderActionSheet = ({
backgroundData: $ReadOnly<{
auth: Auth,
mute: MuteState,
streams: Map<number, Stream>,
subscriptions: Subscription[],
unread: UnreadState,
ownUser: User,
flags: FlagsState,
...
}>,
stream: string,
streamId: number,
topic: string,
|}): void => {
const buttonList = constructHeaderActionButtons({
const buttonList = constructTopicActionButtons({
backgroundData,
stream,
streamId,
topic,
});
const stream = backgroundData.streams.get(streamId);
invariant(stream !== undefined, 'Stream with provided streamId not found.');
showActionSheetWithOptions(
{
title: `#${stream} > ${topic}`,
title: `#${stream.name} > ${topic}`,
options: buttonList.map(button => callbacks._(button.title)),
cancelButtonIndex: buttonList.length - 1,
},
makeButtonCallback(buttonList, {
...backgroundData,
...callbacks,
stream,
streamId,
topic,
}),
);
Expand Down
Loading