diff --git a/src/__tests__/lib/exampleData.js b/src/__tests__/lib/exampleData.js index 24b12b90d86..158a236bca3 100644 --- a/src/__tests__/lib/exampleData.js +++ b/src/__tests__/lib/exampleData.js @@ -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 @@ -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, }); diff --git a/src/message/__tests__/messageActionSheet-test.js b/src/action-sheets/__tests__/action-sheet-test.js similarity index 62% rename from src/message/__tests__/messageActionSheet-test.js rename to src/action-sheets/__tests__/action-sheet-test.js index 8b4c7fa5c26..115261ef496 100644 --- a/src/message/__tests__/messageActionSheet-test.js +++ b/src/action-sheets/__tests__/action-sheet-test.js @@ -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'; @@ -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, @@ -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, @@ -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({ diff --git a/src/message/messageActionSheet.js b/src/action-sheets/index.js similarity index 73% rename from src/message/messageActionSheet.js rename to src/action-sheets/index.js index a0f6706705d..00dfd2be8b9 100644 --- a/src/message/messageActionSheet.js +++ b/src/action-sheets/index.js @@ -32,6 +32,7 @@ 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 = ( @@ -39,11 +40,21 @@ export type ShowActionSheetWithOptions = ( (number) => void, ) => void; +type StreamArgs = { + auth: Auth, + streamId: number, + subscriptions: Map, + streams: Map, + dispatch: Dispatch, + _: GetText, + ... +}; + type TopicArgs = { auth: Auth, streamId: number, topic: string, - subscriptions: $ReadOnlyArray, + subscriptions: Map, streams: Map, dispatch: Dispatch, _: GetText, @@ -60,7 +71,7 @@ type MessageArgs = { ... }; -type Button = {| +type Button = {| (Args): void | Promise, /** The label for the button. */ @@ -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); }; @@ -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, + userSettingStreamNotification: boolean, + ... + }>, + streamId: number, +|}): Button[] => { + 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, @@ -240,7 +332,7 @@ export const constructTopicActionButtons = ({ backgroundData: $ReadOnly<{ mute: MuteState, streams: Map, - subscriptions: $ReadOnlyArray, + subscriptions: Map, unread: UnreadState, ownUser: User, ... @@ -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 { @@ -352,7 +444,10 @@ export const constructNonHeaderActionButtons = ({ } }; -function makeButtonCallback(buttonList: Button[], args: Args) { +function makeButtonCallback( + buttonList: Button[], + args: Args, +) { return buttonIndex => { (async () => { const pressedButton: Button = buttonList[buttonIndex]; @@ -380,7 +475,7 @@ export const showMessageActionSheet = ({ |}, backgroundData: $ReadOnly<{ auth: Auth, - subscriptions: $ReadOnlyArray, + subscriptions: Map, ownUser: User, flags: FlagsState, ... @@ -419,7 +514,7 @@ export const showTopicActionSheet = ({ auth: Auth, mute: MuteState, streams: Map, - subscriptions: $ReadOnlyArray, + subscriptions: Map, unread: UnreadState, ownUser: User, flags: FlagsState, @@ -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, + subscriptions: Map, + 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, + }), + ); +}; diff --git a/src/api/modelTypes.js b/src/api/modelTypes.js index fad54dbb7e9..6e0c3d13536 100644 --- a/src/api/modelTypes.js +++ b/src/api/modelTypes.js @@ -310,18 +310,23 @@ export type Stream = $ReadOnly<{| history_public_to_subscribers: boolean, |}>; -export type Subscription = $ReadOnly<{| - ...$Exact, - 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>, + +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, diff --git a/src/lightbox/Lightbox.js b/src/lightbox/Lightbox.js index 2677b310bfc..fed73315e97 100644 --- a/src/lightbox/Lightbox.js +++ b/src/lightbox/Lightbox.js @@ -11,7 +11,7 @@ import { useActionSheet } from '@expo/react-native-action-sheet'; import * as NavigationService from '../nav/NavigationService'; import type { Message } from '../types'; import { useSelector } from '../react-redux'; -import type { ShowActionSheetWithOptions } from '../message/messageActionSheet'; +import type { ShowActionSheetWithOptions } from '../action-sheets'; import { getAuth, getGlobalSession } from '../selectors'; import { getResource } from '../utils/url'; import LightboxHeader from './LightboxHeader'; diff --git a/src/streams/StreamItem.js b/src/streams/StreamItem.js index bc75956f3d9..f2e2d2eb166 100644 --- a/src/streams/StreamItem.js +++ b/src/streams/StreamItem.js @@ -2,7 +2,23 @@ import React, { useContext } from 'react'; import type { Node } from 'react'; import { View } from 'react-native'; +// $FlowFixMe[untyped-import] +import { useActionSheet } from '@expo/react-native-action-sheet'; +import invariant from 'invariant'; +import { showStreamActionSheet } from '../action-sheets'; +import type { ShowActionSheetWithOptions } from '../action-sheets'; +import { TranslationContext } from '../boot/TranslationProvider'; +import { useDispatch, useSelector } from '../react-redux'; +import { + getAuth, + getFlags, + getSubscriptionsById, + getStreamsById, + getStreamsByName, + getOwnUser, + getSettings, +} from '../selectors'; import styles, { createStyleSheet, ThemeContext } from '../styles'; import { RawLabel, Touchable, UnreadCount, ZulipSwitch } from '../common'; import { foregroundColorFromBackground } from '../utils/color'; @@ -75,6 +91,21 @@ export default function StreamItem(props: Props): Node { onSwitch, } = props; + const showActionSheetWithOptions: ShowActionSheetWithOptions = useActionSheet() + .showActionSheetWithOptions; + const _ = useContext(TranslationContext); + const dispatch = useDispatch(); + const backgroundData = useSelector(state => ({ + auth: getAuth(state), + ownUser: getOwnUser(state), + streams: getStreamsById(state), + subscriptions: getSubscriptionsById(state), + flags: getFlags(state), + userSettingStreamNotification: getSettings(state).streamNotification, + })); + const stream = useSelector(state => getStreamsByName(state).get(name)); + invariant(stream !== undefined, 'No stream with provided stream name was found.'); + const { backgroundColor: themeBackgroundColor, color: themeColor } = useContext(ThemeContext); const wrapperStyle = [styles.listItem, { backgroundColor }, isMuted && componentStyles.muted]; @@ -90,7 +121,17 @@ export default function StreamItem(props: Props): Node { : themeColor; return ( - onPress(name)}> + onPress(name)} + onLongPress={() => { + showStreamActionSheet({ + showActionSheetWithOptions, + callbacks: { dispatch, _ }, + backgroundData, + streamId: stream.stream_id, + }); + }} + > diff --git a/src/streams/StreamSettingsScreen.js b/src/streams/StreamSettingsScreen.js index e0d4c0eae3f..7f46aa5548a 100644 --- a/src/streams/StreamSettingsScreen.js +++ b/src/streams/StreamSettingsScreen.js @@ -21,6 +21,7 @@ import { import styles from '../styles'; import { getSubscriptionsById } from '../subscriptions/subscriptionSelectors'; import * as api from '../api'; +import getIsNotificationEnabled from './getIsNotificationEnabled'; type Props = $ReadOnly<{| navigation: AppNavigationProp<'stream-settings'>, @@ -69,7 +70,7 @@ export default function StreamSettingsScreen(props: Props): Node { }, [auth, stream]); const handleToggleStreamPushNotification = useCallback(() => { - const currentValue = subscription?.push_notifications ?? userSettingStreamNotification; + const currentValue = getIsNotificationEnabled(subscription, userSettingStreamNotification); dispatch(setSubscriptionProperty(stream.stream_id, 'push_notifications', !currentValue)); }, [dispatch, stream, subscription, userSettingStreamNotification]); @@ -93,7 +94,7 @@ export default function StreamSettingsScreen(props: Props): Node { diff --git a/src/streams/TopicItem.js b/src/streams/TopicItem.js index 8a23d49675f..64b8bacc743 100644 --- a/src/streams/TopicItem.js +++ b/src/streams/TopicItem.js @@ -8,15 +8,15 @@ import invariant from 'invariant'; import styles, { BRAND_COLOR, createStyleSheet } from '../styles'; import { RawLabel, Touchable, UnreadCount } from '../common'; -import { showTopicActionSheet } from '../message/messageActionSheet'; -import type { ShowActionSheetWithOptions } from '../message/messageActionSheet'; +import { showTopicActionSheet } from '../action-sheets'; +import type { ShowActionSheetWithOptions } from '../action-sheets'; import { TranslationContext } from '../boot/TranslationProvider'; import { useDispatch, useSelector } from '../react-redux'; import { getAuth, getMute, getFlags, - getSubscriptions, + getSubscriptionsById, getStreamsById, getStreamsByName, getOwnUser, @@ -59,7 +59,7 @@ export default function TopicItem(props: Props): Node { mute: getMute(state), streams: getStreamsById(state), streamsByName: getStreamsByName(state), - subscriptions: getSubscriptions(state), + subscriptions: getSubscriptionsById(state), unread: getUnread(state), ownUser: getOwnUser(state), flags: getFlags(state), diff --git a/src/streams/getIsNotificationEnabled.js b/src/streams/getIsNotificationEnabled.js new file mode 100644 index 00000000000..85dc9cf671e --- /dev/null +++ b/src/streams/getIsNotificationEnabled.js @@ -0,0 +1,13 @@ +/* @flow strict-local */ +import type { Subscription } from '../types'; + +/** + * Whether push notifications are enabled for this stream. + * + * The `userSettingsStreamNotification` parameter should correspond to + * `state.settings.streamNotification`. + */ +export default ( + subscription: Subscription | void, + userSettingStreamNotification: boolean, +): boolean => subscription?.push_notifications ?? userSettingStreamNotification; diff --git a/src/title/TitleStream.js b/src/title/TitleStream.js index 627d64550de..7770223edf6 100644 --- a/src/title/TitleStream.js +++ b/src/title/TitleStream.js @@ -16,13 +16,14 @@ import { getAuth, getMute, getFlags, - getSubscriptions, + getSubscriptionsById, getStreamsById, getOwnUser, getStreamInNarrow, + getSettings, } from '../selectors'; -import { showTopicActionSheet } from '../message/messageActionSheet'; -import type { ShowActionSheetWithOptions } from '../message/messageActionSheet'; +import { showStreamActionSheet, showTopicActionSheet } from '../action-sheets'; +import type { ShowActionSheetWithOptions } from '../action-sheets'; import { getUnread } from '../unread/unreadModel'; type Props = $ReadOnly<{| @@ -52,10 +53,11 @@ export default function TitleStream(props: Props): Node { auth: getAuth(state), mute: getMute(state), streams: getStreamsById(state), - subscriptions: getSubscriptions(state), + subscriptions: getSubscriptionsById(state), unread: getUnread(state), ownUser: getOwnUser(state), flags: getFlags(state), + userSettingStreamNotification: getSettings(state).streamNotification, })); const showActionSheetWithOptions: ShowActionSheetWithOptions = useActionSheet() @@ -75,7 +77,14 @@ export default function TitleStream(props: Props): Node { topic: topicOfNarrow(narrow), }); } - : undefined + : () => { + showStreamActionSheet({ + showActionSheetWithOptions, + callbacks: { dispatch, _ }, + backgroundData, + streamId: stream.stream_id, + }); + } } > diff --git a/src/webview/MessageList.js b/src/webview/MessageList.js index 10adb26fbd6..2a6eca07185 100644 --- a/src/webview/MessageList.js +++ b/src/webview/MessageList.js @@ -40,14 +40,15 @@ import { getMute, getMutedUsers, getOwnUser, + getSettings, getGlobalSettings, - getSubscriptions, + getSubscriptionsById, getStreamsById, getStreamsByName, getRealm, } from '../selectors'; import { withGetText } from '../boot/TranslationProvider'; -import type { ShowActionSheetWithOptions } from '../message/messageActionSheet'; +import type { ShowActionSheetWithOptions } from '../action-sheets'; import { getMessageListElementsMemoized } from '../message/messageSelectors'; import type { WebViewInboundEvent } from './generateInboundEvents'; import type { WebViewOutboundEvent } from './handleOutboundEvents'; @@ -88,10 +89,11 @@ export type BackgroundData = $ReadOnly<{| ownUser: User, streams: Map, streamsByName: Map, - subscriptions: $ReadOnlyArray, + subscriptions: Map, unread: UnreadState, theme: ThemeName, twentyFourHourTime: boolean, + userSettingStreamNotification: boolean, |}>; type OuterProps = $ReadOnly<{| @@ -351,10 +353,11 @@ const MessageList: ComponentType = connect( ownUser: getOwnUser(state), streams: getStreamsById(state), streamsByName: getStreamsByName(state), - subscriptions: getSubscriptions(state), + subscriptions: getSubscriptionsById(state), unread: getUnread(state), theme: getGlobalSettings(state).theme, twentyFourHourTime: getRealm(state).twentyFourHourTime, + userSettingStreamNotification: getSettings(state).streamNotification, }; return { diff --git a/src/webview/handleOutboundEvents.js b/src/webview/handleOutboundEvents.js index 275c7e5ae6e..2bb52690102 100644 --- a/src/webview/handleOutboundEvents.js +++ b/src/webview/handleOutboundEvents.js @@ -7,7 +7,7 @@ import * as api from '../api'; import config from '../config'; import type { Dispatch, GetText, Message, Narrow, Outbox, EditMessage, UserId } from '../types'; import type { BackgroundData } from './MessageList'; -import type { ShowActionSheetWithOptions } from '../message/messageActionSheet'; +import type { ShowActionSheetWithOptions } from '../action-sheets'; import type { JSONableDict } from '../utils/jsonable'; import { showToast } from '../utils/info'; import { streamNameOfStreamMessage, pmUiRecipientsFromMessage } from '../utils/recipient'; @@ -24,7 +24,7 @@ import { navigateToLightbox, messageLinkPress, } from '../actions'; -import { showTopicActionSheet, showMessageActionSheet } from '../message/messageActionSheet'; +import { showTopicActionSheet, showMessageActionSheet } from '../action-sheets'; import { ensureUnreachable } from '../types'; import { base64Utf8Decode } from '../utils/encoding'; diff --git a/src/webview/html/header.js b/src/webview/html/header.js index fca5826f867..8d38da6ed41 100644 --- a/src/webview/html/header.js +++ b/src/webview/html/header.js @@ -55,7 +55,15 @@ export default ( `; } else if (headerStyle === 'full') { const streamName = streamNameOfStreamMessage(message); - const stream = subscriptions.find(x => x.name === streamName); + + // TODO(#3339): like `subscriptions.find(…)`, but for the values from …ById + let stream = null; + for (const sub of subscriptions.values()) { + if (sub.name === streamName) { + stream = sub; + break; + } + } const backgroundColor = stream ? stream.color : 'hsl(0, 0%, 80%)'; const textColor = foregroundColorFromBackground(backgroundColor); diff --git a/static/translations/messages_en.json b/static/translations/messages_en.json index 93455bfcebd..5b299d56691 100644 --- a/static/translations/messages_en.json +++ b/static/translations/messages_en.json @@ -119,6 +119,15 @@ "Message group": "Message group", "starred": "starred", "Enable notifications": "Enable notifications", + "Failed to enable notifications": "Failed to enable notifications", + "Disable notifications": "Disable notifications", + "Failed to disable notifications": "Failed to disable notifications", + "Failed to subscribe": "Failed to subscribe", + "Failed to unsubscribe": "Failed to unsubscribe", + "Pin to top": "Pin to top", + "Failed to pin to top": "Failed to pin to top", + "Unpin from top": "Unpin from top", + "Failed to unpin from top": "Failed to unpin from top", "Jot down something": "Jot down something", "Message {recipient}": "Message {recipient}", "{username} will not be notified unless you subscribe them to this stream.": "{username} will not be notified unless you subscribe them to this stream.",