feat(reactions,chat): expand emoji features across reactions, chat picker, and message reactions

Add 8 new floating reactions (party, thinking, wave, cry, eyes, mindblown, pray, rocket) in a
two-row grid layout without sounds or shortcuts. Replace the basic SmileysPanel in chat with a
full @emoji-mart/react picker featuring categories, search, and skin tones. Expand message
reaction quick-access from 5 to 8 emojis with a "+" button to open the full picker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-09 11:22:48 -07:00
parent ef6a5a1ffe
commit 325960fa15
14 changed files with 261 additions and 61 deletions

View File

@ -1,7 +1,7 @@
@use 'sass:math'; @use 'sass:math';
.reactions-menu { .reactions-menu {
width: 330px; width: 360px;
background: #242528; background: #242528;
box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25); box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25);
border-radius: 6px; border-radius: 6px;
@ -70,15 +70,15 @@
} }
.reactions-row { .reactions-row {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 4px;
.toolbox-button { .toolbox-button {
margin-right: 8px;
touch-action: manipulation; touch-action: manipulation;
position: relative; position: relative;
} }
.toolbox-button:last-of-type {
margin-right: 0;
}
} }
.raise-hand-row { .raise-hand-row {

View File

@ -1314,6 +1314,7 @@
"audioRoute": "Select the sound device", "audioRoute": "Select the sound device",
"boo": "Boo", "boo": "Boo",
"breakoutRooms": "Breakout rooms", "breakoutRooms": "Breakout rooms",
"cry": "Cry",
"callQuality": "Manage video quality", "callQuality": "Manage video quality",
"carmode": "Car Mode", "carmode": "Car Mode",
"cc": "Toggle subtitles", "cc": "Toggle subtitles",
@ -1332,6 +1333,7 @@
"endConference": "End meeting for all", "endConference": "End meeting for all",
"enterFullScreen": "View full screen", "enterFullScreen": "View full screen",
"enterTileView": "Enter tile view", "enterTileView": "Enter tile view",
"eyes": "Eyes",
"exitFullScreen": "Exit full screen", "exitFullScreen": "Exit full screen",
"exitTileView": "Exit tile view", "exitTileView": "Exit tile view",
"expand": "Expand", "expand": "Expand",
@ -1349,6 +1351,7 @@
"leaveConference": "Leave meeting", "leaveConference": "Leave meeting",
"like": "Thumbs Up", "like": "Thumbs Up",
"linkToSalesforce": "Link to Salesforce", "linkToSalesforce": "Link to Salesforce",
"mindblown": "Mind Blown",
"lobbyButton": "Enable/disable lobby mode", "lobbyButton": "Enable/disable lobby mode",
"localRecording": "Toggle local recording controls", "localRecording": "Toggle local recording controls",
"lockRoom": "Toggle meeting password", "lockRoom": "Toggle meeting password",
@ -1365,6 +1368,8 @@
"muteGUMPending": "Connecting your microphone", "muteGUMPending": "Connecting your microphone",
"noiseSuppression": "Extra noise suppression", "noiseSuppression": "Extra noise suppression",
"openChat": "Open chat", "openChat": "Open chat",
"party": "Party",
"pray": "Pray",
"participants": "Open participants panel. {{participantsCount}} participants", "participants": "Open participants panel. {{participantsCount}} participants",
"pip": "Toggle Picture-in-Picture mode", "pip": "Toggle Picture-in-Picture mode",
"privateMessage": "Send private message", "privateMessage": "Send private message",
@ -1376,6 +1381,7 @@
"recording": "Toggle recording", "recording": "Toggle recording",
"remoteMute": "Mute participant", "remoteMute": "Mute participant",
"remoteVideoMute": "Disable camera of participant", "remoteVideoMute": "Disable camera of participant",
"rocket": "Rocket",
"security": "Security options", "security": "Security options",
"selectBackground": "Select Background", "selectBackground": "Select Background",
"selfView": "Toggle self view", "selfView": "Toggle self view",
@ -1396,13 +1402,15 @@
"stopSharedVideo": "Stop video", "stopSharedVideo": "Stop video",
"surprised": "Surprised", "surprised": "Surprised",
"tileView": "Toggle tile view", "tileView": "Toggle tile view",
"thinking": "Thinking",
"toggleCamera": "Toggle camera", "toggleCamera": "Toggle camera",
"toggleFilmstrip": "Toggle filmstrip", "toggleFilmstrip": "Toggle filmstrip",
"unmute": "Unmute microphone", "unmute": "Unmute microphone",
"videoblur": "Toggle video blur", "videoblur": "Toggle video blur",
"videomute": "Stop camera", "videomute": "Stop camera",
"videomuteGUMPending": "Connecting your camera", "videomuteGUMPending": "Connecting your camera",
"videounmute": "Start camera" "videounmute": "Start camera",
"wave": "Wave"
}, },
"addPeople": "Add people to your call", "addPeople": "Add people to your call",
"advancedAudioSettings": { "advancedAudioSettings": {
@ -1426,6 +1434,7 @@
"authenticate": "Authenticate", "authenticate": "Authenticate",
"boo": "Boo", "boo": "Boo",
"callQuality": "Manage video quality", "callQuality": "Manage video quality",
"cry": "Cry",
"chat": "Open / Close chat", "chat": "Open / Close chat",
"clap": "Clap", "clap": "Clap",
"closeChat": "Close chat", "closeChat": "Close chat",
@ -1445,6 +1454,7 @@
"enterTileView": "Enter tile view", "enterTileView": "Enter tile view",
"exitFullScreen": "Exit full screen", "exitFullScreen": "Exit full screen",
"exitTileView": "Exit tile view", "exitTileView": "Exit tile view",
"eyes": "Eyes",
"feedback": "Leave feedback", "feedback": "Leave feedback",
"fileSharing": "File sharing", "fileSharing": "File sharing",
"giphy": "Toggle GIPHY menu", "giphy": "Toggle GIPHY menu",
@ -1464,6 +1474,7 @@
"logout": "Log Out", "logout": "Log Out",
"love": "Heart", "love": "Heart",
"lowerYourHand": "Lower your hand", "lowerYourHand": "Lower your hand",
"mindblown": "Mind Blown",
"moreActions": "More actions", "moreActions": "More actions",
"moreOptions": "More options", "moreOptions": "More options",
"mute": "Mute microphone", "mute": "Mute microphone",
@ -1480,9 +1491,11 @@
"noisyAudioInputTitle": "Your microphone appears to be noisy!", "noisyAudioInputTitle": "Your microphone appears to be noisy!",
"openChat": "Open chat", "openChat": "Open chat",
"openReactionsMenu": "Open reactions menu", "openReactionsMenu": "Open reactions menu",
"party": "Party",
"participants": "Participants", "participants": "Participants",
"pip": "Enter Picture-in-Picture mode", "pip": "Enter Picture-in-Picture mode",
"polls": "Polls", "polls": "Polls",
"pray": "Pray",
"privateMessage": "Send private message", "privateMessage": "Send private message",
"profile": "Edit your profile", "profile": "Edit your profile",
"raiseHand": "Raise your hand", "raiseHand": "Raise your hand",
@ -1496,6 +1509,7 @@
"reactionSilence": "Send silence reaction", "reactionSilence": "Send silence reaction",
"reactionSurprised": "Send surprised reaction", "reactionSurprised": "Send surprised reaction",
"reactions": "Reactions", "reactions": "Reactions",
"rocket": "Rocket",
"security": "Security options", "security": "Security options",
"selectBackground": "Select background", "selectBackground": "Select background",
"shareRoom": "Invite someone", "shareRoom": "Invite someone",
@ -1518,13 +1532,15 @@
"stopSubtitles": "Stop subtitles", "stopSubtitles": "Stop subtitles",
"surprised": "Surprised", "surprised": "Surprised",
"talkWhileMutedPopup": "Trying to speak? You are muted.", "talkWhileMutedPopup": "Trying to speak? You are muted.",
"thinking": "Thinking",
"tileViewToggle": "Toggle tile view", "tileViewToggle": "Toggle tile view",
"toggleCamera": "Toggle camera", "toggleCamera": "Toggle camera",
"unmute": "Unmute microphone", "unmute": "Unmute microphone",
"videoSettings": "Video settings", "videoSettings": "Video settings",
"videomute": "Stop camera", "videomute": "Stop camera",
"videomuteGUMPending": "Connecting your camera", "videomuteGUMPending": "Connecting your camera",
"videounmute": "Start camera" "videounmute": "Start camera",
"wave": "Wave"
}, },
"transcribing": { "transcribing": {
"ccButtonTooltip": "Start / Stop subtitles", "ccButtonTooltip": "Start / Stop subtitles",

28
package-lock.json generated
View File

@ -13,6 +13,8 @@
"@amplitude/analytics-browser": "2.17.12", "@amplitude/analytics-browser": "2.17.12",
"@amplitude/analytics-react-native": "1.4.13", "@amplitude/analytics-react-native": "1.4.13",
"@braintree/sanitize-url": "7.0.0", "@braintree/sanitize-url": "7.0.0",
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
"@emotion/react": "11.10.6", "@emotion/react": "11.10.6",
"@emotion/styled": "11.10.6", "@emotion/styled": "11.10.6",
"@giphy/js-fetch-api": "4.9.3", "@giphy/js-fetch-api": "4.9.3",
@ -2678,6 +2680,22 @@
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/@emoji-mart/data": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz",
"integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==",
"license": "MIT"
},
"node_modules/@emoji-mart/react": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@emoji-mart/react/-/react-1.1.1.tgz",
"integrity": "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==",
"license": "MIT",
"peerDependencies": {
"emoji-mart": "^5.2",
"react": "^16.8 || ^17 || ^18"
}
},
"node_modules/@emotion/babel-plugin": { "node_modules/@emotion/babel-plugin": {
"version": "11.10.6", "version": "11.10.6",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz",
@ -28840,6 +28858,16 @@
"@types/hammerjs": "^2.0.36" "@types/hammerjs": "^2.0.36"
} }
}, },
"@emoji-mart/data": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz",
"integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw=="
},
"@emoji-mart/react": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@emoji-mart/react/-/react-1.1.1.tgz",
"integrity": "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g=="
},
"@emotion/babel-plugin": { "@emotion/babel-plugin": {
"version": "11.10.6", "version": "11.10.6",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz",

View File

@ -19,6 +19,8 @@
"@amplitude/analytics-browser": "2.17.12", "@amplitude/analytics-browser": "2.17.12",
"@amplitude/analytics-react-native": "1.4.13", "@amplitude/analytics-react-native": "1.4.13",
"@braintree/sanitize-url": "7.0.0", "@braintree/sanitize-url": "7.0.0",
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
"@emotion/react": "11.10.6", "@emotion/react": "11.10.6",
"@emotion/styled": "11.10.6", "@emotion/styled": "11.10.6",
"@giphy/js-fetch-api": "4.9.3", "@giphy/js-fetch-api": "4.9.3",

View File

@ -13,7 +13,7 @@ import Input from '../../../base/ui/components/web/Input';
import { CHAT_SIZE } from '../../constants'; import { CHAT_SIZE } from '../../constants';
import { areSmileysDisabled, isSendGroupChatDisabled } from '../../functions'; import { areSmileysDisabled, isSendGroupChatDisabled } from '../../functions';
import SmileysPanel from './SmileysPanel'; import EmojiPicker from './EmojiPicker';
const styles = (_theme: Theme, { _chatWidth }: IProps) => { const styles = (_theme: Theme, { _chatWidth }: IProps) => {
@ -23,18 +23,14 @@ const styles = (_theme: Theme, { _chatWidth }: IProps) => {
boxSizing: 'border-box' as const, boxSizing: 'border-box' as const,
backgroundColor: 'rgba(0, 0, 0, .6) !important', backgroundColor: 'rgba(0, 0, 0, .6) !important',
height: 'auto', height: 'auto',
maxHeight: '435px',
display: 'flex' as const, display: 'flex' as const,
overflow: 'hidden', overflow: 'hidden',
position: 'absolute' as const, position: 'absolute' as const,
width: `${_chatWidth - 32}px`, width: `${_chatWidth - 32}px`,
marginBottom: '5px', marginBottom: '5px',
marginLeft: '-5px', marginLeft: '-5px',
transition: 'max-height 0.3s', borderRadius: '10px'
'& #smileysContainer': {
backgroundColor: '#131519',
borderTop: '1px solid #A4B8D1'
}
}, },
chatDisabled: { chatDisabled: {
borderTop: `1px solid ${_theme.palette.ui02}`, borderTop: `1px solid ${_theme.palette.ui02}`,
@ -183,8 +179,8 @@ class ChatInput extends Component<IProps, IState> {
className = 'smiley-input'> className = 'smiley-input'>
<div <div
className = { classes.smileysPanel } > className = { classes.smileysPanel } >
<SmileysPanel <EmojiPicker
onSmileySelect = { this._onSmileySelect } /> onEmojiSelect = { this._onSmileySelect } />
</div> </div>
</div> </div>
)} )}

View File

@ -0,0 +1,56 @@
import React, { useCallback, useEffect, useState } from 'react';
import { makeStyles } from 'tss-react/mui';
const useStyles = makeStyles()(() => {
return {
loading: {
alignItems: 'center',
display: 'flex',
height: '435px',
justifyContent: 'center'
}
};
});
interface IProps {
onEmojiSelect: (emoji: string) => void;
}
const EmojiPicker: React.FC<IProps> = ({ onEmojiSelect }) => {
const { classes } = useStyles();
const [ PickerComponent, setPickerComponent ] = useState<React.ComponentType<any> | null>(null);
const [ data, setData ] = useState<object | null>(null);
useEffect(() => {
Promise.all([
import('@emoji-mart/react'),
import('@emoji-mart/data')
]).then(([ pickerMod, dataMod ]) => {
setPickerComponent(() => pickerMod.default);
setData(dataMod.default);
});
}, []);
const handleSelect = useCallback((emoji: { native: string; }) => {
onEmojiSelect(emoji.native);
}, [ onEmojiSelect ]);
if (!PickerComponent || !data) {
return (
<div className = { classes.loading }>
Loading...
</div>
);
}
return (
<PickerComponent
data = { data }
onEmojiSelect = { handleSelect }
previewPosition = 'none'
skinTonePosition = 'search'
theme = 'dark' />
);
};
export default EmojiPicker;

View File

@ -3,6 +3,7 @@ import React, { useCallback } from 'react';
import { makeStyles } from 'tss-react/mui'; import { makeStyles } from 'tss-react/mui';
interface IProps { interface IProps {
onExpand?: () => void;
onSelect: (emoji: string) => void; onSelect: (emoji: string) => void;
} }
@ -19,11 +20,25 @@ const useStyles = makeStyles()((theme: Theme) => {
cursor: 'pointer', cursor: 'pointer',
padding: '5px', padding: '5px',
fontSize: '1.5em' fontSize: '1.5em'
},
expandButton: {
cursor: 'pointer',
padding: '5px',
fontSize: '1.2em',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
opacity: 0.7,
'&:hover': {
opacity: 1
}
} }
}; };
}); });
const EmojiSelector: React.FC<IProps> = ({ onSelect }) => { const EmojiSelector: React.FC<IProps> = ({ onSelect, onExpand }) => {
const { classes } = useStyles(); const { classes } = useStyles();
const emojiMap: Record<string, string> = { const emojiMap: Record<string, string> = {
@ -31,7 +46,10 @@ const EmojiSelector: React.FC<IProps> = ({ onSelect }) => {
redHeart: '❤️', redHeart: '❤️',
faceWithTearsOfJoy: '😂', faceWithTearsOfJoy: '😂',
faceWithOpenMouth: '😮', faceWithOpenMouth: '😮',
fire: '🔥' fire: '🔥',
clap: '👏',
party: '🎉',
thinking: '🤔'
}; };
const emojiNames = Object.keys(emojiMap); const emojiNames = Object.keys(emojiMap);
@ -43,6 +61,14 @@ const EmojiSelector: React.FC<IProps> = ({ onSelect }) => {
[ onSelect ] [ onSelect ]
); );
const handleExpand = useCallback(
(event: React.MouseEvent<HTMLSpanElement>) => {
event.preventDefault();
onExpand?.();
},
[ onExpand ]
);
return ( return (
<div className = { classes.emojiGrid }> <div className = { classes.emojiGrid }>
{emojiNames.map(name => ( {emojiNames.map(name => (
@ -53,6 +79,13 @@ const EmojiSelector: React.FC<IProps> = ({ onSelect }) => {
{emojiMap[name]} {emojiMap[name]}
</span> </span>
))} ))}
{onExpand && (
<span
className = { classes.expandButton }
onClick = { handleExpand }>
+
</span>
)}
</div> </div>
); );
}; };

View File

@ -10,6 +10,7 @@ import Button from '../../../base/ui/components/web/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.any'; import { BUTTON_TYPES } from '../../../base/ui/constants.any';
import { sendReaction } from '../../actions.any'; import { sendReaction } from '../../actions.any';
import EmojiPicker from './EmojiPicker';
import EmojiSelector from './EmojiSelector'; import EmojiSelector from './EmojiSelector';
interface IProps { interface IProps {
@ -40,18 +41,21 @@ const ReactButton = ({ messageId, receiverId }: IProps) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const onSendReaction = useCallback(emoji => { const onSendReaction = useCallback((emoji: string) => {
dispatch(sendReaction(emoji, messageId, receiverId)); dispatch(sendReaction(emoji, messageId, receiverId));
}, [ dispatch, messageId, receiverId ]); }, [ dispatch, messageId, receiverId ]);
const [ isPopoverOpen, setIsPopoverOpen ] = useState(false); const [ isPopoverOpen, setIsPopoverOpen ] = useState(false);
const [ showFullPicker, setShowFullPicker ] = useState(false);
const handleReactClick = useCallback(() => { const handleReactClick = useCallback(() => {
setIsPopoverOpen(true); setIsPopoverOpen(true);
setShowFullPicker(false);
}, []); }, []);
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
setIsPopoverOpen(false); setIsPopoverOpen(false);
setShowFullPicker(false);
}, []); }, []);
const handleEmojiSelect = useCallback((emoji: string) => { const handleEmojiSelect = useCallback((emoji: string) => {
@ -59,9 +63,18 @@ const ReactButton = ({ messageId, receiverId }: IProps) => {
handleClose(); handleClose();
}, [ onSendReaction, handleClose ]); }, [ onSendReaction, handleClose ]);
const handleExpand = useCallback(() => {
setShowFullPicker(true);
}, []);
const popoverContent = ( const popoverContent = (
<div className = { classes.popoverContent }> <div className = { classes.popoverContent }>
<EmojiSelector onSelect = { handleEmojiSelect } /> {showFullPicker
? <EmojiPicker onEmojiSelect = { handleEmojiSelect } />
: <EmojiSelector
onExpand = { handleExpand }
onSelect = { handleEmojiSelect } />
}
</div> </div>
); );

View File

@ -79,7 +79,7 @@ class ReactionEmoji extends Component<IProps, IState> {
<div <div
className = { `reaction-emoji reaction-${index}` } className = { `reaction-emoji reaction-${index}` }
id = { uid }> id = { uid }>
{ REACTIONS[reaction].emoji } { REACTIONS[reaction]?.emoji ?? reaction }
</div> </div>
); );
} }

View File

@ -143,6 +143,11 @@ const _getReactionButtons = (dispatch: IStore['dispatch'], t: Function) => {
sendAnalytics(createReactionMenuEvent(key)); sendAnalytics(createReactionMenuEvent(key));
} }
const { shortcutChar } = REACTIONS[key];
const tooltip = shortcutChar
? `${t(`toolbar.${key}`)} (${modifierKey} + ${shortcutChar})`
: t(`toolbar.${key}`);
return (<ReactionButton return (<ReactionButton
accessibilityLabel = { t(`toolbar.accessibilityLabel.${key}`) } accessibilityLabel = { t(`toolbar.accessibilityLabel.${key}`) }
icon = { REACTIONS[key].emoji } icon = { REACTIONS[key].emoji }
@ -150,7 +155,7 @@ const _getReactionButtons = (dispatch: IStore['dispatch'], t: Function) => {
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick = { doSendReaction } onClick = { doSendReaction }
toggled = { false } toggled = { false }
tooltip = { `${t(`toolbar.${key}`)} (${modifierKey} + ${REACTIONS[key].shortcutChar})` } />); tooltip = { tooltip } />);
}); });
}; };

View File

@ -128,9 +128,9 @@ interface IReactions {
[key: string]: { [key: string]: {
emoji: string; emoji: string;
message: string; message: string;
shortcutChar: string; shortcutChar?: string;
soundFiles: string[]; soundFiles?: string[];
soundId: string; soundId?: string;
}; };
} }
@ -183,6 +183,38 @@ export const REACTIONS: IReactions = {
shortcutChar: 'H', shortcutChar: 'H',
soundId: HEART_SOUND_ID, soundId: HEART_SOUND_ID,
soundFiles: HEART_SOUND_FILES soundFiles: HEART_SOUND_FILES
},
party: {
message: ':party:',
emoji: '🎉'
},
thinking: {
message: ':thinking:',
emoji: '🤔'
},
wave: {
message: ':wave:',
emoji: '👋'
},
cry: {
message: ':cry:',
emoji: '😢'
},
eyes: {
message: ':eyes:',
emoji: '👀'
},
mindblown: {
message: ':mindblown:',
emoji: '🤯'
},
pray: {
message: ':pray:',
emoji: '🙏'
},
rocket: {
message: ':rocket:',
emoji: '🚀'
} }
}; };

View File

@ -27,7 +27,10 @@ export function getReactionsQueue(state: IReduxState): Array<IReactionEmojiProps
* @returns {string} * @returns {string}
*/ */
export function getReactionMessageFromBuffer(buffer: Array<string>): string { export function getReactionMessageFromBuffer(buffer: Array<string>): string {
return buffer.map<string>(reaction => REACTIONS[reaction].message).reduce((acc, val) => `${acc}${val}`); return buffer
.filter(reaction => REACTIONS[reaction]?.message)
.map<string>(reaction => REACTIONS[reaction].message)
.reduce((acc, val) => `${acc}${val}`, '');
} }
/** /**
@ -139,7 +142,9 @@ function getSoundThresholdByFrequency(frequency: number): number {
export function getReactionsSoundsThresholds(reactions: Array<string>): Array<ReactionThreshold> { export function getReactionsSoundsThresholds(reactions: Array<string>): Array<ReactionThreshold> {
const unique = getUniqueReactions(reactions); const unique = getUniqueReactions(reactions);
return unique.map<ReactionThreshold>(reaction => { return unique
.filter(reaction => REACTIONS[reaction]?.soundId)
.map<ReactionThreshold>(reaction => {
return { return {
reaction, reaction,
threshold: getSoundThresholdByFrequency(getReactionFrequency(reactions, reaction)) threshold: getSoundThresholdByFrequency(getReactionFrequency(reactions, reaction))

View File

@ -67,15 +67,17 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
case APP_WILL_MOUNT: case APP_WILL_MOUNT:
batch(() => { batch(() => {
Object.keys(REACTIONS).forEach(key => { Object.keys(REACTIONS).forEach(key => {
const { soundId, soundFiles } = REACTIONS[key];
if (soundId && soundFiles) {
for (let i = 0; i < SOUNDS_THRESHOLDS.length; i++) { for (let i = 0; i < SOUNDS_THRESHOLDS.length; i++) {
dispatch(registerSound( dispatch(registerSound(
`${REACTIONS[key].soundId}${SOUNDS_THRESHOLDS[i]}`, `${soundId}${SOUNDS_THRESHOLDS[i]}`,
REACTIONS[key].soundFiles[i] soundFiles[i]
) ));
);
} }
} }
); });
dispatch(registerSound(RAISE_HAND_SOUND_ID, RAISE_HAND_SOUND_FILE)); dispatch(registerSound(RAISE_HAND_SOUND_ID, RAISE_HAND_SOUND_FILE));
}); });
break; break;
@ -83,8 +85,12 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
case APP_WILL_UNMOUNT: case APP_WILL_UNMOUNT:
batch(() => { batch(() => {
Object.keys(REACTIONS).forEach(key => { Object.keys(REACTIONS).forEach(key => {
const { soundId } = REACTIONS[key];
if (soundId) {
for (let i = 0; i < SOUNDS_THRESHOLDS.length; i++) { for (let i = 0; i < SOUNDS_THRESHOLDS.length; i++) {
dispatch(unregisterSound(`${REACTIONS[key].soundId}${SOUNDS_THRESHOLDS[i]}`)); dispatch(unregisterSound(`${soundId}${SOUNDS_THRESHOLDS[i]}`));
}
} }
}); });
dispatch(unregisterSound(RAISE_HAND_SOUND_ID)); dispatch(unregisterSound(RAISE_HAND_SOUND_ID));
@ -146,9 +152,13 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
if (soundsReactions) { if (soundsReactions) {
const reactionSoundsThresholds = getReactionsSoundsThresholds(reactions); const reactionSoundsThresholds = getReactionsSoundsThresholds(reactions);
reactionSoundsThresholds.forEach(reaction => reactionSoundsThresholds.forEach(reaction => {
dispatch(playSound(`${REACTIONS[reaction.reaction].soundId}${reaction.threshold}`)) const { soundId } = REACTIONS[reaction.reaction] ?? {};
);
if (soundId) {
dispatch(playSound(`${soundId}${reaction.threshold}`));
}
});
} }
dispatch(setReactionQueue([ ...queue, ...getReactionsWithId(reactions) ])); dispatch(setReactionQueue([ ...queue, ...getReactionsWithId(reactions) ]));
}); });

View File

@ -590,7 +590,9 @@ export const useKeyboardShortcuts = (toolbarButtons: Array<string>) => {
// If the buttons for sending reactions are not displayed we should disable the shortcuts too. // If the buttons for sending reactions are not displayed we should disable the shortcuts too.
if (_shouldDisplayReactionsButtons) { if (_shouldDisplayReactionsButtons) {
const REACTION_SHORTCUTS = Object.keys(REACTIONS).map(key => { const REACTION_SHORTCUTS = Object.keys(REACTIONS)
.filter(key => REACTIONS[key].shortcutChar)
.map(key => {
const onShortcutSendReaction = () => { const onShortcutSendReaction = () => {
dispatch(addReactionToBuffer(key)); dispatch(addReactionToBuffer(key));
sendAnalytics(createShortcutEvent( sendAnalytics(createShortcutEvent(
@ -599,7 +601,7 @@ export const useKeyboardShortcuts = (toolbarButtons: Array<string>) => {
}; };
return { return {
character: REACTIONS[key].shortcutChar, character: REACTIONS[key].shortcutChar!,
exec: onShortcutSendReaction, exec: onShortcutSendReaction,
helpDescription: `toolbar.reaction${key.charAt(0).toUpperCase()}${key.slice(1)}`, helpDescription: `toolbar.reaction${key.charAt(0).toUpperCase()}${key.slice(1)}`,
altKey: true altKey: true
@ -636,7 +638,9 @@ export const useKeyboardShortcuts = (toolbarButtons: Array<string>) => {
dispatch(unregisterShortcut(letter))); dispatch(unregisterShortcut(letter)));
if (_shouldDisplayReactionsButtons) { if (_shouldDisplayReactionsButtons) {
Object.keys(REACTIONS).map(key => REACTIONS[key].shortcutChar) Object.keys(REACTIONS)
.filter(key => REACTIONS[key].shortcutChar)
.map(key => REACTIONS[key].shortcutChar!)
.forEach(letter => .forEach(letter =>
dispatch(unregisterShortcut(letter, true))); dispatch(unregisterShortcut(letter, true)));
} }