diff --git a/css/_reactions-menu.scss b/css/_reactions-menu.scss index 3406c25..8d82c90 100644 --- a/css/_reactions-menu.scss +++ b/css/_reactions-menu.scss @@ -1,7 +1,7 @@ @use 'sass:math'; .reactions-menu { - width: 330px; + width: 360px; background: #242528; box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25); border-radius: 6px; @@ -70,15 +70,15 @@ } .reactions-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 4px; + .toolbox-button { - margin-right: 8px; touch-action: manipulation; position: relative; } - - .toolbox-button:last-of-type { - margin-right: 0; - } } .raise-hand-row { diff --git a/lang/main.json b/lang/main.json index 5102f55..12bf67b 100644 --- a/lang/main.json +++ b/lang/main.json @@ -1314,6 +1314,7 @@ "audioRoute": "Select the sound device", "boo": "Boo", "breakoutRooms": "Breakout rooms", + "cry": "Cry", "callQuality": "Manage video quality", "carmode": "Car Mode", "cc": "Toggle subtitles", @@ -1332,6 +1333,7 @@ "endConference": "End meeting for all", "enterFullScreen": "View full screen", "enterTileView": "Enter tile view", + "eyes": "Eyes", "exitFullScreen": "Exit full screen", "exitTileView": "Exit tile view", "expand": "Expand", @@ -1349,6 +1351,7 @@ "leaveConference": "Leave meeting", "like": "Thumbs Up", "linkToSalesforce": "Link to Salesforce", + "mindblown": "Mind Blown", "lobbyButton": "Enable/disable lobby mode", "localRecording": "Toggle local recording controls", "lockRoom": "Toggle meeting password", @@ -1365,6 +1368,8 @@ "muteGUMPending": "Connecting your microphone", "noiseSuppression": "Extra noise suppression", "openChat": "Open chat", + "party": "Party", + "pray": "Pray", "participants": "Open participants panel. {{participantsCount}} participants", "pip": "Toggle Picture-in-Picture mode", "privateMessage": "Send private message", @@ -1376,6 +1381,7 @@ "recording": "Toggle recording", "remoteMute": "Mute participant", "remoteVideoMute": "Disable camera of participant", + "rocket": "Rocket", "security": "Security options", "selectBackground": "Select Background", "selfView": "Toggle self view", @@ -1396,13 +1402,15 @@ "stopSharedVideo": "Stop video", "surprised": "Surprised", "tileView": "Toggle tile view", + "thinking": "Thinking", "toggleCamera": "Toggle camera", "toggleFilmstrip": "Toggle filmstrip", "unmute": "Unmute microphone", "videoblur": "Toggle video blur", "videomute": "Stop camera", "videomuteGUMPending": "Connecting your camera", - "videounmute": "Start camera" + "videounmute": "Start camera", + "wave": "Wave" }, "addPeople": "Add people to your call", "advancedAudioSettings": { @@ -1426,6 +1434,7 @@ "authenticate": "Authenticate", "boo": "Boo", "callQuality": "Manage video quality", + "cry": "Cry", "chat": "Open / Close chat", "clap": "Clap", "closeChat": "Close chat", @@ -1445,6 +1454,7 @@ "enterTileView": "Enter tile view", "exitFullScreen": "Exit full screen", "exitTileView": "Exit tile view", + "eyes": "Eyes", "feedback": "Leave feedback", "fileSharing": "File sharing", "giphy": "Toggle GIPHY menu", @@ -1464,6 +1474,7 @@ "logout": "Log Out", "love": "Heart", "lowerYourHand": "Lower your hand", + "mindblown": "Mind Blown", "moreActions": "More actions", "moreOptions": "More options", "mute": "Mute microphone", @@ -1480,9 +1491,11 @@ "noisyAudioInputTitle": "Your microphone appears to be noisy!", "openChat": "Open chat", "openReactionsMenu": "Open reactions menu", + "party": "Party", "participants": "Participants", "pip": "Enter Picture-in-Picture mode", "polls": "Polls", + "pray": "Pray", "privateMessage": "Send private message", "profile": "Edit your profile", "raiseHand": "Raise your hand", @@ -1496,6 +1509,7 @@ "reactionSilence": "Send silence reaction", "reactionSurprised": "Send surprised reaction", "reactions": "Reactions", + "rocket": "Rocket", "security": "Security options", "selectBackground": "Select background", "shareRoom": "Invite someone", @@ -1518,13 +1532,15 @@ "stopSubtitles": "Stop subtitles", "surprised": "Surprised", "talkWhileMutedPopup": "Trying to speak? You are muted.", + "thinking": "Thinking", "tileViewToggle": "Toggle tile view", "toggleCamera": "Toggle camera", "unmute": "Unmute microphone", "videoSettings": "Video settings", "videomute": "Stop camera", "videomuteGUMPending": "Connecting your camera", - "videounmute": "Start camera" + "videounmute": "Start camera", + "wave": "Wave" }, "transcribing": { "ccButtonTooltip": "Start / Stop subtitles", diff --git a/package-lock.json b/package-lock.json index e464ad9..64c15b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "@amplitude/analytics-browser": "2.17.12", "@amplitude/analytics-react-native": "1.4.13", "@braintree/sanitize-url": "7.0.0", + "@emoji-mart/data": "^1.2.1", + "@emoji-mart/react": "^1.1.1", "@emotion/react": "11.10.6", "@emotion/styled": "11.10.6", "@giphy/js-fetch-api": "4.9.3", @@ -2678,6 +2680,22 @@ "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": { "version": "11.10.6", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz", @@ -28840,6 +28858,16 @@ "@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": { "version": "11.10.6", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz", diff --git a/package.json b/package.json index 24c483e..83014f9 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "@amplitude/analytics-browser": "2.17.12", "@amplitude/analytics-react-native": "1.4.13", "@braintree/sanitize-url": "7.0.0", + "@emoji-mart/data": "^1.2.1", + "@emoji-mart/react": "^1.1.1", "@emotion/react": "11.10.6", "@emotion/styled": "11.10.6", "@giphy/js-fetch-api": "4.9.3", diff --git a/react/features/chat/components/web/ChatInput.tsx b/react/features/chat/components/web/ChatInput.tsx index 2b9398f..595fd67 100644 --- a/react/features/chat/components/web/ChatInput.tsx +++ b/react/features/chat/components/web/ChatInput.tsx @@ -13,7 +13,7 @@ import Input from '../../../base/ui/components/web/Input'; import { CHAT_SIZE } from '../../constants'; import { areSmileysDisabled, isSendGroupChatDisabled } from '../../functions'; -import SmileysPanel from './SmileysPanel'; +import EmojiPicker from './EmojiPicker'; const styles = (_theme: Theme, { _chatWidth }: IProps) => { @@ -23,18 +23,14 @@ const styles = (_theme: Theme, { _chatWidth }: IProps) => { boxSizing: 'border-box' as const, backgroundColor: 'rgba(0, 0, 0, .6) !important', height: 'auto', + maxHeight: '435px', display: 'flex' as const, overflow: 'hidden', position: 'absolute' as const, width: `${_chatWidth - 32}px`, marginBottom: '5px', marginLeft: '-5px', - transition: 'max-height 0.3s', - - '& #smileysContainer': { - backgroundColor: '#131519', - borderTop: '1px solid #A4B8D1' - } + borderRadius: '10px' }, chatDisabled: { borderTop: `1px solid ${_theme.palette.ui02}`, @@ -183,8 +179,8 @@ class ChatInput extends Component { className = 'smiley-input'>
- +
)} diff --git a/react/features/chat/components/web/EmojiPicker.tsx b/react/features/chat/components/web/EmojiPicker.tsx new file mode 100644 index 0000000..4e99702 --- /dev/null +++ b/react/features/chat/components/web/EmojiPicker.tsx @@ -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 = ({ onEmojiSelect }) => { + const { classes } = useStyles(); + const [ PickerComponent, setPickerComponent ] = useState | null>(null); + const [ data, setData ] = useState(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 ( +
+ Loading... +
+ ); + } + + return ( + + ); +}; + +export default EmojiPicker; diff --git a/react/features/chat/components/web/EmojiSelector.tsx b/react/features/chat/components/web/EmojiSelector.tsx index 82e4ff2..e0b8f62 100644 --- a/react/features/chat/components/web/EmojiSelector.tsx +++ b/react/features/chat/components/web/EmojiSelector.tsx @@ -3,6 +3,7 @@ import React, { useCallback } from 'react'; import { makeStyles } from 'tss-react/mui'; interface IProps { + onExpand?: () => void; onSelect: (emoji: string) => void; } @@ -19,11 +20,25 @@ const useStyles = makeStyles()((theme: Theme) => { cursor: 'pointer', padding: '5px', 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 = ({ onSelect }) => { +const EmojiSelector: React.FC = ({ onSelect, onExpand }) => { const { classes } = useStyles(); const emojiMap: Record = { @@ -31,7 +46,10 @@ const EmojiSelector: React.FC = ({ onSelect }) => { redHeart: '❤️', faceWithTearsOfJoy: '😂', faceWithOpenMouth: '😮', - fire: '🔥' + fire: '🔥', + clap: '👏', + party: '🎉', + thinking: '🤔' }; const emojiNames = Object.keys(emojiMap); @@ -43,6 +61,14 @@ const EmojiSelector: React.FC = ({ onSelect }) => { [ onSelect ] ); + const handleExpand = useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + onExpand?.(); + }, + [ onExpand ] + ); + return (
{emojiNames.map(name => ( @@ -53,6 +79,13 @@ const EmojiSelector: React.FC = ({ onSelect }) => { {emojiMap[name]} ))} + {onExpand && ( + + + + + )}
); }; diff --git a/react/features/chat/components/web/ReactButton.tsx b/react/features/chat/components/web/ReactButton.tsx index d693392..1b9d0bc 100644 --- a/react/features/chat/components/web/ReactButton.tsx +++ b/react/features/chat/components/web/ReactButton.tsx @@ -10,6 +10,7 @@ import Button from '../../../base/ui/components/web/Button'; import { BUTTON_TYPES } from '../../../base/ui/constants.any'; import { sendReaction } from '../../actions.any'; +import EmojiPicker from './EmojiPicker'; import EmojiSelector from './EmojiSelector'; interface IProps { @@ -40,18 +41,21 @@ const ReactButton = ({ messageId, receiverId }: IProps) => { const dispatch = useDispatch(); const { t } = useTranslation(); - const onSendReaction = useCallback(emoji => { + const onSendReaction = useCallback((emoji: string) => { dispatch(sendReaction(emoji, messageId, receiverId)); }, [ dispatch, messageId, receiverId ]); const [ isPopoverOpen, setIsPopoverOpen ] = useState(false); + const [ showFullPicker, setShowFullPicker ] = useState(false); const handleReactClick = useCallback(() => { setIsPopoverOpen(true); + setShowFullPicker(false); }, []); const handleClose = useCallback(() => { setIsPopoverOpen(false); + setShowFullPicker(false); }, []); const handleEmojiSelect = useCallback((emoji: string) => { @@ -59,9 +63,18 @@ const ReactButton = ({ messageId, receiverId }: IProps) => { handleClose(); }, [ onSendReaction, handleClose ]); + const handleExpand = useCallback(() => { + setShowFullPicker(true); + }, []); + const popoverContent = (
- + {showFullPicker + ? + : + }
); diff --git a/react/features/reactions/components/web/ReactionEmoji.tsx b/react/features/reactions/components/web/ReactionEmoji.tsx index 0a7bbf8..bc9a391 100644 --- a/react/features/reactions/components/web/ReactionEmoji.tsx +++ b/react/features/reactions/components/web/ReactionEmoji.tsx @@ -79,7 +79,7 @@ class ReactionEmoji extends Component {
- { REACTIONS[reaction].emoji } + { REACTIONS[reaction]?.emoji ?? reaction }
); } diff --git a/react/features/reactions/components/web/ReactionsMenu.tsx b/react/features/reactions/components/web/ReactionsMenu.tsx index 8266a08..b1689ff 100644 --- a/react/features/reactions/components/web/ReactionsMenu.tsx +++ b/react/features/reactions/components/web/ReactionsMenu.tsx @@ -143,6 +143,11 @@ const _getReactionButtons = (dispatch: IStore['dispatch'], t: Function) => { sendAnalytics(createReactionMenuEvent(key)); } + const { shortcutChar } = REACTIONS[key]; + const tooltip = shortcutChar + ? `${t(`toolbar.${key}`)} (${modifierKey} + ${shortcutChar})` + : t(`toolbar.${key}`); + return ( { // eslint-disable-next-line react/jsx-no-bind onClick = { doSendReaction } toggled = { false } - tooltip = { `${t(`toolbar.${key}`)} (${modifierKey} + ${REACTIONS[key].shortcutChar})` } />); + tooltip = { tooltip } />); }); }; diff --git a/react/features/reactions/constants.ts b/react/features/reactions/constants.ts index e968556..affd872 100644 --- a/react/features/reactions/constants.ts +++ b/react/features/reactions/constants.ts @@ -128,9 +128,9 @@ interface IReactions { [key: string]: { emoji: string; message: string; - shortcutChar: string; - soundFiles: string[]; - soundId: string; + shortcutChar?: string; + soundFiles?: string[]; + soundId?: string; }; } @@ -183,6 +183,38 @@ export const REACTIONS: IReactions = { shortcutChar: 'H', soundId: HEART_SOUND_ID, 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: '🚀' } }; diff --git a/react/features/reactions/functions.any.ts b/react/features/reactions/functions.any.ts index 3b5a645..6c1b2f7 100644 --- a/react/features/reactions/functions.any.ts +++ b/react/features/reactions/functions.any.ts @@ -27,7 +27,10 @@ export function getReactionsQueue(state: IReduxState): Array): string { - return buffer.map(reaction => REACTIONS[reaction].message).reduce((acc, val) => `${acc}${val}`); + return buffer + .filter(reaction => REACTIONS[reaction]?.message) + .map(reaction => REACTIONS[reaction].message) + .reduce((acc, val) => `${acc}${val}`, ''); } /** @@ -139,12 +142,14 @@ function getSoundThresholdByFrequency(frequency: number): number { export function getReactionsSoundsThresholds(reactions: Array): Array { const unique = getUniqueReactions(reactions); - return unique.map(reaction => { - return { - reaction, - threshold: getSoundThresholdByFrequency(getReactionFrequency(reactions, reaction)) - }; - }); + return unique + .filter(reaction => REACTIONS[reaction]?.soundId) + .map(reaction => { + return { + reaction, + threshold: getSoundThresholdByFrequency(getReactionFrequency(reactions, reaction)) + }; + }); } /** diff --git a/react/features/reactions/middleware.ts b/react/features/reactions/middleware.ts index ce753e2..4eacad3 100644 --- a/react/features/reactions/middleware.ts +++ b/react/features/reactions/middleware.ts @@ -67,15 +67,17 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA 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] - ) - ); + const { soundId, soundFiles } = REACTIONS[key]; + + if (soundId && soundFiles) { + 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)); }); break; @@ -83,8 +85,12 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA 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]}`)); + const { soundId } = REACTIONS[key]; + + if (soundId) { + for (let i = 0; i < SOUNDS_THRESHOLDS.length; i++) { + dispatch(unregisterSound(`${soundId}${SOUNDS_THRESHOLDS[i]}`)); + } } }); dispatch(unregisterSound(RAISE_HAND_SOUND_ID)); @@ -146,9 +152,13 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA if (soundsReactions) { const reactionSoundsThresholds = getReactionsSoundsThresholds(reactions); - reactionSoundsThresholds.forEach(reaction => - dispatch(playSound(`${REACTIONS[reaction.reaction].soundId}${reaction.threshold}`)) - ); + reactionSoundsThresholds.forEach(reaction => { + const { soundId } = REACTIONS[reaction.reaction] ?? {}; + + if (soundId) { + dispatch(playSound(`${soundId}${reaction.threshold}`)); + } + }); } dispatch(setReactionQueue([ ...queue, ...getReactionsWithId(reactions) ])); }); diff --git a/react/features/toolbox/hooks.web.ts b/react/features/toolbox/hooks.web.ts index f35e0ca..580a78e 100644 --- a/react/features/toolbox/hooks.web.ts +++ b/react/features/toolbox/hooks.web.ts @@ -590,21 +590,23 @@ export const useKeyboardShortcuts = (toolbarButtons: Array) => { // If the buttons for sending reactions are not displayed we should disable the shortcuts too. if (_shouldDisplayReactionsButtons) { - const REACTION_SHORTCUTS = Object.keys(REACTIONS).map(key => { - const onShortcutSendReaction = () => { - dispatch(addReactionToBuffer(key)); - sendAnalytics(createShortcutEvent( - `reaction.${key}` - )); - }; + const REACTION_SHORTCUTS = Object.keys(REACTIONS) + .filter(key => REACTIONS[key].shortcutChar) + .map(key => { + const onShortcutSendReaction = () => { + dispatch(addReactionToBuffer(key)); + sendAnalytics(createShortcutEvent( + `reaction.${key}` + )); + }; - return { - character: REACTIONS[key].shortcutChar, - exec: onShortcutSendReaction, - helpDescription: `toolbar.reaction${key.charAt(0).toUpperCase()}${key.slice(1)}`, - altKey: true - }; - }); + return { + character: REACTIONS[key].shortcutChar!, + exec: onShortcutSendReaction, + helpDescription: `toolbar.reaction${key.charAt(0).toUpperCase()}${key.slice(1)}`, + altKey: true + }; + }); REACTION_SHORTCUTS.forEach(shortcut => { dispatch(registerShortcut({ @@ -636,7 +638,9 @@ export const useKeyboardShortcuts = (toolbarButtons: Array) => { dispatch(unregisterShortcut(letter))); 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 => dispatch(unregisterShortcut(letter, true))); }