feat(chat/settings) - add ephemeral chat notifications with user settings support (#10617)
This commit is contained in:
parent
ce0a044742
commit
8e9034601d
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<Props> {
|
||||
/**
|
||||
* 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<string|ReactElement>}
|
||||
*/
|
||||
_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(<Linkify key = { token }>{ token }</Linkify>);
|
||||
} else {
|
||||
message.push(token);
|
||||
}
|
||||
});
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
_processMessage: () => Array<string | React$Element<*>>;
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
{ this._processMessage() }
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Message;
|
|
@ -41,6 +41,9 @@ const DEFAULT_STATE = {
|
|||
userSelectedMicDeviceId: undefined,
|
||||
userSelectedAudioOutputDeviceLabel: undefined,
|
||||
userSelectedCameraDeviceLabel: undefined,
|
||||
userSelectedNotifications: {
|
||||
'notify.chatMessages': true
|
||||
},
|
||||
userSelectedMicDeviceLabel: undefined,
|
||||
userSelectedSkipPrejoin: undefined
|
||||
};
|
||||
|
|
|
@ -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<Props> {
|
|||
*/
|
||||
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(<Linkify key = { i }>{ i }</Linkify>);
|
||||
} else {
|
||||
processedMessage.push(i);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -66,7 +37,7 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
|||
: t('chat.messageAccessibleTitle',
|
||||
{ user: this.props.message.displayName }) }
|
||||
</span>
|
||||
{ processedMessage }
|
||||
<Message text = { this._getMessageText() } />
|
||||
</div>
|
||||
{ message.privateMessage && this._renderPrivateNotice() }
|
||||
</div>
|
||||
|
|
|
@ -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<Array<Object>> = [];
|
||||
const ASCII_EMOTICON_REGEXP_ARRAY: Array<Array<Object>> = [];
|
||||
|
||||
/**
|
||||
* 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<Array<Object>> = [];
|
||||
|
||||
(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<Array<Object>> = [];
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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<Props> {
|
|||
key = { index }
|
||||
numberOfLines = { maxLines }
|
||||
style = { styles.contentText }>
|
||||
{ line }
|
||||
{ replaceNonUnicodeEmojis(line) }
|
||||
</Text>
|
||||
));
|
||||
}
|
||||
|
|
|
@ -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<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderDescription() {
|
||||
const description = this._getDescription();
|
||||
const description = this._getDescription().join(' ');
|
||||
|
||||
// the id is used for testing the UI
|
||||
return (
|
||||
<p data-testid = { this._getDescriptionKey() } >
|
||||
{ description }
|
||||
<Message text = { description } />
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
@ -145,6 +150,35 @@ class Notification extends AbstractNotification<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Props> {
|
|||
* @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 (<div className = { icon }>
|
||||
<div className = { `ribbon ${appearance}` } />
|
||||
<EditorInfoIcon
|
||||
<Icon
|
||||
label = { appearance }
|
||||
secondaryColor = { secIconColor }
|
||||
size = { iconSize } />
|
||||
</>);
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)',
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -50,6 +50,11 @@ export type Props = {
|
|||
*/
|
||||
languages: Array<string>,
|
||||
|
||||
/**
|
||||
* 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<Props, State> {
|
|||
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<Props, State> {
|
|||
|
||||
_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<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the React Element for modifying the enabled notifications settings.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderNotificationsSettings() {
|
||||
const { t, enabledNotifications } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane-element'
|
||||
key = 'notifications'>
|
||||
<h2 className = 'mock-atlaskit-label'>
|
||||
{ t('notify.displayNotifications') }
|
||||
</h2>
|
||||
{
|
||||
Object.keys(enabledNotifications).map(key => (
|
||||
<Checkbox
|
||||
isChecked = { enabledNotifications[key] }
|
||||
key = { key }
|
||||
label = { t(key) }
|
||||
name = { `show-${key}` }
|
||||
/* eslint-disable-next-line react/jsx-no-bind */
|
||||
onChange = { e => this._onEnabledNotificationsChanged(e, key) } />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Props, State> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderSettingsLeft() {
|
||||
const { disableHideSelfView, showPrejoinSettings } = this.props;
|
||||
const { disableHideSelfView, showNotificationsSettings, showPrejoinSettings } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane left'
|
||||
key = 'settings-sub-pane-left'>
|
||||
{ showPrejoinSettings && this._renderPrejoinScreenSettings() }
|
||||
{ showNotificationsSettings && this._renderNotificationsSettings() }
|
||||
{ this._renderKeyboardShortcutCheckbox() }
|
||||
{ !disableHideSelfView && this._renderSelfViewCheckbox() }
|
||||
</div>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue