diff --git a/css/_reactions-menu.scss b/css/_reactions-menu.scss index 0a09a1be51..7c5ebfb184 100644 --- a/css/_reactions-menu.scss +++ b/css/_reactions-menu.scss @@ -48,6 +48,13 @@ display: flex; align-items: center; justify-content: center; + transition: font-size ease .1s; + + @for $i from 1 through 12 { + &.increase-#{$i}{ + font-size: calc(20px + #{$i}px); + } + } } } diff --git a/lang/main.json b/lang/main.json index e45b91de8c..17979e3a63 100644 --- a/lang/main.json +++ b/lang/main.json @@ -591,6 +591,7 @@ "moderationStoppedTitle": "Moderation stopped", "moderationToggleDescription": "by {{participantDisplayName}}", "raiseHandAction": "Raise hand", + "reactionSounds": "Disable sounds", "groupTitle": "Notifications" }, "participantsPane": { @@ -794,6 +795,7 @@ "participantJoined": "Participant Joined", "participantLeft": "Participant Left", "playSounds": "Play sound on", + "reactions": "Meeting reactions", "sameAsSystem": "Same as system ({{label}})", "selectAudioOutput": "Audio output", "selectCamera": "Camera", @@ -884,7 +886,6 @@ "muteEveryonesVideo": "Disable everyone's camera", "muteEveryoneElsesVideo": "Disable everyone else's camera", "participants": "Participants", - "party": "Party Popper", "pip": "Toggle Picture-in-Picture mode", "privateMessage": "Send private message", "profile": "Edit your profile", @@ -901,6 +902,7 @@ "shareYourScreen": "Start / Stop sharing your screen", "shortcuts": "Toggle shortcuts", "show": "Show on stage", + "silence": "Silence", "speakerStats": "Toggle speaker statistics", "surprised": "Surprised", "tileView": "Toggle tile view", @@ -925,6 +927,7 @@ "clap": "Clap", "closeChat": "Close chat", "closeReactionsMenu": "Close reactions menu", + "disableReactionSounds": "You can disable reaction sounds for this meeting", "documentClose": "Close shared document", "documentOpen": "Open shared document", "download": "Download our apps", @@ -960,7 +963,6 @@ "openChat": "Open chat", "openReactionsMenu": "Open reactions menu", "participants": "Participants", - "party": "Celebration", "pip": "Enter Picture-in-Picture mode", "privateMessage": "Send private message", "profile": "Edit your profile", @@ -970,7 +972,7 @@ "reactionClap": "Send clap reaction", "reactionLaugh": "Send laugh reaction", "reactionLike": "Send thumbs up reaction", - "reactionParty": "Send party popper reaction", + "reactionSilence": "Send silence reaction", "reactionSurprised": "Send surprised reaction", "security": "Security options", "Settings": "Settings", @@ -978,6 +980,7 @@ "sharedvideo": "Share video", "shareRoom": "Invite someone", "shortcuts": "View shortcuts", + "silence": "Silence", "speakerStats": "Speaker stats", "startScreenSharing": "Start screen sharing", "startSubtitles": "Start subtitles", diff --git a/react/features/base/settings/reducer.js b/react/features/base/settings/reducer.js index c218697959..f5408f0d13 100644 --- a/react/features/base/settings/reducer.js +++ b/react/features/base/settings/reducer.js @@ -31,6 +31,7 @@ const DEFAULT_STATE = { soundsParticipantJoined: true, soundsParticipantLeft: true, soundsTalkWhileMuted: true, + soundsReactions: true, startAudioOnly: false, startWithAudioMuted: false, startWithVideoMuted: false, diff --git a/react/features/reactions/actionTypes.js b/react/features/reactions/actionTypes.js index fb17b232e5..a3db61735d 100644 --- a/react/features/reactions/actionTypes.js +++ b/react/features/reactions/actionTypes.js @@ -58,3 +58,8 @@ export const SEND_REACTIONS = 'SEND_REACTIONS'; * The type of action to adds reactions to the queue. */ export const PUSH_REACTIONS = 'PUSH_REACTIONS'; + +/** + * The type of action to display disable notification sounds. + */ +export const SHOW_SOUNDS_NOTIFICATION = 'SHOW_SOUNDS_NOTIFICATION'; diff --git a/react/features/reactions/actions.web.js b/react/features/reactions/actions.web.js index ffbf5178f6..09b8d95db3 100644 --- a/react/features/reactions/actions.web.js +++ b/react/features/reactions/actions.web.js @@ -1,16 +1,28 @@ // @flow import { + SHOW_SOUNDS_NOTIFICATION, TOGGLE_REACTIONS_VISIBLE } from './actionTypes'; /** * Toggles the visibility of the reactions menu. * - * @returns {Function} + * @returns {Object} */ export function toggleReactionsMenuVisibility() { return { type: TOGGLE_REACTIONS_VISIBLE }; } + +/** + * Displays the disable sounds notification. + * + * @returns {Object} + */ +export function displayReactionSoundsNotification() { + return { + type: SHOW_SOUNDS_NOTIFICATION + }; +} diff --git a/react/features/reactions/components/web/ReactionButton.js b/react/features/reactions/components/web/ReactionButton.js index 3a7ed05294..72d03bad11 100644 --- a/react/features/reactions/components/web/ReactionButton.js +++ b/react/features/reactions/components/web/ReactionButton.js @@ -28,12 +28,28 @@ type Props = AbstractToolbarButtonProps & { label?: string }; +/** + * The type of the React {@code Component} state of {@link ReactionButton}. + */ +type State = { + + /** + * Used to determine zoom level on reaction burst. + */ + increaseLevel: number, + + /** + * Timeout ID to reset reaction burst. + */ + increaseTimeout: TimeoutID | null +} + /** * Represents a button in the reactions menu. * * @extends AbstractToolbarButton */ -class ReactionButton extends AbstractToolbarButton { +class ReactionButton extends AbstractToolbarButton { /** * Default values for {@code ReactionButton} component's properties. * @@ -52,10 +68,18 @@ class ReactionButton extends AbstractToolbarButton { super(props); this._onKeyDown = this._onKeyDown.bind(this); + this._onClickHandler = this._onClickHandler.bind(this); + + this.state = { + increaseLevel: 0, + increaseTimeout: null + }; } _onKeyDown: (Object) => void; + _onClickHandler: () => void; + /** * Handles 'Enter' key on the button to trigger onClick for accessibility. * We should be handling Space onKeyUp but it conflicts with PTT. @@ -78,6 +102,28 @@ class ReactionButton extends AbstractToolbarButton { } } + /** + * Handles reaction button click. + * + * @returns {void} + */ + _onClickHandler() { + this.props.onClick(); + clearTimeout(this.state.increaseTimeout); + const timeout = setTimeout(() => { + this.setState({ + increaseLevel: 0 + }); + }, 500); + + this.setState(state => { + return { + increaseLevel: state.increaseLevel + 1, + increaseTimeout: timeout + }; + }); + } + /** * Renders the button of this {@code ReactionButton}. * @@ -92,7 +138,7 @@ class ReactionButton extends AbstractToolbarButton { aria-label = { this.props.accessibilityLabel } aria-pressed = { this.props.toggled } className = 'toolbox-button' - onClick = { this.props.onClick } + onClick = { this._onClickHandler } onKeyDown = { this._onKeyDown } role = 'button' tabIndex = { 0 }> @@ -113,10 +159,13 @@ class ReactionButton extends AbstractToolbarButton { * @inheritdoc */ _renderIcon() { + const { toggled, icon, label } = this.props; + const { increaseLevel } = this.state; + return ( -
- {this.props.icon} - {this.props.label && {this.props.label}} +
+ 12 ? 12 : increaseLevel}` }>{icon} + {label && {label}}
); } diff --git a/react/features/reactions/components/web/ReactionsMenu.js b/react/features/reactions/components/web/ReactionsMenu.js index 23f67a2b10..3aee8be94e 100644 --- a/react/features/reactions/components/web/ReactionsMenu.js +++ b/react/features/reactions/components/web/ReactionsMenu.js @@ -11,10 +11,11 @@ import { import { translate } from '../../../base/i18n'; import { getLocalParticipant, getParticipantCount, participantUpdated } from '../../../base/participants'; import { connect } from '../../../base/redux'; +import { playSound } from '../../../base/sounds'; import { dockToolbox } from '../../../toolbox/actions.web'; import { addReactionToBuffer } from '../../actions.any'; import { toggleReactionsMenuVisibility } from '../../actions.web'; -import { REACTIONS } from '../../constants'; +import { RAISE_HAND_SOUND_ID, REACTIONS } from '../../constants'; import ReactionButton from './ReactionButton'; @@ -53,7 +54,12 @@ type Props = { /** * Whether or not it's displayed in the overflow menu. */ - overflowMenu: boolean + overflowMenu: boolean, + + /** + * Whether or not reaction sounds are enabled. + */ + _reactionSounds: boolean }; declare var APP: Object; @@ -106,11 +112,16 @@ class ReactionsMenu extends Component { * @returns {void} */ _onToolbarToggleRaiseHand() { + const { dispatch, _raisedHand, _reactionSounds } = this.props; + sendAnalytics(createToolbarEvent( 'raise.hand', - { enable: !this.props._raisedHand })); + { enable: !_raisedHand })); this._doToggleRaiseHand(); - this.props.dispatch(toggleReactionsMenuVisibility()); + dispatch(toggleReactionsMenuVisibility()); + if (_reactionSounds && _raisedHand) { + dispatch(playSound(RAISE_HAND_SOUND_ID)); + } } /** @@ -212,11 +223,13 @@ class ReactionsMenu extends Component { */ function mapStateToProps(state) { const localParticipant = getLocalParticipant(state); + const { soundsReactions } = state['features/base/settings']; return { _localParticipantID: localParticipant.id, _raisedHand: localParticipant.raisedHand, - _participantCount: getParticipantCount(state) + _participantCount: getParticipantCount(state), + _reactionSounds: soundsReactions }; } diff --git a/react/features/reactions/constants.js b/react/features/reactions/constants.js index 91b3220f47..f5d7156c03 100644 --- a/react/features/reactions/constants.js +++ b/react/features/reactions/constants.js @@ -1,37 +1,69 @@ // @flow -export const REACTIONS = { - like: { - message: ':thumbs_up:', - emoji: '👍', - shortcutChar: 'T' - }, - clap: { - message: ':clap:', - emoji: '👏', - shortcutChar: 'C' - }, - laugh: { - message: ':grinning_face:', - emoji: '😀', - shortcutChar: 'L' - }, - surprised: { - message: ':face_with_open_mouth:', - emoji: '😼', - shortcutChar: 'O' - }, - boo: { - message: ':slightly_frowning_face:', - emoji: '🙁', - shortcutChar: 'B' - }, - party: { - message: ':party_popper:', - emoji: '🎉', - shortcutChar: 'P' - } -}; +import { + CLAP_SOUND_FILES, + LAUGH_SOUND_FILES, + LIKE_SOUND_FILES, + BOO_SOUND_FILES, + SURPRISE_SOUND_FILES, + SILENCE_SOUND_FILES +} from './sounds'; + +/** + * The audio ID prefix of the audio element for which the {@link playAudio} action is + * triggered when a new laugh reaction is received. + * + * @type { string } + */ +export const LAUGH_SOUND_ID = 'LAUGH_SOUND_'; + +/** + * The audio ID prefix of the audio element for which the {@link playAudio} action is + * triggered when a new clap reaction is received. + * + * @type {string} + */ +export const CLAP_SOUND_ID = 'CLAP_SOUND_'; + +/** + * The audio ID prefix of the audio element for which the {@link playAudio} action is + * triggered when a new like reaction is received. + * + * @type {string} + */ +export const LIKE_SOUND_ID = 'LIKE_SOUND_'; + +/** + * The audio ID prefix of the audio element for which the {@link playAudio} action is + * triggered when a new boo reaction is received. + * + * @type {string} + */ +export const BOO_SOUND_ID = 'BOO_SOUND_'; + +/** + * The audio ID prefix of the audio element for which the {@link playAudio} action is + * triggered when a new surprised reaction is received. + * + * @type {string} + */ +export const SURPRISE_SOUND_ID = 'SURPRISE_SOUND_'; + +/** + * The audio ID prefix of the audio element for which the {@link playAudio} action is + * triggered when a new silence reaction is received. + * + * @type {string} + */ +export const SILENCE_SOUND_ID = 'SILENCE_SOUND_'; + +/** + * The audio ID of the audio element for which the {@link playAudio} action is + * triggered when a new raise hand event is received. + * + * @type {string} + */ +export const RAISE_HAND_SOUND_ID = 'RAISE_HAND_SOUND_ID'; export type ReactionEmojiProps = { @@ -45,3 +77,51 @@ export type ReactionEmojiProps = { */ uid: number } + +export const SOUNDS_THRESHOLDS = [ 1, 4, 10 ]; + + +export const REACTIONS = { + like: { + message: ':thumbs_up:', + emoji: '👍', + shortcutChar: 'T', + soundId: LIKE_SOUND_ID, + soundFiles: LIKE_SOUND_FILES + }, + clap: { + message: ':clap:', + emoji: '👏', + shortcutChar: 'C', + soundId: CLAP_SOUND_ID, + soundFiles: CLAP_SOUND_FILES + }, + laugh: { + message: ':grinning_face:', + emoji: '😀', + shortcutChar: 'L', + soundId: LAUGH_SOUND_ID, + soundFiles: LAUGH_SOUND_FILES + }, + surprised: { + message: ':face_with_open_mouth:', + emoji: '😼', + shortcutChar: 'O', + soundId: SURPRISE_SOUND_ID, + soundFiles: SURPRISE_SOUND_FILES + }, + boo: { + message: ':slightly_frowning_face:', + emoji: '🙁', + shortcutChar: 'B', + soundId: BOO_SOUND_ID, + soundFiles: BOO_SOUND_FILES + }, + silence: { + message: ':face_without_mouth:', + emoji: 'đŸ˜¶', + shortcutChar: 'S', + soundId: SILENCE_SOUND_ID, + soundFiles: SILENCE_SOUND_FILES + } +}; diff --git a/react/features/reactions/functions.any.js b/react/features/reactions/functions.any.js index 7fb0bcb6a1..991a32d660 100644 --- a/react/features/reactions/functions.any.js +++ b/react/features/reactions/functions.any.js @@ -5,7 +5,7 @@ import uuid from 'uuid'; import { getLocalParticipant } from '../base/participants'; import { extractFqnFromPath } from '../dynamic-branding/functions'; -import { REACTIONS } from './constants'; +import { REACTIONS, SOUNDS_THRESHOLDS } from './constants'; import logger from './logger'; /** @@ -88,3 +88,57 @@ export async function sendReactionsWebhook(state: Object, reactions: Array) { + return [ ...new Set(reactions) ]; +} + +/** + * Returns frequency of given reaction in array. + * + * @param {Array} reactions - Array of reactions. + * @param {string} reaction - Reaction to get frequency for. + * @returns {number} + */ +function getReactionFrequency(reactions: Array, reaction: string) { + return reactions.filter(r => r === reaction).length; +} + +/** + * Returns the threshold number for a given frequency. + * + * @param {number} frequency - Frequency of reaction. + * @returns {number} + */ +function getSoundThresholdByFrequency(frequency) { + for (const i of SOUNDS_THRESHOLDS) { + if (frequency <= i) { + return i; + } + } + + return SOUNDS_THRESHOLDS[SOUNDS_THRESHOLDS.length - 1]; +} + +/** + * Returns unique reactions with threshold. + * + * @param {Array} reactions - The reactions buffer. + * @returns {Array} + */ +export function getReactionsSoundsThresholds(reactions: Array) { + const unique = getUniqueReactions(reactions); + + return unique.map(reaction => { + return { + reaction, + threshold: getSoundThresholdByFrequency(getReactionFrequency(reactions, reaction)) + }; + }); +} diff --git a/react/features/reactions/middleware.js b/react/features/reactions/middleware.js index ed3d94e18a..7500eb3de7 100644 --- a/react/features/reactions/middleware.js +++ b/react/features/reactions/middleware.js @@ -3,14 +3,19 @@ import { batch } from 'react-redux'; import { ENDPOINT_REACTION_NAME } from '../../../modules/API/constants'; +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app'; import { MiddlewareRegistry } from '../base/redux'; +import { updateSettings } from '../base/settings'; +import { playSound, registerSound, unregisterSound } from '../base/sounds'; import { isVpaasMeeting } from '../jaas/functions'; +import { NOTIFICATION_TIMEOUT, showNotification } from '../notifications'; import { ADD_REACTION_BUFFER, FLUSH_REACTION_BUFFER, SEND_REACTIONS, - PUSH_REACTIONS + PUSH_REACTIONS, + SHOW_SOUNDS_NOTIFICATION } from './actionTypes'; import { addReactionsToChat, @@ -19,7 +24,15 @@ import { sendReactions, setReactionQueue } from './actions.any'; -import { getReactionMessageFromBuffer, getReactionsWithId, sendReactionsWebhook } from './functions.any'; +import { displayReactionSoundsNotification } from './actions.web'; +import { RAISE_HAND_SOUND_ID, REACTIONS, SOUNDS_THRESHOLDS } from './constants'; +import { + getReactionMessageFromBuffer, + getReactionsSoundsThresholds, + getReactionsWithId, + sendReactionsWebhook +} from './functions.any'; +import { RAISE_HAND_SOUND_FILE } from './sounds'; declare var APP: Object; @@ -35,6 +48,33 @@ MiddlewareRegistry.register(store => next => action => { const { dispatch, getState } = store; switch (action.type) { + case APP_WILL_MOUNT: + batch(() => { + Object.keys(REACTIONS).forEach(key => { + for (let i = 0; i < SOUNDS_THRESHOLDS.length; i++) { + dispatch(registerSound( + `${REACTIONS[key].soundId}${SOUNDS_THRESHOLDS[i]}`, + REACTIONS[key].soundFiles[i] + ) + ); + } + } + ); + dispatch(registerSound(RAISE_HAND_SOUND_ID, RAISE_HAND_SOUND_FILE)); + }); + break; + + case APP_WILL_UNMOUNT: + batch(() => { + Object.keys(REACTIONS).forEach(key => { + for (let i = 0; i < SOUNDS_THRESHOLDS.length; i++) { + dispatch(unregisterSound(`${REACTIONS[key].soundId}${SOUNDS_THRESHOLDS[i]}`)); + } + }); + dispatch(unregisterSound(RAISE_HAND_SOUND_ID)); + }); + break; + case ADD_REACTION_BUFFER: { const { timeoutID, buffer } = getState()['features/reactions']; const { reaction } = action; @@ -82,10 +122,36 @@ MiddlewareRegistry.register(store => next => action => { } case PUSH_REACTIONS: { - const queue = store.getState()['features/reactions'].queue; + const state = getState(); + const { queue, notificationDisplayed } = state['features/reactions']; + const { soundsReactions } = state['features/base/settings']; const reactions = action.reactions; - dispatch(setReactionQueue([ ...queue, ...getReactionsWithId(reactions) ])); + batch(() => { + if (!notificationDisplayed && soundsReactions) { + dispatch(displayReactionSoundsNotification()); + } + if (soundsReactions) { + const reactionSoundsThresholds = getReactionsSoundsThresholds(reactions); + + reactionSoundsThresholds.forEach(reaction => + dispatch(playSound(`${REACTIONS[reaction.reaction].soundId}${reaction.threshold}`)) + ); + } + dispatch(setReactionQueue([ ...queue, ...getReactionsWithId(reactions) ])); + }); + break; + } + + case SHOW_SOUNDS_NOTIFICATION: { + dispatch(showNotification({ + titleKey: 'toolbar.disableReactionSounds', + customActionNameKey: 'notify.reactionSounds', + customActionHandler: () => dispatch(updateSettings({ + soundsReactions: false + })) + }, NOTIFICATION_TIMEOUT)); + break; } } diff --git a/react/features/reactions/reducer.js b/react/features/reactions/reducer.js index dcb8bcf130..851c1ed39c 100644 --- a/react/features/reactions/reducer.js +++ b/react/features/reactions/reducer.js @@ -6,7 +6,8 @@ import { TOGGLE_REACTIONS_VISIBLE, SET_REACTION_QUEUE, ADD_REACTION_BUFFER, - FLUSH_REACTION_BUFFER + FLUSH_REACTION_BUFFER, + SHOW_SOUNDS_NOTIFICATION } from './actionTypes'; /** @@ -17,7 +18,8 @@ import { * visible: boolean, * message: string, * timeoutID: number, - * queue: Array + * queue: Array, + * notificationDisplayed: boolean * }} */ function _getInitialState() { @@ -49,7 +51,12 @@ function _getInitialState() { * * @type {Array} */ - queue: [] + queue: [], + + /** + * Whether or not the disable reaction sounds notification was shown + */ + notificationDisplayed: false }; } @@ -84,6 +91,13 @@ ReducerRegistry.register( queue: action.value }; } + + case SHOW_SOUNDS_NOTIFICATION: { + return { + ...state, + notificationDisplayed: true + }; + } } return state; diff --git a/react/features/reactions/sounds.js b/react/features/reactions/sounds.js new file mode 100644 index 0000000000..1338486016 --- /dev/null +++ b/react/features/reactions/sounds.js @@ -0,0 +1,48 @@ +/** + * The name of the bundled audio files which will be played for the laugh reaction sound. + * + * @type {Array} + */ +export const LAUGH_SOUND_FILES = [ 'reactions-laughter.mp3', 'reactions-laughter.mp3', 'reactions-laughter.mp3' ]; + +/** + * The name of the bundled audio file which will be played for the clap reaction sound. + * + * @type {Array} + */ +export const CLAP_SOUND_FILES = [ 'reactions–applause.mp3', 'reactions–applause.mp3', 'reactions–applause.mp3' ]; + +/** + * The name of the bundled audio file which will be played for the like reaction sound. + * + * @type {Array} + */ +export const LIKE_SOUND_FILES = [ 'reactions–thumbs-up.mp3', 'reactions–thumbs-up.mp3', 'reactions–thumbs-up.mp3' ]; + +/** + * The name of the bundled audio file which will be played for the boo reaction sound. + * + * @type {Array} + */ +export const BOO_SOUND_FILES = [ 'reactions–boo.mp3', 'reactions–boo.mp3', 'reactions–boo.mp3' ]; + +/** + * The name of the bundled audio file which will be played for the surprised reaction sound. + * + * @type {Array} + */ +export const SURPRISE_SOUND_FILES = [ 'reactions–surprise.mp3', 'reactions–surprise.mp3', 'reactions–surprise.mp3' ]; + +/** + * The name of the bundled audio file which will be played for the silence reaction sound. + * + * @type {Array} + */ +export const SILENCE_SOUND_FILES = [ 'reactions–crickets.mp3', 'reactions–crickets.mp3', 'reactions–crickets.mp3' ]; + +/** + * The name of the bundled audio file which will be played for the raise hand sound. + * + * @type {string} + */ +export const RAISE_HAND_SOUND_FILE = 'reactions–raised-hand.mp3'; diff --git a/react/features/settings/actions.js b/react/features/settings/actions.js index cf0c9e3d84..37030eaa86 100644 --- a/react/features/settings/actions.js +++ b/react/features/settings/actions.js @@ -142,14 +142,16 @@ export function submitSoundsTab(newState: Object): Function { const shouldUpdate = (newState.soundsIncomingMessage !== currentState.soundsIncomingMessage) || (newState.soundsParticipantJoined !== currentState.soundsParticipantJoined) || (newState.soundsParticipantLeft !== currentState.soundsParticipantLeft) - || (newState.soundsTalkWhileMuted !== currentState.soundsTalkWhileMuted); + || (newState.soundsTalkWhileMuted !== currentState.soundsTalkWhileMuted) + || (newState.soundsReactions !== currentState.soundsReactions); if (shouldUpdate) { dispatch(updateSettings({ soundsIncomingMessage: newState.soundsIncomingMessage, soundsParticipantJoined: newState.soundsParticipantJoined, soundsParticipantLeft: newState.soundsParticipantLeft, - soundsTalkWhileMuted: newState.soundsTalkWhileMuted + soundsTalkWhileMuted: newState.soundsTalkWhileMuted, + soundsReactions: newState.soundsReactions })); } }; diff --git a/react/features/settings/components/web/SoundsTab.js b/react/features/settings/components/web/SoundsTab.js index 20cb7fb5aa..5b1bfe4786 100644 --- a/react/features/settings/components/web/SoundsTab.js +++ b/react/features/settings/components/web/SoundsTab.js @@ -35,6 +35,16 @@ export type Props = { */ soundsTalkWhileMuted: Boolean, + /** + * Whether or not the sound for reactions should play. + */ + soundsReactions: Boolean, + + /** + * Whether or not the reactions feature is enabled. + */ + enableReactions: Boolean, + /** * Invoked to obtain translated strings. */ @@ -85,6 +95,8 @@ class SoundsTab extends AbstractDialogTab { soundsParticipantJoined, soundsParticipantLeft, soundsTalkWhileMuted, + soundsReactions, + enableReactions, t } = this.props; @@ -95,6 +107,12 @@ class SoundsTab extends AbstractDialogTab {

{t('settings.playSounds')}

+ {enableReactions && + } extends Component

{ +export default class AbstractToolbarButton extends Component { /** * Initializes a new {@code AbstractToolbarButton} instance. * diff --git a/sounds/reactions-laughter.mp3 b/sounds/reactions-laughter.mp3 new file mode 100644 index 0000000000..5638b53684 Binary files /dev/null and b/sounds/reactions-laughter.mp3 differ diff --git a/sounds/reactions–applause.mp3 b/sounds/reactions–applause.mp3 new file mode 100644 index 0000000000..926caa1b25 Binary files /dev/null and b/sounds/reactions–applause.mp3 differ diff --git a/sounds/reactions–boo.mp3 b/sounds/reactions–boo.mp3 new file mode 100644 index 0000000000..c57e687dac Binary files /dev/null and b/sounds/reactions–boo.mp3 differ diff --git a/sounds/reactions–crickets.mp3 b/sounds/reactions–crickets.mp3 new file mode 100644 index 0000000000..1b7e6ebed0 Binary files /dev/null and b/sounds/reactions–crickets.mp3 differ diff --git a/sounds/reactions–raised-hand.mp3 b/sounds/reactions–raised-hand.mp3 new file mode 100644 index 0000000000..74504b1ec7 Binary files /dev/null and b/sounds/reactions–raised-hand.mp3 differ diff --git a/sounds/reactions–surprise.mp3 b/sounds/reactions–surprise.mp3 new file mode 100644 index 0000000000..ee2d8ccd77 Binary files /dev/null and b/sounds/reactions–surprise.mp3 differ diff --git a/sounds/reactions–thumbs-up.mp3 b/sounds/reactions–thumbs-up.mp3 new file mode 100644 index 0000000000..0d49c55d94 Binary files /dev/null and b/sounds/reactions–thumbs-up.mp3 differ