From 8e9034601d66947840adb21399559f405a80da49 Mon Sep 17 00:00:00 2001 From: Mihaela Dumitru Date: Fri, 17 Dec 2021 14:39:15 +0200 Subject: [PATCH] feat(chat/settings) - add ephemeral chat notifications with user settings support (#10617) --- config.js | 1 + lang/main.json | 2 + .../base/react/components/web/Message.js | 86 +++++++++++++++++++ react/features/base/settings/reducer.js | 3 + .../chat/components/web/ChatMessage.js | 33 +------ react/features/chat/functions.js | 57 ++++++++---- react/features/chat/middleware.js | 12 ++- react/features/notifications/actions.js | 18 ++++ .../components/AbstractNotification.js | 6 ++ .../components/native/Notification.js | 3 +- .../components/web/Notification.js | 51 +++++++++-- .../components/web/NotificationsContainer.js | 4 + react/features/notifications/constants.js | 10 +++ react/features/settings/actions.js | 11 +++ .../settings/components/web/MoreTab.js | 65 +++++++++++++- .../settings/components/web/SettingsDialog.js | 9 +- react/features/settings/functions.js | 25 ++++++ 17 files changed, 331 insertions(+), 65 deletions(-) create mode 100644 react/features/base/react/components/web/Message.js diff --git a/config.js b/config.js index b33fcce7b..ee6dd51df 100644 --- a/config.js +++ b/config.js @@ -1161,6 +1161,7 @@ var config = { // 'lobby.joinRejectedMessage', // shown when while in a lobby, user's request to join is rejected // 'lobby.notificationTitle', // shown when lobby is toggled and when join requests are allowed / denied // 'localRecording.localRecording', // shown when a local recording is started + // 'notify.chatMessages', // shown when receiving chat messages while the chat window is closed // 'notify.disconnected', // shown when a participant has left // 'notify.connectedOneMember', // show when a participant joined // 'notify.connectedTwoMembers', // show when two participants joined simultaneously diff --git a/lang/main.json b/lang/main.json index 4e6822f69..de96003de 100644 --- a/lang/main.json +++ b/lang/main.json @@ -581,10 +581,12 @@ "allowedUnmute": "You can unmute your microphone, start your camera or share your screen.", "audioUnmuteBlockedTitle": "Mic unmute blocked!", "audioUnmuteBlockedDescription": "Mic unmute operation has been temporarily blocked because of system limits.", + "chatMessages": "Chat messages", "connectedOneMember": "{{name}} joined the meeting", "connectedThreePlusMembers": "{{name}} and many others joined the meeting", "connectedTwoMembers": "{{first}} and {{second}} joined the meeting", "disconnected": "disconnected", + "displayNotifications": "Display notifications for", "focus": "Conference focus", "focusFail": "{{component}} not available - retry in {{ms}} sec", "hostAskedUnmute": "The moderator would like you to speak", diff --git a/react/features/base/react/components/web/Message.js b/react/features/base/react/components/web/Message.js new file mode 100644 index 000000000..77cec123d --- /dev/null +++ b/react/features/base/react/components/web/Message.js @@ -0,0 +1,86 @@ +// @flow + +import React, { Component } from 'react'; +import { toArray } from 'react-emoji-render'; + +import Linkify from './Linkify'; + +type Props = { + + /** + * The body of the message. + */ + text: string +}; + +/** + * Renders the content of a chat message. + */ +class Message extends Component { + /** + * Initializes a new {@code Message} instance. + * + * @param {Props} props - The props of the component. + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + // Bind event handlers so they are only bound once for every instance + this._processMessage = this._processMessage.bind(this); + } + + /** + * Parses and builds the message tokens to include emojis and urls. + * + * @returns {Array} + */ + _processMessage() { + const { text } = this.props; + const message = []; + + // Tokenize the text in order to avoid emoji substitution for URLs + const tokens = text ? text.split(' ') : []; + + const content = []; + + for (const token of tokens) { + if (token.includes('://')) { + + // Bypass the emojification when urls are involved + content.push(token); + } else { + content.push(...toArray(token, { className: 'smiley' })); + } + + content.push(' '); + } + + content.forEach(token => { + if (typeof token === 'string' && token !== ' ') { + message.push({ token }); + } else { + message.push(token); + } + }); + + return message; + } + + _processMessage: () => Array>; + + /** + * Implements React's {@link Component#render()}. + * + * @returns {ReactElement} + */ + render() { + return ( + <> + { this._processMessage() } + + ); + } +} + +export default Message; diff --git a/react/features/base/settings/reducer.js b/react/features/base/settings/reducer.js index 3ba7b7d44..68c91170a 100644 --- a/react/features/base/settings/reducer.js +++ b/react/features/base/settings/reducer.js @@ -41,6 +41,9 @@ const DEFAULT_STATE = { userSelectedMicDeviceId: undefined, userSelectedAudioOutputDeviceLabel: undefined, userSelectedCameraDeviceLabel: undefined, + userSelectedNotifications: { + 'notify.chatMessages': true + }, userSelectedMicDeviceLabel: undefined, userSelectedSkipPrejoin: undefined }; diff --git a/react/features/chat/components/web/ChatMessage.js b/react/features/chat/components/web/ChatMessage.js index 66315e125..4fd389c7b 100644 --- a/react/features/chat/components/web/ChatMessage.js +++ b/react/features/chat/components/web/ChatMessage.js @@ -1,10 +1,9 @@ // @flow import React from 'react'; -import { toArray } from 'react-emoji-render'; import { translate } from '../../../base/i18n'; -import { Linkify } from '../../../base/react'; +import Message from '../../../base/react/components/web/Message'; import { MESSAGE_TYPE_LOCAL } from '../../constants'; import AbstractChatMessage, { type Props } from '../AbstractChatMessage'; @@ -22,34 +21,6 @@ class ChatMessage extends AbstractChatMessage { */ render() { const { message, t } = this.props; - const processedMessage = []; - - const txt = this._getMessageText(); - - // Tokenize the text in order to avoid emoji substitution for URLs. - const tokens = txt.split(' '); - - // Content is an array of text and emoji components - const content = []; - - for (const token of tokens) { - if (token.includes('://')) { - // It contains a link, bypass the emojification. - content.push(token); - } else { - content.push(...toArray(token, { className: 'smiley' })); - } - - content.push(' '); - } - - content.forEach(i => { - if (typeof i === 'string' && i !== ' ') { - processedMessage.push({ i }); - } else { - processedMessage.push(i); - } - }); return (
{ : t('chat.messageAccessibleTitle', { user: this.props.message.displayName }) } - { processedMessage } +
{ message.privateMessage && this._renderPrivateNotice() } diff --git a/react/features/chat/functions.js b/react/features/chat/functions.js index 6ce421fb8..6a0e0b0e2 100644 --- a/react/features/chat/functions.js +++ b/react/features/chat/functions.js @@ -7,39 +7,56 @@ import { escapeRegexp } from '../base/util'; /** * An ASCII emoticon regexp array to find and replace old-style ASCII - * emoticons (such as :O) to new Unicode representation, so then devices - * and browsers that support them can render these natively without - * a 3rd party component. + * emoticons (such as :O) with the new Unicode representation, so that + * devices and browsers that support them can render these natively + * without a 3rd party component. * * NOTE: this is currently only used on mobile, but it can be used * on web too once we drop support for browsers that don't support * unicode emoji rendering. */ -const EMOTICON_REGEXP_ARRAY: Array> = []; +const ASCII_EMOTICON_REGEXP_ARRAY: Array> = []; + +/** + * An emoji regexp array to find and replace alias emoticons + * (such as :smiley:) with the new Unicode representation, so that + * devices and browsers that support them can render these natively + * without a 3rd party component. + * + * NOTE: this is currently only used on mobile, but it can be used + * on web too once we drop support for browsers that don't support + * unicode emoji rendering. + */ +const SLACK_EMOJI_REGEXP_ARRAY: Array> = []; (function() { for (const [ key, value ] of Object.entries(aliases)) { - let escapedValues; - const asciiEmojies = emojiAsciiAliases[key]; - // Adding ascii emoticons - if (asciiEmojies) { - escapedValues = asciiEmojies.map(v => escapeRegexp(v)); - } else { - escapedValues = []; + // Add ASCII emoticons + const asciiEmoticons = emojiAsciiAliases[key]; + + if (asciiEmoticons) { + const asciiEscapedValues = asciiEmoticons.map(v => escapeRegexp(v)); + + const asciiRegexp = `(${asciiEscapedValues.join('|')})`; + + // Escape urls + const formattedAsciiRegexp = key === 'confused' + ? `(?=(${asciiRegexp}))(:(?!//).)` + : asciiRegexp; + + ASCII_EMOTICON_REGEXP_ARRAY.push([ new RegExp(formattedAsciiRegexp, 'g'), value ]); } - // Adding slack-type emoji format - escapedValues.push(escapeRegexp(`:${key}:`)); + // Add slack-type emojis + const emojiRegexp = `\\B(${escapeRegexp(`:${key}:`)})\\B`; - const regexp = `\\B(${escapedValues.join('|')})\\B`; - - EMOTICON_REGEXP_ARRAY.push([ new RegExp(regexp, 'g'), value ]); + SLACK_EMOJI_REGEXP_ARRAY.push([ new RegExp(emojiRegexp, 'g'), value ]); } })(); /** - * Replaces ascii and other non-unicode emoticons with unicode emojis to let the emojis be rendered + * Replaces ASCII and other non-unicode emoticons with unicode emojis to let the emojis be rendered * by the platform native renderer. * * @param {string} message - The message to parse and replace. @@ -48,7 +65,11 @@ const EMOTICON_REGEXP_ARRAY: Array> = []; export function replaceNonUnicodeEmojis(message: string) { let replacedMessage = message; - for (const [ regexp, replaceValue ] of EMOTICON_REGEXP_ARRAY) { + for (const [ regexp, replaceValue ] of SLACK_EMOJI_REGEXP_ARRAY) { + replacedMessage = replacedMessage.replace(regexp, replaceValue); + } + + for (const [ regexp, replaceValue ] of ASCII_EMOTICON_REGEXP_ARRAY) { replacedMessage = replacedMessage.replace(regexp, replaceValue); } diff --git a/react/features/chat/middleware.js b/react/features/chat/middleware.js index 92180bef8..3d236bd8c 100644 --- a/react/features/chat/middleware.js +++ b/react/features/chat/middleware.js @@ -17,6 +17,7 @@ import { } from '../base/participants'; import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux'; import { playSound, registerSound, unregisterSound } from '../base/sounds'; +import { NOTIFICATION_TIMEOUT_TYPE, showMessageNotification } from '../notifications'; import { resetNbUnreadPollsMessages } from '../polls/actions'; import { ADD_REACTION_MESSAGE } from '../reactions/actionTypes'; import { pushReactions } from '../reactions/actions.any'; @@ -304,7 +305,7 @@ function _handleReceivedMessage({ dispatch, getState }, const state = getState(); const { isOpen: isChatOpen } = state['features/chat']; const { iAmRecorder } = state['features/base/config']; - const { soundsIncomingMessage: soundEnabled } = state['features/base/settings']; + const { soundsIncomingMessage: soundEnabled, userSelectedNotifications } = state['features/base/settings']; if (soundEnabled && shouldPlaySound && !isChatOpen) { dispatch(playSound(INCOMING_MSG_SOUND_ID)); @@ -318,6 +319,7 @@ function _handleReceivedMessage({ dispatch, getState }, const hasRead = participant.local || isChatOpen; const timestampToDate = timestamp ? new Date(timestamp) : new Date(); const millisecondsTimestamp = timestampToDate.getTime(); + const shouldShowNotification = userSelectedNotifications['notify.chatMessages'] && !hasRead && !isReaction; dispatch(addMessage({ displayName, @@ -331,6 +333,13 @@ function _handleReceivedMessage({ dispatch, getState }, isReaction })); + if (shouldShowNotification) { + dispatch(showMessageNotification({ + title: displayName, + description: message + }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM)); + } + if (typeof APP !== 'undefined') { // Logic for web only: @@ -345,7 +354,6 @@ function _handleReceivedMessage({ dispatch, getState }, if (!iAmRecorder) { dispatch(showToolbox(4000)); } - } } diff --git a/react/features/notifications/actions.js b/react/features/notifications/actions.js index d74a5d43c..c215a32db 100644 --- a/react/features/notifications/actions.js +++ b/react/features/notifications/actions.js @@ -14,6 +14,7 @@ import { SHOW_NOTIFICATION } from './actionTypes'; import { + NOTIFICATION_ICON, NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TIMEOUT, NOTIFICATION_TYPE, @@ -156,6 +157,23 @@ export function showWarningNotification(props: Object, type: ?string) { }, type); } +/** + * Queues a message notification for display. + * + * @param {Object} props - The props needed to show the notification component. + * @param {string} type - Notification type. + * @returns {Object} + */ +export function showMessageNotification(props: Object, type: ?string) { + return showNotification({ + ...props, + concatText: true, + titleKey: 'notify.chatMessages', + appearance: NOTIFICATION_TYPE.NORMAL, + icon: NOTIFICATION_ICON.MESSAGE + }, type); +} + /** * An array of names of participants that have joined the conference. The array * is replaced with an empty array as notifications are displayed. diff --git a/react/features/notifications/components/AbstractNotification.js b/react/features/notifications/components/AbstractNotification.js index 98d0579e3..df61b37d1 100644 --- a/react/features/notifications/components/AbstractNotification.js +++ b/react/features/notifications/components/AbstractNotification.js @@ -55,6 +55,12 @@ export type Props = { */ hideErrorSupportLink: boolean, + /** + * The type of icon to be displayed. If not passed in, the appearance + * type will be used. + */ + icon?: String, + /** * Whether or not the dismiss button should be displayed. */ diff --git a/react/features/notifications/components/native/Notification.js b/react/features/notifications/components/native/Notification.js index 37d287419..0cc7cc908 100644 --- a/react/features/notifications/components/native/Notification.js +++ b/react/features/notifications/components/native/Notification.js @@ -5,6 +5,7 @@ import { Text, TouchableOpacity, View } from 'react-native'; import { translate } from '../../../base/i18n'; import { Icon, IconClose } from '../../../base/icons'; +import { replaceNonUnicodeEmojis } from '../../../chat/functions'; import AbstractNotification, { type Props } from '../AbstractNotification'; @@ -81,7 +82,7 @@ class Notification extends AbstractNotification { key = { index } numberOfLines = { maxLines } style = { styles.contentText }> - { line } + { replaceNonUnicodeEmojis(line) } )); } diff --git a/react/features/notifications/components/web/Notification.js b/react/features/notifications/components/web/Notification.js index c53f2a768..d77941de7 100644 --- a/react/features/notifications/components/web/Notification.js +++ b/react/features/notifications/components/web/Notification.js @@ -1,12 +1,17 @@ // @flow import Flag from '@atlaskit/flag'; +import EditorErrorIcon from '@atlaskit/icon/glyph/editor/error'; import EditorInfoIcon from '@atlaskit/icon/glyph/editor/info'; +import EditorSuccessIcon from '@atlaskit/icon/glyph/editor/success'; +import EditorWarningIcon from '@atlaskit/icon/glyph/editor/warning'; +import QuestionsIcon from '@atlaskit/icon/glyph/questions'; import React from 'react'; import { translate } from '../../../base/i18n'; +import Message from '../../../base/react/components/web/Message'; import { colors } from '../../../base/ui/Tokens'; -import { NOTIFICATION_TYPE } from '../../constants'; +import { NOTIFICATION_ICON, NOTIFICATION_TYPE } from '../../constants'; import AbstractNotification, { type Props } from '../AbstractNotification'; @@ -71,12 +76,12 @@ class Notification extends AbstractNotification { * @returns {ReactElement} */ _renderDescription() { - const description = this._getDescription(); + const description = this._getDescription().join(' '); // the id is used for testing the UI return (

- { description } +

); } @@ -145,6 +150,35 @@ class Notification extends AbstractNotification { } } + /** + * Returns the Icon type component to be used, based on icon or appearance. + * + * @returns {ReactElement} + */ + _getIcon() { + let Icon; + + switch (this.props.icon || this.props.appearance) { + case NOTIFICATION_ICON.ERROR: + Icon = EditorErrorIcon; + break; + case NOTIFICATION_ICON.WARNING: + Icon = EditorWarningIcon; + break; + case NOTIFICATION_ICON.SUCCESS: + Icon = EditorSuccessIcon; + break; + case NOTIFICATION_ICON.MESSAGE: + Icon = QuestionsIcon; + break; + default: + Icon = EditorInfoIcon; + break; + } + + return Icon; + } + /** * Creates an icon component depending on the configured notification * appearance. @@ -153,17 +187,18 @@ class Notification extends AbstractNotification { * @returns {ReactElement} */ _mapAppearanceToIcon() { - const appearance = this.props.appearance; - const secIconColor = ICON_COLOR[this.props.appearance]; + const { appearance, icon } = this.props; + const secIconColor = ICON_COLOR[appearance]; const iconSize = 'medium'; + const Icon = this._getIcon(); - return (<> + return (
- - ); +
); } } diff --git a/react/features/notifications/components/web/NotificationsContainer.js b/react/features/notifications/components/web/NotificationsContainer.js index fa51c30a2..65adeccfd 100644 --- a/react/features/notifications/components/web/NotificationsContainer.js +++ b/react/features/notifications/components/web/NotificationsContainer.js @@ -86,6 +86,10 @@ const useStyles = theme => { color: theme.palette.field01 }, + '& div.message > span': { + color: theme.palette.link01Active + }, + '& .ribbon': { width: '4px', height: 'calc(100% - 16px)', diff --git a/react/features/notifications/constants.js b/react/features/notifications/constants.js index 9b4fcacab..586b5cc1f 100644 --- a/react/features/notifications/constants.js +++ b/react/features/notifications/constants.js @@ -46,6 +46,16 @@ export const NOTIFICATION_TYPE_PRIORITIES = { [NOTIFICATION_TYPE.WARNING]: 4 }; +/** + * The set of possible notification icons. + * + * @enum {string} + */ +export const NOTIFICATION_ICON = { + ...NOTIFICATION_TYPE, + MESSAGE: 'message' +}; + /** * The identifier of the raise hand notification. * diff --git a/react/features/settings/actions.js b/react/features/settings/actions.js index 97a3918da..211c9ead0 100644 --- a/react/features/settings/actions.js +++ b/react/features/settings/actions.js @@ -101,6 +101,17 @@ export function submitMoreTab(newState: Object): Function { }); } + const enabledNotifications = newState.enabledNotifications; + + if (enabledNotifications !== currentState.enabledNotifications) { + dispatch(updateSettings({ + userSelectedNotifications: { + ...getState()['features/base/settings'].userSelectedNotifications, + ...enabledNotifications + } + })); + } + if (newState.currentLanguage !== currentState.currentLanguage) { i18next.changeLanguage(newState.currentLanguage); } diff --git a/react/features/settings/components/web/MoreTab.js b/react/features/settings/components/web/MoreTab.js index 3f514c537..a57758ace 100644 --- a/react/features/settings/components/web/MoreTab.js +++ b/react/features/settings/components/web/MoreTab.js @@ -50,6 +50,11 @@ export type Props = { */ languages: Array, + /** + * The types of enabled notifications that can be configured and their specific visibility. + */ + enabledNotifications: Object, + /** * Whether or not to display the language select dropdown. */ @@ -60,6 +65,11 @@ export type Props = { */ showModeratorSettings: boolean, + /** + * Whether or not to display notifications settings. + */ + showNotificationsSettings: boolean, + /** * Whether or not to display the prejoin settings section. */ @@ -122,6 +132,7 @@ class MoreTab extends AbstractDialogTab { this._onFramerateItemSelect = this._onFramerateItemSelect.bind(this); this._onLanguageDropdownOpenChange = this._onLanguageDropdownOpenChange.bind(this); this._onLanguageItemSelect = this._onLanguageItemSelect.bind(this); + this._onEnabledNotificationsChanged = this._onEnabledNotificationsChanged.bind(this); this._onShowPrejoinPageChanged = this._onShowPrejoinPageChanged.bind(this); this._onKeyboardShortcutEnableChanged = this._onKeyboardShortcutEnableChanged.bind(this); this._onHideSelfViewChanged = this._onHideSelfViewChanged.bind(this); @@ -220,6 +231,26 @@ class MoreTab extends AbstractDialogTab { _onKeyboardShortcutEnableChanged: (Object) => void; + /** + * Callback invoked to select if the given type of + * notifications should be shown. + * + * @param {Object} e - The key event to handle. + * @param {string} type - The type of the notification. + * + * @returns {void} + */ + _onEnabledNotificationsChanged({ target: { checked } }, type) { + super._onChange({ + enabledNotifications: { + ...this.props.enabledNotifications, + [type]: checked + } + }); + } + + _onEnabledNotificationsChanged: (Object, string) => void; + /** * Callback invoked to select if global keyboard shortcuts * should be enabled. @@ -428,6 +459,37 @@ class MoreTab extends AbstractDialogTab { ); } + /** + * Returns the React Element for modifying the enabled notifications settings. + * + * @private + * @returns {ReactElement} + */ + _renderNotificationsSettings() { + const { t, enabledNotifications } = this.props; + + return ( +
+

+ { t('notify.displayNotifications') } +

+ { + Object.keys(enabledNotifications).map(key => ( + this._onEnabledNotificationsChanged(e, key) } /> + )) + } +
+ ); + } + /** * Returns the React element that needs to be displayed on the right half of the more tabs. * @@ -453,13 +515,14 @@ class MoreTab extends AbstractDialogTab { * @returns {ReactElement} */ _renderSettingsLeft() { - const { disableHideSelfView, showPrejoinSettings } = this.props; + const { disableHideSelfView, showNotificationsSettings, showPrejoinSettings } = this.props; return (
{ showPrejoinSettings && this._renderPrejoinScreenSettings() } + { showNotificationsSettings && this._renderNotificationsSettings() } { this._renderKeyboardShortcutCheckbox() } { !disableHideSelfView && this._renderSelfViewCheckbox() }
diff --git a/react/features/settings/components/web/SettingsDialog.js b/react/features/settings/components/web/SettingsDialog.js index a01684bd9..35c6c478e 100644 --- a/react/features/settings/components/web/SettingsDialog.js +++ b/react/features/settings/components/web/SettingsDialog.js @@ -31,7 +31,6 @@ import MoreTab from './MoreTab'; import ProfileTab from './ProfileTab'; import SoundsTab from './SoundsTab'; -declare var APP: Object; declare var interfaceConfig: Object; /** @@ -144,7 +143,8 @@ function _mapStateToProps(state) { const moreTabProps = getMoreTabProps(state); const moderatorTabProps = getModeratorTabProps(state); const { showModeratorSettings } = moderatorTabProps; - const { showLanguageSettings, showPrejoinSettings } = moreTabProps; + const { showLanguageSettings, showNotificationsSettings, showPrejoinSettings } = moreTabProps; + const showMoreTab = showLanguageSettings || showNotificationsSettings || showPrejoinSettings; const showProfileSettings = configuredTabs.includes('profile') && !state['features/base/config'].disableProfile; const showCalendarSettings @@ -231,7 +231,7 @@ function _mapStateToProps(state) { }); } - if (showLanguageSettings || showPrejoinSettings) { + if (showMoreTab) { tabs.push({ name: SETTINGS_TABS.MORE, component: MoreTab, @@ -245,7 +245,8 @@ function _mapStateToProps(state) { currentFramerate: tabState.currentFramerate, currentLanguage: tabState.currentLanguage, hideSelfView: tabState.hideSelfView, - showPrejoinPage: tabState.showPrejoinPage + showPrejoinPage: tabState.showPrejoinPage, + enabledNotifications: tabState.enabledNotifications }; }, styles: 'settings-pane more-pane', diff --git a/react/features/settings/functions.js b/react/features/settings/functions.js index 129aa0eac..1a1789c61 100644 --- a/react/features/settings/functions.js +++ b/react/features/settings/functions.js @@ -79,6 +79,28 @@ export function normalizeUserInputURL(url: string) { /* eslint-enable no-param-reassign */ } +/** + * Returns the notification types and their user selected configuration. + * + * @param {(Function|Object)} stateful -The (whole) redux state, or redux's + * {@code getState} function to be used to retrieve the state. + * @returns {Object} - The section of notifications to be configured. + */ +export function getNotificationsMap(stateful: Object | Function) { + const state = toState(stateful); + const { notifications } = state['features/base/config']; + const { userSelectedNotifications } = state['features/base/settings']; + + return Object.keys(userSelectedNotifications) + .filter(key => !notifications || notifications.includes(key)) + .reduce((notificationsMap, key) => { + return { + ...notificationsMap, + [key]: userSelectedNotifications[key] + }; + }, {}); +} + /** * Returns the properties for the "More" tab from settings dialog from Redux * state. @@ -92,6 +114,7 @@ export function getMoreTabProps(stateful: Object | Function) { const framerate = state['features/screen-share'].captureFrameRate ?? SS_DEFAULT_FRAME_RATE; const language = i18next.language || DEFAULT_LANGUAGE; const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || []; + const enabledNotifications = getNotificationsMap(stateful); // when self view is controlled by the config we hide the settings const { disableSelfView, disableSelfViewSettings } = state['features/base/config']; @@ -104,6 +127,8 @@ export function getMoreTabProps(stateful: Object | Function) { hideSelfView: getHideSelfView(state), languages: LANGUAGES, showLanguageSettings: configuredTabs.includes('language'), + enabledNotifications, + showNotificationsSettings: Object.keys(enabledNotifications).length > 0, showPrejoinPage: !state['features/base/settings'].userSelectedSkipPrejoin, showPrejoinSettings: state['features/base/config'].prejoinConfig?.enabled };