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
This commit is contained in:
robertpin 2021-08-23 12:57:56 +03:00 committed by GitHub
parent fe41eef398
commit c7a91e1974
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 435 additions and 59 deletions

View File

@ -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);
}
}
}
}

View File

@ -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",

View File

@ -31,6 +31,7 @@ const DEFAULT_STATE = {
soundsParticipantJoined: true,
soundsParticipantLeft: true,
soundsTalkWhileMuted: true,
soundsReactions: true,
startAudioOnly: false,
startWithAudioMuted: false,
startWithVideoMuted: false,

View File

@ -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';

View File

@ -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
};
}

View File

@ -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<Props> {
class ReactionButton extends AbstractToolbarButton<Props, State> {
/**
* Default values for {@code ReactionButton} component's properties.
*
@ -52,10 +68,18 @@ class ReactionButton extends AbstractToolbarButton<Props> {
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<Props> {
}
}
/**
* 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<Props> {
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<Props> {
* @inheritdoc
*/
_renderIcon() {
const { toggled, icon, label } = this.props;
const { increaseLevel } = this.state;
return (
<div className = { `toolbox-icon ${this.props.toggled ? 'toggled' : ''}` }>
<span className = 'emoji'>{this.props.icon}</span>
{this.props.label && <span className = 'text'>{this.props.label}</span>}
<div className = { `toolbox-icon ${toggled ? 'toggled' : ''}` }>
<span className = { `emoji increase-${increaseLevel > 12 ? 12 : increaseLevel}` }>{icon}</span>
{label && <span className = 'text'>{label}</span>}
</div>
);
}

View File

@ -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<Props> {
* @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<Props> {
*/
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
};
}

View File

@ -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
}
};

View File

@ -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<?stri
}
}
}
/**
* Returns unique reactions from the reactions buffer.
*
* @param {Array} reactions - The reactions buffer.
* @returns {Array}
*/
function getUniqueReactions(reactions: Array<string>) {
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<string>, 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<string>) {
const unique = getUniqueReactions(reactions);
return unique.map<Object>(reaction => {
return {
reaction,
threshold: getSoundThresholdByFrequency(getReactionFrequency(reactions, reaction))
};
});
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -0,0 +1,48 @@
/**
* The name of the bundled audio files which will be played for the laugh reaction sound.
*
* @type {Array<string>}
*/
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<string>}
*/
export const CLAP_SOUND_FILES = [ 'reactionsapplause.mp3', 'reactionsapplause.mp3', 'reactionsapplause.mp3' ];
/**
* The name of the bundled audio file which will be played for the like reaction sound.
*
* @type {Array<string>}
*/
export const LIKE_SOUND_FILES = [ 'reactionsthumbs-up.mp3', 'reactionsthumbs-up.mp3', 'reactionsthumbs-up.mp3' ];
/**
* The name of the bundled audio file which will be played for the boo reaction sound.
*
* @type {Array<string>}
*/
export const BOO_SOUND_FILES = [ 'reactionsboo.mp3', 'reactionsboo.mp3', 'reactionsboo.mp3' ];
/**
* The name of the bundled audio file which will be played for the surprised reaction sound.
*
* @type {Array<string>}
*/
export const SURPRISE_SOUND_FILES = [ 'reactionssurprise.mp3', 'reactionssurprise.mp3', 'reactionssurprise.mp3' ];
/**
* The name of the bundled audio file which will be played for the silence reaction sound.
*
* @type {Array<string>}
*/
export const SILENCE_SOUND_FILES = [ 'reactionscrickets.mp3', 'reactionscrickets.mp3', 'reactionscrickets.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 = 'reactionsraised-hand.mp3';

View File

@ -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
}));
}
};

View File

@ -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<Props> {
soundsParticipantJoined,
soundsParticipantLeft,
soundsTalkWhileMuted,
soundsReactions,
enableReactions,
t
} = this.props;
@ -95,6 +107,12 @@ class SoundsTab extends AbstractDialogTab<Props> {
<h2 className = 'mock-atlaskit-label'>
{t('settings.playSounds')}
</h2>
{enableReactions && <Checkbox
isChecked = { soundsReactions }
label = { t('settings.reactions') }
name = 'soundsReactions'
onChange = { this._onChange } />
}
<Checkbox
isChecked = { soundsIncomingMessage }
label = { t('settings.incomingMessage') }

View File

@ -171,14 +171,18 @@ export function getSoundsTabProps(stateful: Object | Function) {
soundsIncomingMessage,
soundsParticipantJoined,
soundsParticipantLeft,
soundsTalkWhileMuted
soundsTalkWhileMuted,
soundsReactions
} = state['features/base/settings'];
const { enableReactions } = state['features/base/config'];
return {
soundsIncomingMessage,
soundsParticipantJoined,
soundsParticipantLeft,
soundsTalkWhileMuted
soundsTalkWhileMuted,
soundsReactions,
enableReactions
};
}

View File

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

Binary file not shown.

Binary file not shown.

BIN
sounds/reactions–boo.mp3 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.