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:
parent
ef6a5a1ffe
commit
325960fa15
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 } />);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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: '🚀'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,12 +142,14 @@ 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
|
||||||
return {
|
.filter(reaction => REACTIONS[reaction]?.soundId)
|
||||||
reaction,
|
.map<ReactionThreshold>(reaction => {
|
||||||
threshold: getSoundThresholdByFrequency(getReactionFrequency(reactions, reaction))
|
return {
|
||||||
};
|
reaction,
|
||||||
});
|
threshold: getSoundThresholdByFrequency(getReactionFrequency(reactions, reaction))
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
for (let i = 0; i < SOUNDS_THRESHOLDS.length; i++) {
|
const { soundId, soundFiles } = REACTIONS[key];
|
||||||
dispatch(registerSound(
|
|
||||||
`${REACTIONS[key].soundId}${SOUNDS_THRESHOLDS[i]}`,
|
if (soundId && soundFiles) {
|
||||||
REACTIONS[key].soundFiles[i]
|
for (let i = 0; i < SOUNDS_THRESHOLDS.length; i++) {
|
||||||
)
|
dispatch(registerSound(
|
||||||
);
|
`${soundId}${SOUNDS_THRESHOLDS[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 => {
|
||||||
for (let i = 0; i < SOUNDS_THRESHOLDS.length; i++) {
|
const { soundId } = REACTIONS[key];
|
||||||
dispatch(unregisterSound(`${REACTIONS[key].soundId}${SOUNDS_THRESHOLDS[i]}`));
|
|
||||||
|
if (soundId) {
|
||||||
|
for (let i = 0; i < SOUNDS_THRESHOLDS.length; 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) ]));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -590,21 +590,23 @@ 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)
|
||||||
const onShortcutSendReaction = () => {
|
.filter(key => REACTIONS[key].shortcutChar)
|
||||||
dispatch(addReactionToBuffer(key));
|
.map(key => {
|
||||||
sendAnalytics(createShortcutEvent(
|
const onShortcutSendReaction = () => {
|
||||||
`reaction.${key}`
|
dispatch(addReactionToBuffer(key));
|
||||||
));
|
sendAnalytics(createShortcutEvent(
|
||||||
};
|
`reaction.${key}`
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
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
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
REACTION_SHORTCUTS.forEach(shortcut => {
|
REACTION_SHORTCUTS.forEach(shortcut => {
|
||||||
dispatch(registerShortcut({
|
dispatch(registerShortcut({
|
||||||
|
|
@ -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)));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue