瀏覽代碼

feat(reaction-sounds) Added sounds for reactions (#9775)

* Added sounds for reactions

* Updated reactions list

* Added reactions to sound settings

* Added support for multiple sounds

* Added feature flag for sounds

* Updated sound settings

Moved reactions toggle at the top of the list

* Added disable reaction sounds notification

* Added reaction button zoom for burst intensity

* Fixed raise hand sound

* Fixed register sounds for reactions

* Changed boo emoji

* Updated sounds

* Fixed lint errors

* Fixed reaction sounds file names

* Fix raise hand sound

Play sound only on raise hand not on lower hand

* Fixed types for sound constants

* Fixed type for raise hand sound constant
master
robertpin 4 年之前
父節點
當前提交
c7a91e1974
沒有連結到貢獻者的電子郵件帳戶。

+ 7
- 0
css/_reactions-menu.scss 查看文件

@@ -48,6 +48,13 @@
48 48
 			display: flex;
49 49
 			align-items: center;
50 50
 			justify-content: center;
51
+			transition: font-size ease .1s;
52
+
53
+			@for $i from 1 through 12 {
54
+				&.increase-#{$i}{
55
+					font-size: calc(20px + #{$i}px);
56
+				}
57
+			}
51 58
 		}
52 59
 	}
53 60
 

+ 6
- 3
lang/main.json 查看文件

@@ -591,6 +591,7 @@
591 591
         "moderationStoppedTitle": "Moderation stopped",
592 592
         "moderationToggleDescription": "by {{participantDisplayName}}",
593 593
         "raiseHandAction": "Raise hand",
594
+        "reactionSounds": "Disable sounds",
594 595
         "groupTitle": "Notifications"
595 596
     },
596 597
     "participantsPane": {
@@ -794,6 +795,7 @@
794 795
         "participantJoined": "Participant Joined",
795 796
         "participantLeft": "Participant Left",
796 797
         "playSounds": "Play sound on",
798
+        "reactions": "Meeting reactions",
797 799
         "sameAsSystem": "Same as system ({{label}})",
798 800
         "selectAudioOutput": "Audio output",
799 801
         "selectCamera": "Camera",
@@ -884,7 +886,6 @@
884 886
             "muteEveryonesVideo": "Disable everyone's camera",
885 887
             "muteEveryoneElsesVideo": "Disable everyone else's camera",
886 888
             "participants": "Participants",
887
-            "party": "Party Popper",
888 889
             "pip": "Toggle Picture-in-Picture mode",
889 890
             "privateMessage": "Send private message",
890 891
             "profile": "Edit your profile",
@@ -901,6 +902,7 @@
901 902
             "shareYourScreen": "Start / Stop sharing your screen",
902 903
             "shortcuts": "Toggle shortcuts",
903 904
             "show": "Show on stage",
905
+            "silence": "Silence",
904 906
             "speakerStats": "Toggle speaker statistics",
905 907
             "surprised": "Surprised",
906 908
             "tileView": "Toggle tile view",
@@ -925,6 +927,7 @@
925 927
         "clap": "Clap",
926 928
         "closeChat": "Close chat",
927 929
         "closeReactionsMenu": "Close reactions menu",
930
+        "disableReactionSounds": "You can disable reaction sounds for this meeting",
928 931
         "documentClose": "Close shared document",
929 932
         "documentOpen": "Open shared document",
930 933
         "download": "Download our apps",
@@ -960,7 +963,6 @@
960 963
         "openChat": "Open chat",
961 964
         "openReactionsMenu": "Open reactions menu",
962 965
         "participants": "Participants",
963
-        "party": "Celebration",
964 966
         "pip": "Enter Picture-in-Picture mode",
965 967
         "privateMessage": "Send private message",
966 968
         "profile": "Edit your profile",
@@ -970,7 +972,7 @@
970 972
         "reactionClap": "Send clap reaction",
971 973
         "reactionLaugh": "Send laugh reaction",
972 974
         "reactionLike": "Send thumbs up reaction",
973
-        "reactionParty": "Send party popper reaction",
975
+        "reactionSilence": "Send silence reaction",
974 976
         "reactionSurprised": "Send surprised reaction",
975 977
         "security": "Security options",
976 978
         "Settings": "Settings",
@@ -978,6 +980,7 @@
978 980
         "sharedvideo": "Share video",
979 981
         "shareRoom": "Invite someone",
980 982
         "shortcuts": "View shortcuts",
983
+        "silence": "Silence",
981 984
         "speakerStats": "Speaker stats",
982 985
         "startScreenSharing": "Start screen sharing",
983 986
         "startSubtitles": "Start subtitles",

+ 1
- 0
react/features/base/settings/reducer.js 查看文件

@@ -31,6 +31,7 @@ const DEFAULT_STATE = {
31 31
     soundsParticipantJoined: true,
32 32
     soundsParticipantLeft: true,
33 33
     soundsTalkWhileMuted: true,
34
+    soundsReactions: true,
34 35
     startAudioOnly: false,
35 36
     startWithAudioMuted: false,
36 37
     startWithVideoMuted: false,

+ 5
- 0
react/features/reactions/actionTypes.js 查看文件

@@ -58,3 +58,8 @@ export const SEND_REACTIONS = 'SEND_REACTIONS';
58 58
  * The type of action to adds reactions to the queue.
59 59
  */
60 60
 export const PUSH_REACTIONS = 'PUSH_REACTIONS';
61
+
62
+/**
63
+ * The type of action to display disable notification sounds.
64
+ */
65
+export const SHOW_SOUNDS_NOTIFICATION = 'SHOW_SOUNDS_NOTIFICATION';

+ 13
- 1
react/features/reactions/actions.web.js 查看文件

@@ -1,16 +1,28 @@
1 1
 // @flow
2 2
 
3 3
 import {
4
+    SHOW_SOUNDS_NOTIFICATION,
4 5
     TOGGLE_REACTIONS_VISIBLE
5 6
 } from './actionTypes';
6 7
 
7 8
 /**
8 9
  * Toggles the visibility of the reactions menu.
9 10
  *
10
- * @returns {Function}
11
+ * @returns {Object}
11 12
  */
12 13
 export function toggleReactionsMenuVisibility() {
13 14
     return {
14 15
         type: TOGGLE_REACTIONS_VISIBLE
15 16
     };
16 17
 }
18
+
19
+/**
20
+ * Displays the disable sounds notification.
21
+ *
22
+ * @returns {Object}
23
+ */
24
+export function displayReactionSoundsNotification() {
25
+    return {
26
+        type: SHOW_SOUNDS_NOTIFICATION
27
+    };
28
+}

+ 54
- 5
react/features/reactions/components/web/ReactionButton.js 查看文件

@@ -28,12 +28,28 @@ type Props = AbstractToolbarButtonProps & {
28 28
     label?: string
29 29
 };
30 30
 
31
+/**
32
+ * The type of the React {@code Component} state of {@link ReactionButton}.
33
+ */
34
+type State = {
35
+
36
+    /**
37
+     * Used to determine zoom level on reaction burst.
38
+     */
39
+    increaseLevel: number,
40
+
41
+    /**
42
+     * Timeout ID to reset reaction burst.
43
+     */
44
+    increaseTimeout: TimeoutID | null
45
+}
46
+
31 47
 /**
32 48
  * Represents a button in the reactions menu.
33 49
  *
34 50
  * @extends AbstractToolbarButton
35 51
  */
36
-class ReactionButton extends AbstractToolbarButton<Props> {
52
+class ReactionButton extends AbstractToolbarButton<Props, State> {
37 53
     /**
38 54
      * Default values for {@code ReactionButton} component's properties.
39 55
      *
@@ -52,10 +68,18 @@ class ReactionButton extends AbstractToolbarButton<Props> {
52 68
         super(props);
53 69
 
54 70
         this._onKeyDown = this._onKeyDown.bind(this);
71
+        this._onClickHandler = this._onClickHandler.bind(this);
72
+
73
+        this.state = {
74
+            increaseLevel: 0,
75
+            increaseTimeout: null
76
+        };
55 77
     }
56 78
 
57 79
     _onKeyDown: (Object) => void;
58 80
 
81
+    _onClickHandler: () => void;
82
+
59 83
     /**
60 84
      * Handles 'Enter' key on the button to trigger onClick for accessibility.
61 85
      * We should be handling Space onKeyUp but it conflicts with PTT.
@@ -78,6 +102,28 @@ class ReactionButton extends AbstractToolbarButton<Props> {
78 102
         }
79 103
     }
80 104
 
105
+    /**
106
+     * Handles reaction button click.
107
+     *
108
+     * @returns {void}
109
+     */
110
+    _onClickHandler() {
111
+        this.props.onClick();
112
+        clearTimeout(this.state.increaseTimeout);
113
+        const timeout = setTimeout(() => {
114
+            this.setState({
115
+                increaseLevel: 0
116
+            });
117
+        }, 500);
118
+
119
+        this.setState(state => {
120
+            return {
121
+                increaseLevel: state.increaseLevel + 1,
122
+                increaseTimeout: timeout
123
+            };
124
+        });
125
+    }
126
+
81 127
     /**
82 128
      * Renders the button of this {@code ReactionButton}.
83 129
      *
@@ -92,7 +138,7 @@ class ReactionButton extends AbstractToolbarButton<Props> {
92 138
                 aria-label = { this.props.accessibilityLabel }
93 139
                 aria-pressed = { this.props.toggled }
94 140
                 className = 'toolbox-button'
95
-                onClick = { this.props.onClick }
141
+                onClick = { this._onClickHandler }
96 142
                 onKeyDown = { this._onKeyDown }
97 143
                 role = 'button'
98 144
                 tabIndex = { 0 }>
@@ -113,10 +159,13 @@ class ReactionButton extends AbstractToolbarButton<Props> {
113 159
      * @inheritdoc
114 160
      */
115 161
     _renderIcon() {
162
+        const { toggled, icon, label } = this.props;
163
+        const { increaseLevel } = this.state;
164
+
116 165
         return (
117
-            <div className = { `toolbox-icon ${this.props.toggled ? 'toggled' : ''}` }>
118
-                <span className = 'emoji'>{this.props.icon}</span>
119
-                {this.props.label && <span className = 'text'>{this.props.label}</span>}
166
+            <div className = { `toolbox-icon ${toggled ? 'toggled' : ''}` }>
167
+                <span className = { `emoji increase-${increaseLevel > 12 ? 12 : increaseLevel}` }>{icon}</span>
168
+                {label && <span className = 'text'>{label}</span>}
120 169
             </div>
121 170
         );
122 171
     }

+ 18
- 5
react/features/reactions/components/web/ReactionsMenu.js 查看文件

@@ -11,10 +11,11 @@ import {
11 11
 import { translate } from '../../../base/i18n';
12 12
 import { getLocalParticipant, getParticipantCount, participantUpdated } from '../../../base/participants';
13 13
 import { connect } from '../../../base/redux';
14
+import { playSound } from '../../../base/sounds';
14 15
 import { dockToolbox } from '../../../toolbox/actions.web';
15 16
 import { addReactionToBuffer } from '../../actions.any';
16 17
 import { toggleReactionsMenuVisibility } from '../../actions.web';
17
-import { REACTIONS } from '../../constants';
18
+import { RAISE_HAND_SOUND_ID, REACTIONS } from '../../constants';
18 19
 
19 20
 import ReactionButton from './ReactionButton';
20 21
 
@@ -53,7 +54,12 @@ type Props = {
53 54
     /**
54 55
      * Whether or not it's displayed in the overflow menu.
55 56
      */
56
-    overflowMenu: boolean
57
+    overflowMenu: boolean,
58
+
59
+    /**
60
+     * Whether or not reaction sounds are enabled.
61
+     */
62
+    _reactionSounds: boolean
57 63
 };
58 64
 
59 65
 declare var APP: Object;
@@ -106,11 +112,16 @@ class ReactionsMenu extends Component<Props> {
106 112
      * @returns {void}
107 113
      */
108 114
     _onToolbarToggleRaiseHand() {
115
+        const { dispatch, _raisedHand, _reactionSounds } = this.props;
116
+
109 117
         sendAnalytics(createToolbarEvent(
110 118
             'raise.hand',
111
-            { enable: !this.props._raisedHand }));
119
+            { enable: !_raisedHand }));
112 120
         this._doToggleRaiseHand();
113
-        this.props.dispatch(toggleReactionsMenuVisibility());
121
+        dispatch(toggleReactionsMenuVisibility());
122
+        if (_reactionSounds && _raisedHand) {
123
+            dispatch(playSound(RAISE_HAND_SOUND_ID));
124
+        }
114 125
     }
115 126
 
116 127
     /**
@@ -212,11 +223,13 @@ class ReactionsMenu extends Component<Props> {
212 223
  */
213 224
 function mapStateToProps(state) {
214 225
     const localParticipant = getLocalParticipant(state);
226
+    const { soundsReactions } = state['features/base/settings'];
215 227
 
216 228
     return {
217 229
         _localParticipantID: localParticipant.id,
218 230
         _raisedHand: localParticipant.raisedHand,
219
-        _participantCount: getParticipantCount(state)
231
+        _participantCount: getParticipantCount(state),
232
+        _reactionSounds: soundsReactions
220 233
     };
221 234
 }
222 235
 

+ 102
- 22
react/features/reactions/constants.js 查看文件

@@ -1,47 +1,127 @@
1 1
 // @flow
2 2
 
3
+import {
4
+    CLAP_SOUND_FILES,
5
+    LAUGH_SOUND_FILES,
6
+    LIKE_SOUND_FILES,
7
+    BOO_SOUND_FILES,
8
+    SURPRISE_SOUND_FILES,
9
+    SILENCE_SOUND_FILES
10
+} from './sounds';
11
+
12
+/**
13
+ * The audio ID prefix of the audio element for which the {@link playAudio} action is
14
+ * triggered when a new laugh reaction is received.
15
+ *
16
+ * @type { string }
17
+ */
18
+export const LAUGH_SOUND_ID = 'LAUGH_SOUND_';
19
+
20
+/**
21
+ * The audio ID prefix of the audio element for which the {@link playAudio} action is
22
+ * triggered when a new clap reaction is received.
23
+ *
24
+ * @type {string}
25
+ */
26
+export const CLAP_SOUND_ID = 'CLAP_SOUND_';
27
+
28
+/**
29
+ * The audio ID prefix of the audio element for which the {@link playAudio} action is
30
+ * triggered when a new like reaction is received.
31
+ *
32
+ * @type {string}
33
+ */
34
+export const LIKE_SOUND_ID = 'LIKE_SOUND_';
35
+
36
+/**
37
+ * The audio ID prefix of the audio element for which the {@link playAudio} action is
38
+ * triggered when a new boo reaction is received.
39
+ *
40
+ * @type {string}
41
+ */
42
+export const BOO_SOUND_ID = 'BOO_SOUND_';
43
+
44
+/**
45
+ * The audio ID prefix of the audio element for which the {@link playAudio} action is
46
+ * triggered when a new surprised reaction is received.
47
+ *
48
+ * @type {string}
49
+ */
50
+export const SURPRISE_SOUND_ID = 'SURPRISE_SOUND_';
51
+
52
+/**
53
+ * The audio ID prefix of the audio element for which the {@link playAudio} action is
54
+ * triggered when a new silence reaction is received.
55
+ *
56
+ * @type {string}
57
+ */
58
+export const SILENCE_SOUND_ID = 'SILENCE_SOUND_';
59
+
60
+/**
61
+ * The audio ID of the audio element for which the {@link playAudio} action is
62
+ * triggered when a new raise hand event is received.
63
+ *
64
+ * @type {string}
65
+ */
66
+export const RAISE_HAND_SOUND_ID = 'RAISE_HAND_SOUND_ID';
67
+
68
+export type ReactionEmojiProps = {
69
+
70
+    /**
71
+     * Reaction to be displayed.
72
+     */
73
+    reaction: string,
74
+
75
+    /**
76
+     * Id of the reaction.
77
+     */
78
+    uid: number
79
+}
80
+
81
+export const SOUNDS_THRESHOLDS = [ 1, 4, 10 ];
82
+
83
+
3 84
 export const REACTIONS = {
4 85
     like: {
5 86
         message: ':thumbs_up:',
6 87
         emoji: '👍',
7
-        shortcutChar: 'T'
88
+        shortcutChar: 'T',
89
+        soundId: LIKE_SOUND_ID,
90
+        soundFiles: LIKE_SOUND_FILES
8 91
     },
9 92
     clap: {
10 93
         message: ':clap:',
11 94
         emoji: '👏',
12
-        shortcutChar: 'C'
95
+        shortcutChar: 'C',
96
+        soundId: CLAP_SOUND_ID,
97
+        soundFiles: CLAP_SOUND_FILES
13 98
     },
14 99
     laugh: {
15 100
         message: ':grinning_face:',
16 101
         emoji: '😀',
17
-        shortcutChar: 'L'
102
+        shortcutChar: 'L',
103
+        soundId: LAUGH_SOUND_ID,
104
+        soundFiles: LAUGH_SOUND_FILES
18 105
     },
19 106
     surprised: {
20 107
         message: ':face_with_open_mouth:',
21 108
         emoji: '😮',
22
-        shortcutChar: 'O'
109
+        shortcutChar: 'O',
110
+        soundId: SURPRISE_SOUND_ID,
111
+        soundFiles: SURPRISE_SOUND_FILES
23 112
     },
24 113
     boo: {
25 114
         message: ':slightly_frowning_face:',
26 115
         emoji: '🙁',
27
-        shortcutChar: 'B'
116
+        shortcutChar: 'B',
117
+        soundId: BOO_SOUND_ID,
118
+        soundFiles: BOO_SOUND_FILES
28 119
     },
29
-    party: {
30
-        message: ':party_popper:',
31
-        emoji: '🎉',
32
-        shortcutChar: 'P'
120
+    silence: {
121
+        message: ':face_without_mouth:',
122
+        emoji: '😶',
123
+        shortcutChar: 'S',
124
+        soundId: SILENCE_SOUND_ID,
125
+        soundFiles: SILENCE_SOUND_FILES
33 126
     }
34 127
 };
35
-
36
-export type ReactionEmojiProps = {
37
-
38
-    /**
39
-     * Reaction to be displayed.
40
-     */
41
-    reaction: string,
42
-
43
-    /**
44
-     * Id of the reaction.
45
-     */
46
-    uid: number
47
-}

+ 55
- 1
react/features/reactions/functions.any.js 查看文件

@@ -5,7 +5,7 @@ import uuid from 'uuid';
5 5
 import { getLocalParticipant } from '../base/participants';
6 6
 import { extractFqnFromPath } from '../dynamic-branding/functions';
7 7
 
8
-import { REACTIONS } from './constants';
8
+import { REACTIONS, SOUNDS_THRESHOLDS } from './constants';
9 9
 import logger from './logger';
10 10
 
11 11
 /**
@@ -88,3 +88,57 @@ export async function sendReactionsWebhook(state: Object, reactions: Array<?stri
88 88
         }
89 89
     }
90 90
 }
91
+
92
+/**
93
+ * Returns unique reactions from the reactions buffer.
94
+ *
95
+ * @param {Array} reactions - The reactions buffer.
96
+ * @returns {Array}
97
+ */
98
+function getUniqueReactions(reactions: Array<string>) {
99
+    return [ ...new Set(reactions) ];
100
+}
101
+
102
+/**
103
+ * Returns frequency of given reaction in array.
104
+ *
105
+ * @param {Array} reactions - Array of reactions.
106
+ * @param {string} reaction - Reaction to get frequency for.
107
+ * @returns {number}
108
+ */
109
+function getReactionFrequency(reactions: Array<string>, reaction: string) {
110
+    return reactions.filter(r => r === reaction).length;
111
+}
112
+
113
+/**
114
+ * Returns the threshold number for a given frequency.
115
+ *
116
+ * @param {number} frequency - Frequency of reaction.
117
+ * @returns {number}
118
+ */
119
+function getSoundThresholdByFrequency(frequency) {
120
+    for (const i of SOUNDS_THRESHOLDS) {
121
+        if (frequency <= i) {
122
+            return i;
123
+        }
124
+    }
125
+
126
+    return SOUNDS_THRESHOLDS[SOUNDS_THRESHOLDS.length - 1];
127
+}
128
+
129
+/**
130
+ * Returns unique reactions with threshold.
131
+ *
132
+ * @param {Array} reactions - The reactions buffer.
133
+ * @returns {Array}
134
+ */
135
+export function getReactionsSoundsThresholds(reactions: Array<string>) {
136
+    const unique = getUniqueReactions(reactions);
137
+
138
+    return unique.map<Object>(reaction => {
139
+        return {
140
+            reaction,
141
+            threshold: getSoundThresholdByFrequency(getReactionFrequency(reactions, reaction))
142
+        };
143
+    });
144
+}

+ 70
- 4
react/features/reactions/middleware.js 查看文件

@@ -3,14 +3,19 @@
3 3
 import { batch } from 'react-redux';
4 4
 
5 5
 import { ENDPOINT_REACTION_NAME } from '../../../modules/API/constants';
6
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
6 7
 import { MiddlewareRegistry } from '../base/redux';
8
+import { updateSettings } from '../base/settings';
9
+import { playSound, registerSound, unregisterSound } from '../base/sounds';
7 10
 import { isVpaasMeeting } from '../jaas/functions';
11
+import { NOTIFICATION_TIMEOUT, showNotification } from '../notifications';
8 12
 
9 13
 import {
10 14
     ADD_REACTION_BUFFER,
11 15
     FLUSH_REACTION_BUFFER,
12 16
     SEND_REACTIONS,
13
-    PUSH_REACTIONS
17
+    PUSH_REACTIONS,
18
+    SHOW_SOUNDS_NOTIFICATION
14 19
 } from './actionTypes';
15 20
 import {
16 21
     addReactionsToChat,
@@ -19,7 +24,15 @@ import {
19 24
     sendReactions,
20 25
     setReactionQueue
21 26
 } from './actions.any';
22
-import { getReactionMessageFromBuffer, getReactionsWithId, sendReactionsWebhook } from './functions.any';
27
+import { displayReactionSoundsNotification } from './actions.web';
28
+import { RAISE_HAND_SOUND_ID, REACTIONS, SOUNDS_THRESHOLDS } from './constants';
29
+import {
30
+    getReactionMessageFromBuffer,
31
+    getReactionsSoundsThresholds,
32
+    getReactionsWithId,
33
+    sendReactionsWebhook
34
+} from './functions.any';
35
+import { RAISE_HAND_SOUND_FILE } from './sounds';
23 36
 
24 37
 
25 38
 declare var APP: Object;
@@ -35,6 +48,33 @@ MiddlewareRegistry.register(store => next => action => {
35 48
     const { dispatch, getState } = store;
36 49
 
37 50
     switch (action.type) {
51
+    case APP_WILL_MOUNT:
52
+        batch(() => {
53
+            Object.keys(REACTIONS).forEach(key => {
54
+                for (let i = 0; i < SOUNDS_THRESHOLDS.length; i++) {
55
+                    dispatch(registerSound(
56
+                        `${REACTIONS[key].soundId}${SOUNDS_THRESHOLDS[i]}`,
57
+                        REACTIONS[key].soundFiles[i]
58
+                    )
59
+                    );
60
+                }
61
+            }
62
+            );
63
+            dispatch(registerSound(RAISE_HAND_SOUND_ID, RAISE_HAND_SOUND_FILE));
64
+        });
65
+        break;
66
+
67
+    case APP_WILL_UNMOUNT:
68
+        batch(() => {
69
+            Object.keys(REACTIONS).forEach(key => {
70
+                for (let i = 0; i < SOUNDS_THRESHOLDS.length; i++) {
71
+                    dispatch(unregisterSound(`${REACTIONS[key].soundId}${SOUNDS_THRESHOLDS[i]}`));
72
+                }
73
+            });
74
+            dispatch(unregisterSound(RAISE_HAND_SOUND_ID));
75
+        });
76
+        break;
77
+
38 78
     case ADD_REACTION_BUFFER: {
39 79
         const { timeoutID, buffer } = getState()['features/reactions'];
40 80
         const { reaction } = action;
@@ -82,10 +122,36 @@ MiddlewareRegistry.register(store => next => action => {
82 122
     }
83 123
 
84 124
     case PUSH_REACTIONS: {
85
-        const queue = store.getState()['features/reactions'].queue;
125
+        const state = getState();
126
+        const { queue, notificationDisplayed } = state['features/reactions'];
127
+        const { soundsReactions } = state['features/base/settings'];
86 128
         const reactions = action.reactions;
87 129
 
88
-        dispatch(setReactionQueue([ ...queue, ...getReactionsWithId(reactions) ]));
130
+        batch(() => {
131
+            if (!notificationDisplayed && soundsReactions) {
132
+                dispatch(displayReactionSoundsNotification());
133
+            }
134
+            if (soundsReactions) {
135
+                const reactionSoundsThresholds = getReactionsSoundsThresholds(reactions);
136
+
137
+                reactionSoundsThresholds.forEach(reaction =>
138
+                    dispatch(playSound(`${REACTIONS[reaction.reaction].soundId}${reaction.threshold}`))
139
+                );
140
+            }
141
+            dispatch(setReactionQueue([ ...queue, ...getReactionsWithId(reactions) ]));
142
+        });
143
+        break;
144
+    }
145
+
146
+    case SHOW_SOUNDS_NOTIFICATION: {
147
+        dispatch(showNotification({
148
+            titleKey: 'toolbar.disableReactionSounds',
149
+            customActionNameKey: 'notify.reactionSounds',
150
+            customActionHandler: () => dispatch(updateSettings({
151
+                soundsReactions: false
152
+            }))
153
+        }, NOTIFICATION_TIMEOUT));
154
+        break;
89 155
     }
90 156
     }
91 157
 

+ 17
- 3
react/features/reactions/reducer.js 查看文件

@@ -6,7 +6,8 @@ import {
6 6
     TOGGLE_REACTIONS_VISIBLE,
7 7
     SET_REACTION_QUEUE,
8 8
     ADD_REACTION_BUFFER,
9
-    FLUSH_REACTION_BUFFER
9
+    FLUSH_REACTION_BUFFER,
10
+    SHOW_SOUNDS_NOTIFICATION
10 11
 } from './actionTypes';
11 12
 
12 13
 /**
@@ -17,7 +18,8 @@ import {
17 18
  *     visible: boolean,
18 19
  *     message: string,
19 20
  *     timeoutID: number,
20
- *     queue: Array
21
+ *     queue: Array,
22
+ *     notificationDisplayed: boolean
21 23
  * }}
22 24
  */
23 25
 function _getInitialState() {
@@ -49,7 +51,12 @@ function _getInitialState() {
49 51
          *
50 52
          * @type {Array}
51 53
          */
52
-        queue: []
54
+        queue: [],
55
+
56
+        /**
57
+         * Whether or not the disable reaction sounds notification was shown
58
+         */
59
+        notificationDisplayed: false
53 60
     };
54 61
 }
55 62
 
@@ -84,6 +91,13 @@ ReducerRegistry.register(
84 91
                 queue: action.value
85 92
             };
86 93
         }
94
+
95
+        case SHOW_SOUNDS_NOTIFICATION: {
96
+            return {
97
+                ...state,
98
+                notificationDisplayed: true
99
+            };
100
+        }
87 101
         }
88 102
 
89 103
         return state;

+ 48
- 0
react/features/reactions/sounds.js 查看文件

@@ -0,0 +1,48 @@
1
+/**
2
+ * The name of the bundled audio files which will be played for the laugh reaction sound.
3
+ *
4
+ * @type {Array<string>}
5
+ */
6
+export const LAUGH_SOUND_FILES = [ 'reactions-laughter.mp3', 'reactions-laughter.mp3', 'reactions-laughter.mp3' ];
7
+
8
+/**
9
+ * The name of the bundled audio file which will be played for the clap reaction sound.
10
+ *
11
+ * @type {Array<string>}
12
+ */
13
+export const CLAP_SOUND_FILES = [ 'reactions–applause.mp3', 'reactions–applause.mp3', 'reactions–applause.mp3' ];
14
+
15
+/**
16
+ * The name of the bundled audio file which will be played for the like reaction sound.
17
+ *
18
+ * @type {Array<string>}
19
+ */
20
+export const LIKE_SOUND_FILES = [ 'reactions–thumbs-up.mp3', 'reactions–thumbs-up.mp3', 'reactions–thumbs-up.mp3' ];
21
+
22
+/**
23
+ * The name of the bundled audio file which will be played for the boo reaction sound.
24
+ *
25
+ * @type {Array<string>}
26
+ */
27
+export const BOO_SOUND_FILES = [ 'reactions–boo.mp3', 'reactions–boo.mp3', 'reactions–boo.mp3' ];
28
+
29
+/**
30
+ * The name of the bundled audio file which will be played for the surprised reaction sound.
31
+ *
32
+ * @type {Array<string>}
33
+ */
34
+export const SURPRISE_SOUND_FILES = [ 'reactions–surprise.mp3', 'reactions–surprise.mp3', 'reactions–surprise.mp3' ];
35
+
36
+/**
37
+ * The name of the bundled audio file which will be played for the silence reaction sound.
38
+ *
39
+ * @type {Array<string>}
40
+ */
41
+export const SILENCE_SOUND_FILES = [ 'reactions–crickets.mp3', 'reactions–crickets.mp3', 'reactions–crickets.mp3' ];
42
+
43
+/**
44
+ * The name of the bundled audio file which will be played for the raise hand sound.
45
+ *
46
+ * @type {string}
47
+ */
48
+export const RAISE_HAND_SOUND_FILE = 'reactions–raised-hand.mp3';

+ 4
- 2
react/features/settings/actions.js 查看文件

@@ -142,14 +142,16 @@ export function submitSoundsTab(newState: Object): Function {
142 142
         const shouldUpdate = (newState.soundsIncomingMessage !== currentState.soundsIncomingMessage)
143 143
             || (newState.soundsParticipantJoined !== currentState.soundsParticipantJoined)
144 144
             || (newState.soundsParticipantLeft !== currentState.soundsParticipantLeft)
145
-            || (newState.soundsTalkWhileMuted !== currentState.soundsTalkWhileMuted);
145
+            || (newState.soundsTalkWhileMuted !== currentState.soundsTalkWhileMuted)
146
+            || (newState.soundsReactions !== currentState.soundsReactions);
146 147
 
147 148
         if (shouldUpdate) {
148 149
             dispatch(updateSettings({
149 150
                 soundsIncomingMessage: newState.soundsIncomingMessage,
150 151
                 soundsParticipantJoined: newState.soundsParticipantJoined,
151 152
                 soundsParticipantLeft: newState.soundsParticipantLeft,
152
-                soundsTalkWhileMuted: newState.soundsTalkWhileMuted
153
+                soundsTalkWhileMuted: newState.soundsTalkWhileMuted,
154
+                soundsReactions: newState.soundsReactions
153 155
             }));
154 156
         }
155 157
     };

+ 18
- 0
react/features/settings/components/web/SoundsTab.js 查看文件

@@ -35,6 +35,16 @@ export type Props = {
35 35
      */
36 36
     soundsTalkWhileMuted: Boolean,
37 37
 
38
+    /**
39
+    * Whether or not the sound for reactions should play.
40
+    */
41
+    soundsReactions: Boolean,
42
+
43
+    /**
44
+     * Whether or not the reactions feature is enabled.
45
+     */
46
+    enableReactions: Boolean,
47
+
38 48
     /**
39 49
      * Invoked to obtain translated strings.
40 50
      */
@@ -85,6 +95,8 @@ class SoundsTab extends AbstractDialogTab<Props> {
85 95
             soundsParticipantJoined,
86 96
             soundsParticipantLeft,
87 97
             soundsTalkWhileMuted,
98
+            soundsReactions,
99
+            enableReactions,
88 100
             t
89 101
         } = this.props;
90 102
 
@@ -95,6 +107,12 @@ class SoundsTab extends AbstractDialogTab<Props> {
95 107
                 <h2 className = 'mock-atlaskit-label'>
96 108
                     {t('settings.playSounds')}
97 109
                 </h2>
110
+                {enableReactions && <Checkbox
111
+                    isChecked = { soundsReactions }
112
+                    label = { t('settings.reactions') }
113
+                    name = 'soundsReactions'
114
+                    onChange = { this._onChange } />
115
+                }
98 116
                 <Checkbox
99 117
                     isChecked = { soundsIncomingMessage }
100 118
                     label = { t('settings.incomingMessage') }

+ 6
- 2
react/features/settings/functions.js 查看文件

@@ -171,14 +171,18 @@ export function getSoundsTabProps(stateful: Object | Function) {
171 171
         soundsIncomingMessage,
172 172
         soundsParticipantJoined,
173 173
         soundsParticipantLeft,
174
-        soundsTalkWhileMuted
174
+        soundsTalkWhileMuted,
175
+        soundsReactions
175 176
     } = state['features/base/settings'];
177
+    const { enableReactions } = state['features/base/config'];
176 178
 
177 179
     return {
178 180
         soundsIncomingMessage,
179 181
         soundsParticipantJoined,
180 182
         soundsParticipantLeft,
181
-        soundsTalkWhileMuted
183
+        soundsTalkWhileMuted,
184
+        soundsReactions,
185
+        enableReactions
182 186
     };
183 187
 }
184 188
 

+ 1
- 1
react/features/toolbox/components/AbstractToolbarButton.js 查看文件

@@ -50,7 +50,7 @@ export type Props = {
50 50
  *
51 51
  * @abstract
52 52
  */
53
-export default class AbstractToolbarButton<P: Props> extends Component<P> {
53
+export default class AbstractToolbarButton<P: Props, State=void> extends Component<P, State> {
54 54
     /**
55 55
      * Initializes a new {@code AbstractToolbarButton} instance.
56 56
      *

二進制
sounds/reactions-laughter.mp3 查看文件


二進制
sounds/reactions–applause.mp3 查看文件


二進制
sounds/reactions–boo.mp3 查看文件


二進制
sounds/reactions–crickets.mp3 查看文件


二進制
sounds/reactions–raised-hand.mp3 查看文件


二進制
sounds/reactions–surprise.mp3 查看文件


二進制
sounds/reactions–thumbs-up.mp3 查看文件


Loading…
取消
儲存