Quellcode durchsuchen

feat(chat/settings) - add ephemeral chat notifications with user settings support (#10617)

master
Mihaela Dumitru vor 3 Jahren
Ursprung
Commit
8e9034601d
Es ist kein Account mit der E-Mail-Adresse des Committers verbunden

+ 1
- 0
config.js Datei anzeigen

@@ -1161,6 +1161,7 @@ var config = {
1161 1161
     //     'lobby.joinRejectedMessage', // shown when while in a lobby, user's request to join is rejected
1162 1162
     //     'lobby.notificationTitle', // shown when lobby is toggled and when join requests are allowed / denied
1163 1163
     //     'localRecording.localRecording', // shown when a local recording is started
1164
+    //     'notify.chatMessages', // shown when receiving chat messages while the chat window is closed
1164 1165
     //     'notify.disconnected', // shown when a participant has left
1165 1166
     //     'notify.connectedOneMember', // show when a participant joined
1166 1167
     //     'notify.connectedTwoMembers', // show when two participants joined simultaneously

+ 2
- 0
lang/main.json Datei anzeigen

@@ -581,10 +581,12 @@
581 581
         "allowedUnmute": "You can unmute your microphone, start your camera or share your screen.",
582 582
         "audioUnmuteBlockedTitle": "Mic unmute blocked!",
583 583
         "audioUnmuteBlockedDescription": "Mic unmute operation has been temporarily blocked because of system limits.",
584
+        "chatMessages": "Chat messages",
584 585
         "connectedOneMember": "{{name}} joined the meeting",
585 586
         "connectedThreePlusMembers": "{{name}} and many others joined the meeting",
586 587
         "connectedTwoMembers": "{{first}} and {{second}} joined the meeting",
587 588
         "disconnected": "disconnected",
589
+        "displayNotifications": "Display notifications for",
588 590
         "focus": "Conference focus",
589 591
         "focusFail": "{{component}} not available - retry in {{ms}} sec",
590 592
         "hostAskedUnmute": "The moderator would like you to speak",

+ 86
- 0
react/features/base/react/components/web/Message.js Datei anzeigen

@@ -0,0 +1,86 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { toArray } from 'react-emoji-render';
5
+
6
+import Linkify from './Linkify';
7
+
8
+type Props = {
9
+
10
+    /**
11
+     * The body of the message.
12
+     */
13
+    text: string
14
+};
15
+
16
+/**
17
+ * Renders the content of a chat message.
18
+ */
19
+class Message extends Component<Props> {
20
+    /**
21
+     * Initializes a new {@code Message} instance.
22
+     *
23
+     * @param {Props} props - The props of the component.
24
+     * @inheritdoc
25
+     */
26
+    constructor(props: Props) {
27
+        super(props);
28
+
29
+        // Bind event handlers so they are only bound once for every instance
30
+        this._processMessage = this._processMessage.bind(this);
31
+    }
32
+
33
+    /**
34
+     * Parses and builds the message tokens to include emojis and urls.
35
+     *
36
+     * @returns {Array<string|ReactElement>}
37
+     */
38
+    _processMessage() {
39
+        const { text } = this.props;
40
+        const message = [];
41
+
42
+        // Tokenize the text in order to avoid emoji substitution for URLs
43
+        const tokens = text ? text.split(' ') : [];
44
+
45
+        const content = [];
46
+
47
+        for (const token of tokens) {
48
+            if (token.includes('://')) {
49
+
50
+                // Bypass the emojification when urls are involved
51
+                content.push(token);
52
+            } else {
53
+                content.push(...toArray(token, { className: 'smiley' }));
54
+            }
55
+
56
+            content.push(' ');
57
+        }
58
+
59
+        content.forEach(token => {
60
+            if (typeof token === 'string' && token !== ' ') {
61
+                message.push(<Linkify key = { token }>{ token }</Linkify>);
62
+            } else {
63
+                message.push(token);
64
+            }
65
+        });
66
+
67
+        return message;
68
+    }
69
+
70
+    _processMessage: () => Array<string | React$Element<*>>;
71
+
72
+    /**
73
+     * Implements React's {@link Component#render()}.
74
+     *
75
+     * @returns {ReactElement}
76
+     */
77
+    render() {
78
+        return (
79
+            <>
80
+                { this._processMessage() }
81
+            </>
82
+        );
83
+    }
84
+}
85
+
86
+export default Message;

+ 3
- 0
react/features/base/settings/reducer.js Datei anzeigen

@@ -41,6 +41,9 @@ const DEFAULT_STATE = {
41 41
     userSelectedMicDeviceId: undefined,
42 42
     userSelectedAudioOutputDeviceLabel: undefined,
43 43
     userSelectedCameraDeviceLabel: undefined,
44
+    userSelectedNotifications: {
45
+        'notify.chatMessages': true
46
+    },
44 47
     userSelectedMicDeviceLabel: undefined,
45 48
     userSelectedSkipPrejoin: undefined
46 49
 };

+ 2
- 31
react/features/chat/components/web/ChatMessage.js Datei anzeigen

@@ -1,10 +1,9 @@
1 1
 // @flow
2 2
 
3 3
 import React from 'react';
4
-import { toArray } from 'react-emoji-render';
5 4
 
6 5
 import { translate } from '../../../base/i18n';
7
-import { Linkify } from '../../../base/react';
6
+import Message from '../../../base/react/components/web/Message';
8 7
 import { MESSAGE_TYPE_LOCAL } from '../../constants';
9 8
 import AbstractChatMessage, { type Props } from '../AbstractChatMessage';
10 9
 
@@ -22,34 +21,6 @@ class ChatMessage extends AbstractChatMessage<Props> {
22 21
      */
23 22
     render() {
24 23
         const { message, t } = this.props;
25
-        const processedMessage = [];
26
-
27
-        const txt = this._getMessageText();
28
-
29
-        // Tokenize the text in order to avoid emoji substitution for URLs.
30
-        const tokens = txt.split(' ');
31
-
32
-        // Content is an array of text and emoji components
33
-        const content = [];
34
-
35
-        for (const token of tokens) {
36
-            if (token.includes('://')) {
37
-                // It contains a link, bypass the emojification.
38
-                content.push(token);
39
-            } else {
40
-                content.push(...toArray(token, { className: 'smiley' }));
41
-            }
42
-
43
-            content.push(' ');
44
-        }
45
-
46
-        content.forEach(i => {
47
-            if (typeof i === 'string' && i !== ' ') {
48
-                processedMessage.push(<Linkify key = { i }>{ i }</Linkify>);
49
-            } else {
50
-                processedMessage.push(i);
51
-            }
52
-        });
53 24
 
54 25
         return (
55 26
             <div
@@ -66,7 +37,7 @@ class ChatMessage extends AbstractChatMessage<Props> {
66 37
                                         : t('chat.messageAccessibleTitle',
67 38
                                         { user: this.props.message.displayName }) }
68 39
                                 </span>
69
-                                { processedMessage }
40
+                                <Message text = { this._getMessageText() } />
70 41
                             </div>
71 42
                             { message.privateMessage && this._renderPrivateNotice() }
72 43
                         </div>

+ 40
- 19
react/features/chat/functions.js Datei anzeigen

@@ -7,39 +7,56 @@ import { escapeRegexp } from '../base/util';
7 7
 
8 8
 /**
9 9
  * An ASCII emoticon regexp array to find and replace old-style ASCII
10
- * emoticons (such as :O) to new Unicode representation, so then devices
11
- * and browsers that support them can render these natively without
12
- * a 3rd party component.
10
+ * emoticons (such as :O) with the new Unicode representation, so that
11
+ * devices and browsers that support them can render these natively
12
+ * without a 3rd party component.
13 13
  *
14 14
  * NOTE: this is currently only used on mobile, but it can be used
15 15
  * on web too once we drop support for browsers that don't support
16 16
  * unicode emoji rendering.
17 17
  */
18
-const EMOTICON_REGEXP_ARRAY: Array<Array<Object>> = [];
18
+const ASCII_EMOTICON_REGEXP_ARRAY: Array<Array<Object>> = [];
19
+
20
+/**
21
+ * An emoji regexp array to find and replace alias emoticons
22
+ * (such as :smiley:) with the new Unicode representation, so that
23
+ * devices and browsers that support them can render these natively
24
+ * without a 3rd party component.
25
+ *
26
+ * NOTE: this is currently only used on mobile, but it can be used
27
+ * on web too once we drop support for browsers that don't support
28
+ * unicode emoji rendering.
29
+ */
30
+const SLACK_EMOJI_REGEXP_ARRAY: Array<Array<Object>> = [];
19 31
 
20 32
 (function() {
21 33
     for (const [ key, value ] of Object.entries(aliases)) {
22
-        let escapedValues;
23
-        const asciiEmojies = emojiAsciiAliases[key];
24
-
25
-        // Adding ascii emoticons
26
-        if (asciiEmojies) {
27
-            escapedValues = asciiEmojies.map(v => escapeRegexp(v));
28
-        } else {
29
-            escapedValues = [];
30
-        }
31 34
 
32
-        // Adding slack-type emoji format
33
-        escapedValues.push(escapeRegexp(`:${key}:`));
35
+        // Add ASCII emoticons
36
+        const asciiEmoticons = emojiAsciiAliases[key];
34 37
 
35
-        const regexp = `\\B(${escapedValues.join('|')})\\B`;
38
+        if (asciiEmoticons) {
39
+            const asciiEscapedValues = asciiEmoticons.map(v => escapeRegexp(v));
36 40
 
37
-        EMOTICON_REGEXP_ARRAY.push([ new RegExp(regexp, 'g'), value ]);
41
+            const asciiRegexp = `(${asciiEscapedValues.join('|')})`;
42
+
43
+            // Escape urls
44
+            const formattedAsciiRegexp = key === 'confused'
45
+                ? `(?=(${asciiRegexp}))(:(?!//).)`
46
+                : asciiRegexp;
47
+
48
+            ASCII_EMOTICON_REGEXP_ARRAY.push([ new RegExp(formattedAsciiRegexp, 'g'), value ]);
49
+        }
50
+
51
+        // Add slack-type emojis
52
+        const emojiRegexp = `\\B(${escapeRegexp(`:${key}:`)})\\B`;
53
+
54
+        SLACK_EMOJI_REGEXP_ARRAY.push([ new RegExp(emojiRegexp, 'g'), value ]);
38 55
     }
39 56
 })();
40 57
 
41 58
 /**
42
- * Replaces ascii and other non-unicode emoticons with unicode emojis to let the emojis be rendered
59
+ * Replaces ASCII and other non-unicode emoticons with unicode emojis to let the emojis be rendered
43 60
  * by the platform native renderer.
44 61
  *
45 62
  * @param {string} message - The message to parse and replace.
@@ -48,7 +65,11 @@ const EMOTICON_REGEXP_ARRAY: Array<Array<Object>> = [];
48 65
 export function replaceNonUnicodeEmojis(message: string) {
49 66
     let replacedMessage = message;
50 67
 
51
-    for (const [ regexp, replaceValue ] of EMOTICON_REGEXP_ARRAY) {
68
+    for (const [ regexp, replaceValue ] of SLACK_EMOJI_REGEXP_ARRAY) {
69
+        replacedMessage = replacedMessage.replace(regexp, replaceValue);
70
+    }
71
+
72
+    for (const [ regexp, replaceValue ] of ASCII_EMOTICON_REGEXP_ARRAY) {
52 73
         replacedMessage = replacedMessage.replace(regexp, replaceValue);
53 74
     }
54 75
 

+ 10
- 2
react/features/chat/middleware.js Datei anzeigen

@@ -17,6 +17,7 @@ import {
17 17
 } from '../base/participants';
18 18
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
19 19
 import { playSound, registerSound, unregisterSound } from '../base/sounds';
20
+import { NOTIFICATION_TIMEOUT_TYPE, showMessageNotification } from '../notifications';
20 21
 import { resetNbUnreadPollsMessages } from '../polls/actions';
21 22
 import { ADD_REACTION_MESSAGE } from '../reactions/actionTypes';
22 23
 import { pushReactions } from '../reactions/actions.any';
@@ -304,7 +305,7 @@ function _handleReceivedMessage({ dispatch, getState },
304 305
     const state = getState();
305 306
     const { isOpen: isChatOpen } = state['features/chat'];
306 307
     const { iAmRecorder } = state['features/base/config'];
307
-    const { soundsIncomingMessage: soundEnabled } = state['features/base/settings'];
308
+    const { soundsIncomingMessage: soundEnabled, userSelectedNotifications } = state['features/base/settings'];
308 309
 
309 310
     if (soundEnabled && shouldPlaySound && !isChatOpen) {
310 311
         dispatch(playSound(INCOMING_MSG_SOUND_ID));
@@ -318,6 +319,7 @@ function _handleReceivedMessage({ dispatch, getState },
318 319
     const hasRead = participant.local || isChatOpen;
319 320
     const timestampToDate = timestamp ? new Date(timestamp) : new Date();
320 321
     const millisecondsTimestamp = timestampToDate.getTime();
322
+    const shouldShowNotification = userSelectedNotifications['notify.chatMessages'] && !hasRead && !isReaction;
321 323
 
322 324
     dispatch(addMessage({
323 325
         displayName,
@@ -331,6 +333,13 @@ function _handleReceivedMessage({ dispatch, getState },
331 333
         isReaction
332 334
     }));
333 335
 
336
+    if (shouldShowNotification) {
337
+        dispatch(showMessageNotification({
338
+            title: displayName,
339
+            description: message
340
+        }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
341
+    }
342
+
334 343
     if (typeof APP !== 'undefined') {
335 344
         // Logic for web only:
336 345
 
@@ -345,7 +354,6 @@ function _handleReceivedMessage({ dispatch, getState },
345 354
         if (!iAmRecorder) {
346 355
             dispatch(showToolbox(4000));
347 356
         }
348
-
349 357
     }
350 358
 }
351 359
 

+ 18
- 0
react/features/notifications/actions.js Datei anzeigen

@@ -14,6 +14,7 @@ import {
14 14
     SHOW_NOTIFICATION
15 15
 } from './actionTypes';
16 16
 import {
17
+    NOTIFICATION_ICON,
17 18
     NOTIFICATION_TIMEOUT_TYPE,
18 19
     NOTIFICATION_TIMEOUT,
19 20
     NOTIFICATION_TYPE,
@@ -156,6 +157,23 @@ export function showWarningNotification(props: Object, type: ?string) {
156 157
     }, type);
157 158
 }
158 159
 
160
+/**
161
+ * Queues a message notification for display.
162
+ *
163
+ * @param {Object} props - The props needed to show the notification component.
164
+ * @param {string} type - Notification type.
165
+ * @returns {Object}
166
+ */
167
+export function showMessageNotification(props: Object, type: ?string) {
168
+    return showNotification({
169
+        ...props,
170
+        concatText: true,
171
+        titleKey: 'notify.chatMessages',
172
+        appearance: NOTIFICATION_TYPE.NORMAL,
173
+        icon: NOTIFICATION_ICON.MESSAGE
174
+    }, type);
175
+}
176
+
159 177
 /**
160 178
  * An array of names of participants that have joined the conference. The array
161 179
  * is replaced with an empty array as notifications are displayed.

+ 6
- 0
react/features/notifications/components/AbstractNotification.js Datei anzeigen

@@ -55,6 +55,12 @@ export type Props = {
55 55
      */
56 56
     hideErrorSupportLink: boolean,
57 57
 
58
+    /**
59
+     * The type of icon to be displayed. If not passed in, the appearance
60
+     * type will be used.
61
+     */
62
+    icon?: String,
63
+
58 64
     /**
59 65
      * Whether or not the dismiss button should be displayed.
60 66
      */

+ 2
- 1
react/features/notifications/components/native/Notification.js Datei anzeigen

@@ -5,6 +5,7 @@ import { Text, TouchableOpacity, View } from 'react-native';
5 5
 
6 6
 import { translate } from '../../../base/i18n';
7 7
 import { Icon, IconClose } from '../../../base/icons';
8
+import { replaceNonUnicodeEmojis } from '../../../chat/functions';
8 9
 import AbstractNotification, {
9 10
     type Props
10 11
 } from '../AbstractNotification';
@@ -81,7 +82,7 @@ class Notification extends AbstractNotification<Props> {
81 82
                     key = { index }
82 83
                     numberOfLines = { maxLines }
83 84
                     style = { styles.contentText }>
84
-                    { line }
85
+                    { replaceNonUnicodeEmojis(line) }
85 86
                 </Text>
86 87
             ));
87 88
         }

+ 43
- 8
react/features/notifications/components/web/Notification.js Datei anzeigen

@@ -1,12 +1,17 @@
1 1
 // @flow
2 2
 
3 3
 import Flag from '@atlaskit/flag';
4
+import EditorErrorIcon from '@atlaskit/icon/glyph/editor/error';
4 5
 import EditorInfoIcon from '@atlaskit/icon/glyph/editor/info';
6
+import EditorSuccessIcon from '@atlaskit/icon/glyph/editor/success';
7
+import EditorWarningIcon from '@atlaskit/icon/glyph/editor/warning';
8
+import QuestionsIcon from '@atlaskit/icon/glyph/questions';
5 9
 import React from 'react';
6 10
 
7 11
 import { translate } from '../../../base/i18n';
12
+import Message from '../../../base/react/components/web/Message';
8 13
 import { colors } from '../../../base/ui/Tokens';
9
-import { NOTIFICATION_TYPE } from '../../constants';
14
+import { NOTIFICATION_ICON, NOTIFICATION_TYPE } from '../../constants';
10 15
 import AbstractNotification, {
11 16
     type Props
12 17
 } from '../AbstractNotification';
@@ -71,12 +76,12 @@ class Notification extends AbstractNotification<Props> {
71 76
      * @returns {ReactElement}
72 77
      */
73 78
     _renderDescription() {
74
-        const description = this._getDescription();
79
+        const description = this._getDescription().join(' ');
75 80
 
76 81
         // the id is used for testing the UI
77 82
         return (
78 83
             <p data-testid = { this._getDescriptionKey() } >
79
-                { description }
84
+                <Message text = { description } />
80 85
             </p>
81 86
         );
82 87
     }
@@ -145,6 +150,35 @@ class Notification extends AbstractNotification<Props> {
145 150
         }
146 151
     }
147 152
 
153
+    /**
154
+     * Returns the Icon type component to be used, based on icon or appearance.
155
+     *
156
+     * @returns {ReactElement}
157
+     */
158
+    _getIcon() {
159
+        let Icon;
160
+
161
+        switch (this.props.icon || this.props.appearance) {
162
+        case NOTIFICATION_ICON.ERROR:
163
+            Icon = EditorErrorIcon;
164
+            break;
165
+        case NOTIFICATION_ICON.WARNING:
166
+            Icon = EditorWarningIcon;
167
+            break;
168
+        case NOTIFICATION_ICON.SUCCESS:
169
+            Icon = EditorSuccessIcon;
170
+            break;
171
+        case NOTIFICATION_ICON.MESSAGE:
172
+            Icon = QuestionsIcon;
173
+            break;
174
+        default:
175
+            Icon = EditorInfoIcon;
176
+            break;
177
+        }
178
+
179
+        return Icon;
180
+    }
181
+
148 182
     /**
149 183
      * Creates an icon component depending on the configured notification
150 184
      * appearance.
@@ -153,17 +187,18 @@ class Notification extends AbstractNotification<Props> {
153 187
      * @returns {ReactElement}
154 188
      */
155 189
     _mapAppearanceToIcon() {
156
-        const appearance = this.props.appearance;
157
-        const secIconColor = ICON_COLOR[this.props.appearance];
190
+        const { appearance, icon } = this.props;
191
+        const secIconColor = ICON_COLOR[appearance];
158 192
         const iconSize = 'medium';
193
+        const Icon = this._getIcon();
159 194
 
160
-        return (<>
195
+        return (<div className = { icon }>
161 196
             <div className = { `ribbon ${appearance}` } />
162
-            <EditorInfoIcon
197
+            <Icon
163 198
                 label = { appearance }
164 199
                 secondaryColor = { secIconColor }
165 200
                 size = { iconSize } />
166
-        </>);
201
+        </div>);
167 202
     }
168 203
 }
169 204
 

+ 4
- 0
react/features/notifications/components/web/NotificationsContainer.js Datei anzeigen

@@ -86,6 +86,10 @@ const useStyles = theme => {
86 86
                 color: theme.palette.field01
87 87
             },
88 88
 
89
+            '& div.message > span': {
90
+                color: theme.palette.link01Active
91
+            },
92
+
89 93
             '& .ribbon': {
90 94
                 width: '4px',
91 95
                 height: 'calc(100% - 16px)',

+ 10
- 0
react/features/notifications/constants.js Datei anzeigen

@@ -46,6 +46,16 @@ export const NOTIFICATION_TYPE_PRIORITIES = {
46 46
     [NOTIFICATION_TYPE.WARNING]: 4
47 47
 };
48 48
 
49
+/**
50
+ * The set of possible notification icons.
51
+ *
52
+ * @enum {string}
53
+ */
54
+export const NOTIFICATION_ICON = {
55
+    ...NOTIFICATION_TYPE,
56
+    MESSAGE: 'message'
57
+};
58
+
49 59
 /**
50 60
  * The identifier of the raise hand notification.
51 61
  *

+ 11
- 0
react/features/settings/actions.js Datei anzeigen

@@ -101,6 +101,17 @@ export function submitMoreTab(newState: Object): Function {
101 101
             });
102 102
         }
103 103
 
104
+        const enabledNotifications = newState.enabledNotifications;
105
+
106
+        if (enabledNotifications !== currentState.enabledNotifications) {
107
+            dispatch(updateSettings({
108
+                userSelectedNotifications: {
109
+                    ...getState()['features/base/settings'].userSelectedNotifications,
110
+                    ...enabledNotifications
111
+                }
112
+            }));
113
+        }
114
+
104 115
         if (newState.currentLanguage !== currentState.currentLanguage) {
105 116
             i18next.changeLanguage(newState.currentLanguage);
106 117
         }

+ 64
- 1
react/features/settings/components/web/MoreTab.js Datei anzeigen

@@ -50,6 +50,11 @@ export type Props = {
50 50
      */
51 51
     languages: Array<string>,
52 52
 
53
+    /**
54
+     * The types of enabled notifications that can be configured and their specific visibility.
55
+     */
56
+    enabledNotifications: Object,
57
+
53 58
     /**
54 59
      * Whether or not to display the language select dropdown.
55 60
      */
@@ -60,6 +65,11 @@ export type Props = {
60 65
      */
61 66
     showModeratorSettings: boolean,
62 67
 
68
+    /**
69
+     * Whether or not to display notifications settings.
70
+     */
71
+    showNotificationsSettings: boolean,
72
+
63 73
     /**
64 74
      * Whether or not to display the prejoin settings section.
65 75
      */
@@ -122,6 +132,7 @@ class MoreTab extends AbstractDialogTab<Props, State> {
122 132
         this._onFramerateItemSelect = this._onFramerateItemSelect.bind(this);
123 133
         this._onLanguageDropdownOpenChange = this._onLanguageDropdownOpenChange.bind(this);
124 134
         this._onLanguageItemSelect = this._onLanguageItemSelect.bind(this);
135
+        this._onEnabledNotificationsChanged = this._onEnabledNotificationsChanged.bind(this);
125 136
         this._onShowPrejoinPageChanged = this._onShowPrejoinPageChanged.bind(this);
126 137
         this._onKeyboardShortcutEnableChanged = this._onKeyboardShortcutEnableChanged.bind(this);
127 138
         this._onHideSelfViewChanged = this._onHideSelfViewChanged.bind(this);
@@ -220,6 +231,26 @@ class MoreTab extends AbstractDialogTab<Props, State> {
220 231
 
221 232
     _onKeyboardShortcutEnableChanged: (Object) => void;
222 233
 
234
+    /**
235
+     * Callback invoked to select if the given type of
236
+     * notifications should be shown.
237
+     *
238
+     * @param {Object} e - The key event to handle.
239
+     * @param {string} type - The type of the notification.
240
+     *
241
+     * @returns {void}
242
+     */
243
+    _onEnabledNotificationsChanged({ target: { checked } }, type) {
244
+        super._onChange({
245
+            enabledNotifications: {
246
+                ...this.props.enabledNotifications,
247
+                [type]: checked
248
+            }
249
+        });
250
+    }
251
+
252
+    _onEnabledNotificationsChanged: (Object, string) => void;
253
+
223 254
     /**
224 255
      * Callback invoked to select if global keyboard shortcuts
225 256
      * should be enabled.
@@ -428,6 +459,37 @@ class MoreTab extends AbstractDialogTab<Props, State> {
428 459
         );
429 460
     }
430 461
 
462
+    /**
463
+     * Returns the React Element for modifying the enabled notifications settings.
464
+     *
465
+     * @private
466
+     * @returns {ReactElement}
467
+     */
468
+    _renderNotificationsSettings() {
469
+        const { t, enabledNotifications } = this.props;
470
+
471
+        return (
472
+            <div
473
+                className = 'settings-sub-pane-element'
474
+                key = 'notifications'>
475
+                <h2 className = 'mock-atlaskit-label'>
476
+                    { t('notify.displayNotifications') }
477
+                </h2>
478
+                {
479
+                    Object.keys(enabledNotifications).map(key => (
480
+                        <Checkbox
481
+                            isChecked = { enabledNotifications[key] }
482
+                            key = { key }
483
+                            label = { t(key) }
484
+                            name = { `show-${key}` }
485
+                            /* eslint-disable-next-line react/jsx-no-bind */
486
+                            onChange = { e => this._onEnabledNotificationsChanged(e, key) } />
487
+                    ))
488
+                }
489
+            </div>
490
+        );
491
+    }
492
+
431 493
     /**
432 494
      * Returns the React element that needs to be displayed on the right half of the more tabs.
433 495
      *
@@ -453,13 +515,14 @@ class MoreTab extends AbstractDialogTab<Props, State> {
453 515
      * @returns {ReactElement}
454 516
      */
455 517
     _renderSettingsLeft() {
456
-        const { disableHideSelfView, showPrejoinSettings } = this.props;
518
+        const { disableHideSelfView, showNotificationsSettings, showPrejoinSettings } = this.props;
457 519
 
458 520
         return (
459 521
             <div
460 522
                 className = 'settings-sub-pane left'
461 523
                 key = 'settings-sub-pane-left'>
462 524
                 { showPrejoinSettings && this._renderPrejoinScreenSettings() }
525
+                { showNotificationsSettings && this._renderNotificationsSettings() }
463 526
                 { this._renderKeyboardShortcutCheckbox() }
464 527
                 { !disableHideSelfView && this._renderSelfViewCheckbox() }
465 528
             </div>

+ 5
- 4
react/features/settings/components/web/SettingsDialog.js Datei anzeigen

@@ -31,7 +31,6 @@ import MoreTab from './MoreTab';
31 31
 import ProfileTab from './ProfileTab';
32 32
 import SoundsTab from './SoundsTab';
33 33
 
34
-declare var APP: Object;
35 34
 declare var interfaceConfig: Object;
36 35
 
37 36
 /**
@@ -144,7 +143,8 @@ function _mapStateToProps(state) {
144 143
     const moreTabProps = getMoreTabProps(state);
145 144
     const moderatorTabProps = getModeratorTabProps(state);
146 145
     const { showModeratorSettings } = moderatorTabProps;
147
-    const { showLanguageSettings, showPrejoinSettings } = moreTabProps;
146
+    const { showLanguageSettings, showNotificationsSettings, showPrejoinSettings } = moreTabProps;
147
+    const showMoreTab = showLanguageSettings || showNotificationsSettings || showPrejoinSettings;
148 148
     const showProfileSettings
149 149
         = configuredTabs.includes('profile') && !state['features/base/config'].disableProfile;
150 150
     const showCalendarSettings
@@ -231,7 +231,7 @@ function _mapStateToProps(state) {
231 231
         });
232 232
     }
233 233
 
234
-    if (showLanguageSettings || showPrejoinSettings) {
234
+    if (showMoreTab) {
235 235
         tabs.push({
236 236
             name: SETTINGS_TABS.MORE,
237 237
             component: MoreTab,
@@ -245,7 +245,8 @@ function _mapStateToProps(state) {
245 245
                     currentFramerate: tabState.currentFramerate,
246 246
                     currentLanguage: tabState.currentLanguage,
247 247
                     hideSelfView: tabState.hideSelfView,
248
-                    showPrejoinPage: tabState.showPrejoinPage
248
+                    showPrejoinPage: tabState.showPrejoinPage,
249
+                    enabledNotifications: tabState.enabledNotifications
249 250
                 };
250 251
             },
251 252
             styles: 'settings-pane more-pane',

+ 25
- 0
react/features/settings/functions.js Datei anzeigen

@@ -79,6 +79,28 @@ export function normalizeUserInputURL(url: string) {
79 79
     /* eslint-enable no-param-reassign */
80 80
 }
81 81
 
82
+/**
83
+ * Returns the notification types and their user selected configuration.
84
+ *
85
+ * @param {(Function|Object)} stateful -The (whole) redux state, or redux's
86
+ * {@code getState} function to be used to retrieve the state.
87
+ * @returns {Object} - The section of notifications to be configured.
88
+ */
89
+export function getNotificationsMap(stateful: Object | Function) {
90
+    const state = toState(stateful);
91
+    const { notifications } = state['features/base/config'];
92
+    const { userSelectedNotifications } = state['features/base/settings'];
93
+
94
+    return Object.keys(userSelectedNotifications)
95
+        .filter(key => !notifications || notifications.includes(key))
96
+        .reduce((notificationsMap, key) => {
97
+            return {
98
+                ...notificationsMap,
99
+                [key]: userSelectedNotifications[key]
100
+            };
101
+        }, {});
102
+}
103
+
82 104
 /**
83 105
  * Returns the properties for the "More" tab from settings dialog from Redux
84 106
  * state.
@@ -92,6 +114,7 @@ export function getMoreTabProps(stateful: Object | Function) {
92 114
     const framerate = state['features/screen-share'].captureFrameRate ?? SS_DEFAULT_FRAME_RATE;
93 115
     const language = i18next.language || DEFAULT_LANGUAGE;
94 116
     const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
117
+    const enabledNotifications = getNotificationsMap(stateful);
95 118
 
96 119
     // when self view is controlled by the config we hide the settings
97 120
     const { disableSelfView, disableSelfViewSettings } = state['features/base/config'];
@@ -104,6 +127,8 @@ export function getMoreTabProps(stateful: Object | Function) {
104 127
         hideSelfView: getHideSelfView(state),
105 128
         languages: LANGUAGES,
106 129
         showLanguageSettings: configuredTabs.includes('language'),
130
+        enabledNotifications,
131
+        showNotificationsSettings: Object.keys(enabledNotifications).length > 0,
107 132
         showPrejoinPage: !state['features/base/settings'].userSelectedSkipPrejoin,
108 133
         showPrejoinSettings: state['features/base/config'].prejoinConfig?.enabled
109 134
     };

Laden…
Abbrechen
Speichern