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
10 changes: 8 additions & 2 deletions src/__tests__/lib/exampleData.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ 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';
import {
getSettings,
getStreamsById,
getStreamsByName,
getSubscriptionsById,
} from '../../selectors';

/* ========================================================================
* Utilities
Expand Down Expand Up @@ -775,8 +780,9 @@ export const backgroundData: BackgroundData = deepFreeze({
ownUser: selfUser,
streams: getStreamsById(baseReduxState),
streamsByName: getStreamsByName(baseReduxState),
subscriptions: [],
subscriptions: getSubscriptionsById(baseReduxState),
unread: baseReduxState.unread,
theme: 'default',
twentyFourHourTime: false,
userSettingStreamNotification: getSettings(baseReduxState).streamNotification,
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import deepFreeze from 'deep-freeze';
import { HOME_NARROW } from '../../utils/narrow';

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

Expand Down Expand Up @@ -97,7 +101,9 @@ describe('constructTopicActionButtons', () => {
});

test('show Unmute stream option if stream is not in home view', () => {
const subscriptions = [{ ...eg.subscription, in_home_view: false, ...stream }];
const subscriptions = deepFreeze(
new Map([[stream.stream_id, { ...eg.subscription, in_home_view: false, ...stream }]]),
);
const buttons = constructTopicActionButtons({
backgroundData: { ...eg.backgroundData, subscriptions, streams },
streamId,
Expand All @@ -107,7 +113,9 @@ describe('constructTopicActionButtons', () => {
});

test('show mute stream option if stream is in home view', () => {
const subscriptions = [{ ...eg.subscription, in_home_view: true, ...stream }];
const subscriptions = deepFreeze(
new Map([[stream.stream_id, { ...eg.subscription, in_home_view: true, ...stream }]]),
);
const buttons = constructTopicActionButtons({
backgroundData: { ...eg.backgroundData, subscriptions, streams },
streamId,
Expand All @@ -116,6 +124,67 @@ describe('constructTopicActionButtons', () => {
expect(buttonTitles(buttons)).toContain('Mute stream');
});

test('show "subscribe" option, if stream is not subscribed yet', () => {
const buttons = constructStreamActionButtons({
backgroundData: { ...eg.backgroundData, streams },
streamId,
});
expect(buttonTitles(buttons)).toContain('Subscribe');
});

test('show "unsubscribe" option, if stream is subscribed', () => {
const subscriptions = deepFreeze(new Map([[eg.subscription.stream_id, eg.subscription]]));
const buttons = constructStreamActionButtons({
backgroundData: { ...eg.backgroundData, subscriptions },
streamId: eg.subscription.stream_id,
});
expect(buttonTitles(buttons)).toContain('Unsubscribe');
});

test('show "enable notification" if push notifications are not enabled for stream', () => {
const subscriptions = deepFreeze(
new Map([[stream.stream_id, { ...eg.subscription, push_notifications: false, ...stream }]]),
);
const buttons = constructStreamActionButtons({
backgroundData: { ...eg.backgroundData, subscriptions },
streamId,
});
expect(buttonTitles(buttons)).toContain('Enable notifications');
});

test('show "disable notification" if push notifications are enabled for stream', () => {
const subscriptions = deepFreeze(
new Map([[stream.stream_id, { ...eg.subscription, push_notifications: true, ...stream }]]),
);
const buttons = constructStreamActionButtons({
backgroundData: { ...eg.backgroundData, subscriptions },
streamId,
});
expect(buttonTitles(buttons)).toContain('Disable notifications');
});

test('show "pin to top" if stream is not pinned to top', () => {
const subscriptions = deepFreeze(
new Map([[stream.stream_id, { ...eg.subscription, pin_to_top: false, ...stream }]]),
);
const buttons = constructStreamActionButtons({
backgroundData: { ...eg.backgroundData, subscriptions },
streamId,
});
expect(buttonTitles(buttons)).toContain('Pin to top');
});

test('show "unpin from top" if stream is pinned to top', () => {
const subscriptions = deepFreeze(
new Map([[stream.stream_id, { ...eg.subscription, pin_to_top: true, ...stream }]]),
);
const buttons = constructStreamActionButtons({
backgroundData: { ...eg.backgroundData, subscriptions },
streamId,
});
expect(buttonTitles(buttons)).toContain('Unpin from top');
});

test('show delete topic option if current user is an admin', () => {
const ownUser = { ...eg.selfUser, is_admin: true };
const buttons = constructTopicActionButtons({
Expand Down
150 changes: 143 additions & 7 deletions src/message/messageActionSheet.js → src/action-sheets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,29 @@ import { navigateToMessageReactionScreen } from '../nav/navActions';
import { deleteMessagesForTopic } from '../topics/topicActions';
import * as logging from '../utils/logging';
import { getUnreadCountForTopic } from '../unread/unreadModel';
import getIsNotificationEnabled from '../streams/getIsNotificationEnabled';

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

type StreamArgs = {
auth: Auth,
streamId: number,
subscriptions: Map<number, Subscription>,
streams: Map<number, Stream>,
dispatch: Dispatch,
_: GetText,
...
};

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

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

/** The label for the button. */
Expand Down Expand Up @@ -196,6 +207,46 @@ const showStreamSettings = ({ streamId, subscriptions }) => {
showStreamSettings.title = 'Stream settings';
showStreamSettings.errorMessage = 'Failed to show stream settings';

const subscribe = async ({ auth, streamId, streams }) => {
const stream = streams.get(streamId);
invariant(stream !== undefined, 'Stream with provided streamId not found.');
await api.subscriptionAdd(auth, [{ name: stream.name }]);
};
subscribe.title = 'Subscribe';
subscribe.errorMessage = 'Failed to subscribe';

const unsubscribe = async ({ auth, streamId, subscriptions }) => {
const sub = subscriptions.get(streamId);
invariant(sub !== undefined, 'Subscription with provided streamId not found.');
await api.subscriptionRemove(auth, [sub.name]);
};
unsubscribe.title = 'Unsubscribe';
unsubscribe.errorMessage = 'Failed to unsubscribe';

const pinToTop = async ({ auth, streamId }) => {
await api.setSubscriptionProperty(auth, streamId, 'pin_to_top', true);
};
pinToTop.title = 'Pin to top';
pinToTop.errorMessage = 'Failed to pin to top';

const unpinFromTop = async ({ auth, streamId }) => {
await api.setSubscriptionProperty(auth, streamId, 'pin_to_top', false);
};
unpinFromTop.title = 'Unpin from top';
unpinFromTop.errorMessage = 'Failed to unpin from top';

const enableNotifications = async ({ auth, streamId }) => {
await api.setSubscriptionProperty(auth, streamId, 'push_notifications', true);
};
enableNotifications.title = 'Enable notifications';
enableNotifications.errorMessage = 'Failed to enable notifications';

const disableNotifications = async ({ auth, streamId }) => {
await api.setSubscriptionProperty(auth, streamId, 'push_notifications', false);
};
disableNotifications.title = 'Disable notifications';
disableNotifications.errorMessage = 'Failed to disable notifications';

const starMessage = async ({ auth, message }) => {
await api.toggleMessageStarred(auth, [message.id], true);
};
Expand Down Expand Up @@ -232,6 +283,47 @@ const cancel = params => {};
cancel.title = 'Cancel';
cancel.errorMessage = 'Failed to hide menu';

export const constructStreamActionButtons = ({
backgroundData: { ownUser, subscriptions, userSettingStreamNotification },
streamId,
}: {|
backgroundData: $ReadOnly<{
ownUser: User,
subscriptions: Map<number, Subscription>,
userSettingStreamNotification: boolean,
...
}>,
streamId: number,
|}): Button<StreamArgs>[] => {
const buttons = [];

const sub = subscriptions.get(streamId);
if (sub) {
if (!sub.in_home_view) {
buttons.push(unmuteStream);
} else {
buttons.push(muteStream);
}
if (sub.pin_to_top) {
buttons.push(unpinFromTop);
} else {
buttons.push(pinToTop);
}
const isNotificationEnabled = getIsNotificationEnabled(sub, userSettingStreamNotification);
if (isNotificationEnabled) {
buttons.push(disableNotifications);
} else {
buttons.push(enableNotifications);
}
buttons.push(unsubscribe);
} else {
buttons.push(subscribe);
}
buttons.push(showStreamSettings);
buttons.push(cancel);
return buttons;
};

export const constructTopicActionButtons = ({
backgroundData: { mute, ownUser, streams, subscriptions, unread },
streamId,
Expand All @@ -240,7 +332,7 @@ export const constructTopicActionButtons = ({
backgroundData: $ReadOnly<{
mute: MuteState,
streams: Map<number, Stream>,
subscriptions: $ReadOnlyArray<Subscription>,
subscriptions: Map<number, Subscription>,
unread: UnreadState,
ownUser: User,
...
Expand All @@ -263,7 +355,7 @@ export const constructTopicActionButtons = ({
} else {
buttons.push(muteTopic);
}
const sub = subscriptions.find(x => x.stream_id === streamId);
const sub = subscriptions.get(streamId);
if (sub && !sub.in_home_view) {
buttons.push(unmuteStream);
} else {
Expand Down Expand Up @@ -352,7 +444,10 @@ export const constructNonHeaderActionButtons = ({
}
};

function makeButtonCallback<Args: TopicArgs | MessageArgs>(buttonList: Button<Args>[], args: Args) {
function makeButtonCallback<Args: StreamArgs | TopicArgs | MessageArgs>(
buttonList: Button<Args>[],
args: Args,
) {
return buttonIndex => {
(async () => {
const pressedButton: Button<Args> = buttonList[buttonIndex];
Expand Down Expand Up @@ -380,7 +475,7 @@ export const showMessageActionSheet = ({
|},
backgroundData: $ReadOnly<{
auth: Auth,
subscriptions: $ReadOnlyArray<Subscription>,
subscriptions: Map<number, Subscription>,
ownUser: User,
flags: FlagsState,
...
Expand Down Expand Up @@ -419,7 +514,7 @@ export const showTopicActionSheet = ({
auth: Auth,
mute: MuteState,
streams: Map<number, Stream>,
subscriptions: $ReadOnlyArray<Subscription>,
subscriptions: Map<number, Subscription>,
unread: UnreadState,
ownUser: User,
flags: FlagsState,
Expand Down Expand Up @@ -449,3 +544,44 @@ export const showTopicActionSheet = ({
}),
);
};

export const showStreamActionSheet = ({
showActionSheetWithOptions,
callbacks,
backgroundData,
streamId,
}: {|
showActionSheetWithOptions: ShowActionSheetWithOptions,
callbacks: {|
dispatch: Dispatch,
_: GetText,
|},
backgroundData: $ReadOnly<{
auth: Auth,
ownUser: User,
streams: Map<number, Stream>,
subscriptions: Map<number, Subscription>,
userSettingStreamNotification: boolean,
...
}>,
streamId: number,
|}): void => {
const buttonList = constructStreamActionButtons({
backgroundData,
streamId,
});
const stream = backgroundData.streams.get(streamId);
invariant(stream !== undefined, 'Stream with provided streamId not found.');
showActionSheetWithOptions(
{
title: `#${stream.name}`,
options: buttonList.map(button => callbacks._(button.title)),
cancelButtonIndex: buttonList.length - 1,
},
makeButtonCallback(buttonList, {
...backgroundData,
...callbacks,
streamId,
}),
);
};
29 changes: 17 additions & 12 deletions src/api/modelTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,18 +310,23 @@ export type Stream = $ReadOnly<{|
history_public_to_subscribers: boolean,
|}>;

export type Subscription = $ReadOnly<{|
...$Exact<Stream>,
color: string,
in_home_view: boolean,
pin_to_top: boolean,
audible_notifications: boolean,
desktop_notifications: boolean,
email_address: string,
is_old_stream: boolean,
push_notifications: null | boolean,
stream_weekly_traffic: number,
|}>;
export type Subscription = {|
...$ReadOnly<$Exact<Stream>>,
+color: string,
+in_home_view: boolean,
+pin_to_top: boolean,
+email_address: string,
+is_old_stream: boolean,
+stream_weekly_traffic: number,

/** (To interpret this value, see `getIsNotificationEnabled`.) */
+push_notifications: null | boolean,

// To meaningfully interpret these, we'll need logic similar to that for
// `push_notifications`. Pending that, the `-` ensures we don't read them.
-audible_notifications: boolean,
-desktop_notifications: boolean,
|};

export type Topic = $ReadOnly<{|
name: string,
Expand Down
Loading