diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/reactions-button/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/reactions-button/component.jsx index d39e8cd2e5c9..efc1fd658b4e 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/reactions-button/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/reactions-button/component.jsx @@ -4,11 +4,14 @@ import PropTypes from 'prop-types'; import BBBMenu from '/imports/ui/components/common/menu/component'; import UserReactionService from '/imports/ui/components/user-reaction/service'; import UserListService from '/imports/ui/components/user-list/service'; -import { Emoji } from 'emoji-mart'; import { convertRemToPixels } from '/imports/utils/dom-utils'; +import data from '@emoji-mart/data'; +import { init } from 'emoji-mart'; import Styled from './styles'; +const REACTIONS = Meteor.settings.public.userReaction.reactions; + const ReactionsButton = (props) => { const { intl, @@ -20,6 +23,9 @@ const ReactionsButton = (props) => { autoCloseReactionsBar, } = props; + // initialize emoji-mart data, need for the new version + init({ data }); + const [showEmojiPicker, setShowEmojiPicker] = useState(false); const intlMessages = defineMessages({ @@ -74,43 +80,20 @@ const ReactionsButton = (props) => { }; const emojiProps = { - native: true, size: convertRemToPixels(1.5), padding: '4px', }; - const reactions = [ - { - id: 'smiley', - native: '😃', - }, - { - id: 'neutral_face', - native: '😐', - }, - { - id: 'slightly_frowning_face', - native: '🙁', - }, - { - id: '+1', - native: '👍', - }, - { - id: '-1', - native: '👎', - }, - { - id: 'clap', - native: '👏', - }, - ]; + const handReaction = { + id: 'hand', + native: '✋', + }; let actions = []; - reactions.forEach(({ id, native }) => { + REACTIONS.forEach(({ id, native }) => { actions.push({ - label: , + label: , key: id, onClick: () => handleReactionSelect(native), customStyles: actionCustomStyles, @@ -118,29 +101,29 @@ const ReactionsButton = (props) => { }); actions.push({ - label: {RaiseHandButtonLabel()}, + label: {RaiseHandButtonLabel()}, key: 'hand', onClick: () => handleRaiseHandButtonClick(), customStyles: {...actionCustomStyles, width: 'auto'}, }); const icon = !raiseHand && currentUserReaction === 'none' ? 'hand' : null; - const currentUserReactionEmoji = reactions.find(({ native }) => native === currentUserReaction); + const currentUserReactionEmoji = REACTIONS.find(({ native }) => native === currentUserReaction); let customIcon = null; if (raiseHand) { - customIcon = ; + customIcon = ; } else { if (!icon) { - customIcon = ; + customIcon = ; } } return ( + {this.renderActionsBar()} + {customStyleUrl ? : null} {customStyle ? : null} {isRandomUserSelectModalOpen ? this.handleEmojiSelect(emojiObject)} - showPreview={false} - showSkinTones={false} /> ); diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-form/styles.js b/bigbluebutton-html5/imports/ui/components/chat/message-form/styles.js index 343552b51c3b..d8d91d50b9a0 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/message-form/styles.js +++ b/bigbluebutton-html5/imports/ui/components/chat/message-form/styles.js @@ -109,21 +109,11 @@ const EmojiButton = styled(Button)` `; const EmojiPickerWrapper = styled.div` - .emoji-mart { - max-width: 100% !important; - } - .emoji-mart-anchor { - cursor: pointer; - } - .emoji-mart-emoji { - cursor: pointer !important; - } - .emoji-mart-category-list { - span { - cursor: pointer !important; - display: inline-block !important; - } + em-emoji-picker { + width: 100%; + max-height: 300px; } + padding-bottom: 5px; `; const EmojiPicker = styled(EmojiPickerComponent)``; diff --git a/bigbluebutton-html5/imports/ui/components/emoji-picker/component.jsx b/bigbluebutton-html5/imports/ui/components/emoji-picker/component.jsx index 2c630fbda2a6..0b3b015db4cf 100644 --- a/bigbluebutton-html5/imports/ui/components/emoji-picker/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/emoji-picker/component.jsx @@ -1,26 +1,16 @@ import React from 'react'; import PropTypes from 'prop-types'; import { injectIntl } from 'react-intl'; -import { Picker } from 'emoji-mart'; -import 'emoji-mart/css/emoji-mart.css'; +import data from '@emoji-mart/data'; +import Picker from '@emoji-mart/react'; const DISABLE_EMOJIS = Meteor.settings.public.chat.disableEmojis; -const FREQUENT_SORT_ON_CLICK = Meteor.settings.public.chat.emojiPicker.frequentEmojiSortOnClick; const propTypes = { intl: PropTypes.shape({ formatMessage: PropTypes.func.isRequired, }).isRequired, onEmojiSelect: PropTypes.func.isRequired, - style: PropTypes.shape({}), - showPreview: PropTypes.bool, - showSkinTones: PropTypes.bool, -}; - -const defaultProps = { - style: null, - showPreview: true, - showSkinTones: true, }; const emojisToExclude = [ @@ -31,8 +21,6 @@ const EmojiPicker = (props) => { const { intl, onEmojiSelect, - showPreview, - showSkinTones, } = props; const i18n = { @@ -65,23 +53,19 @@ const EmojiPicker = (props) => { return ( onEmojiSelect(emojiObject, event)} - enableFrequentEmojiSort={FREQUENT_SORT_ON_CLICK} - native - title="" + data={data} + onEmojiSelect={(emojiObject, event) => onEmojiSelect(emojiObject, event)} emojiSize={24} - emojiTooltip i18n={i18n} - showPreview={showPreview} - showSkinTones={showSkinTones} - useButton - emojisToShowFilter={(emoji) => !emojisToExclude.includes(emoji.unified)} + previewPosition="none" + skinTonePosition="none" + theme="light" + dynamicWidth + exceptEmojis={emojisToExclude} /> ); }; EmojiPicker.propTypes = propTypes; -EmojiPicker.defaultProps = defaultProps; export default injectIntl(EmojiPicker); diff --git a/bigbluebutton-html5/imports/ui/components/emoji-picker/reactions-picker/component.jsx b/bigbluebutton-html5/imports/ui/components/emoji-picker/reactions-picker/component.jsx deleted file mode 100644 index 01731e7ee42d..000000000000 --- a/bigbluebutton-html5/imports/ui/components/emoji-picker/reactions-picker/component.jsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { injectIntl } from 'react-intl'; -import { Picker } from 'emoji-mart'; -import 'emoji-mart/css/emoji-mart.css'; - -const propTypes = { - intl: PropTypes.shape({ - formatMessage: PropTypes.func.isRequired, - }).isRequired, - onEmojiSelect: PropTypes.func.isRequired, -}; - -const defaultProps = { -}; - -const emojisToExclude = [ - // reactions - '1F605', '1F61C', '1F604', '1F609', '1F602', '1F92A', - '1F634', '1F62C', '1F633', '1F60D', '02665', '1F499', - '1F615', '1F61F', '1F928', '1F644', '1F612', '1F621', - '1F614', '1F625', '1F62D', '1F62F', '1F631', '1F630', - '1F44F', '1F44C', '1F44D', '1F44E', '1F4AA', '1F44A', - '1F64C', '1F64F', '1F440', '1F4A9', '1F921', '1F480', -]; - -const inlineStyle = { - margin: '.5rem 0', - alignSelf: 'center', - width: '100%', -}; - -const ReactionsPicker = (props) => { - const { - intl, - onEmojiSelect, - style, - } = props; - - const i18n = { - notfound: intl.formatMessage({ id: 'app.emojiPicker.notFound' }), - clear: intl.formatMessage({ id: 'app.emojiPicker.clear' }), - skintext: intl.formatMessage({ id: 'app.emojiPicker.skintext' }), - categories: { - reactions: intl.formatMessage({ id: 'app.emojiPicker.categories.reactions' }), - }, - categorieslabel: intl.formatMessage({ id: 'app.emojiPicker.categories.label' }), - }; - - return ( - onEmojiSelect(emojiObject, event)} - native - emoji="" - title="" - emojiSize={30} - emojiTooltip - style={Object.assign(inlineStyle, style)} - i18n={i18n} - showPreview={false} - showSkinTones={false} - emojisToShowFilter={(emoji) => emojisToExclude.includes(emoji.unified)} - /> - - ); -}; - -ReactionsPicker.propTypes = propTypes; -ReactionsPicker.defaultProps = defaultProps; - -export default injectIntl(ReactionsPicker); diff --git a/bigbluebutton-html5/imports/ui/components/emoji-picker/reactions-picker/styles.js b/bigbluebutton-html5/imports/ui/components/emoji-picker/reactions-picker/styles.js new file mode 100644 index 000000000000..fda0dd7df31b --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/emoji-picker/reactions-picker/styles.js @@ -0,0 +1,35 @@ +import styled, { css } from 'styled-components'; + +const EmojiWrapper = styled.span` + padding-top: 0.9em; + padding-bottom: 0.1em; + ${({ selected }) => !selected && css` + :hover { + border-radius:100%; + outline-color: transparent; + outline-style:solid; + box-shadow: 0 0 0 0.25em #eee; + background-color: #eee; + opacity: 0.75; + } + `} + ${({ selected }) => selected && css` + em-emoji { + cursor: not-allowed; + } + `} + ${({ selected, emoji }) => selected && selected !== emoji && css` + opacity: 0.75; + `} + ${({ selected, emoji }) => selected && selected === emoji && css` + border-radius:100%; + outline-color: transparent; + outline-style:solid; + box-shadow: 0 0 0 0.25em #eee; + background-color: #eee; + `} +`; + +export default { + EmojiWrapper, +}; diff --git a/bigbluebutton-html5/imports/ui/components/emoji-rain/component.jsx b/bigbluebutton-html5/imports/ui/components/emoji-rain/component.jsx new file mode 100644 index 000000000000..8cca52e40274 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/emoji-rain/component.jsx @@ -0,0 +1,113 @@ +import React, { useRef, useState, useEffect } from 'react'; +import Settings from '/imports/ui/services/settings'; +import Service from './service'; + +const EmojiRain = ({ reactions }) => { + const containerRef = useRef(null); + const [isAnimating, setIsAnimating] = useState(false); + const EMOJI_SIZE = Meteor.settings.public.app.emojiRain.emojiSize; + const NUMBER_OF_EMOJIS = Meteor.settings.public.app.emojiRain.numberOfEmojis; + const EMOJI_RAIN_ENABLED = Meteor.settings.public.app.emojiRain.enabled; + + const { animations } = Settings.application; + + function createEmojiRain(emoji) { + const coord = Service.getInteractionsButtonCoordenates(); + const flyingEmojis = []; + + for (i = 0; i < NUMBER_OF_EMOJIS; i++) { + const initialPosition = { + x: coord.x + coord.width / 8, + y: coord.y + coord.height / 5, + }; + const endPosition = { + x: Math.floor(Math.random() * 100) + coord.x - 100 / 2, + y: Math.floor(Math.random() * 300) + coord.y / 2, + }; + const scale = Math.floor(Math.random() * (8 - 4 + 1)) - 40; + const sec = Math.floor(Math.random() * 1700) + 2000; + + const shapeElement = document.createElement('svg'); + const emojiElement = document.createElement('text'); + emojiElement.setAttribute('x', '50%'); + emojiElement.setAttribute('y', '50%'); + emojiElement.innerHTML = emoji; + + shapeElement.style.position = 'absolute'; + shapeElement.style.left = `${initialPosition.x}px`; + shapeElement.style.top = `${initialPosition.y}px`; + shapeElement.style.transform = `scaleX(0.${scale}) scaleY(0.${scale})`; + shapeElement.style.transition = `${sec}ms`; + shapeElement.style.fontSize = `${EMOJI_SIZE}em`; + shapeElement.style.pointerEvents = 'none'; + + shapeElement.appendChild(emojiElement); + containerRef.current.appendChild(shapeElement); + + flyingEmojis.push({ shapeElement, endPosition }); + } + + requestAnimationFrame(() => setTimeout(() => flyingEmojis.forEach((emoji) => { + const { shapeElement, endPosition } = emoji; + shapeElement.style.left = `${endPosition.x}px`; + shapeElement.style.top = `${endPosition.y}px`; + shapeElement.style.transform = 'scaleX(0) scaleY(0)'; + }), 0)); + + setTimeout(() => { + flyingEmojis.forEach((emoji) => emoji.shapeElement.remove()); + flyingEmojis.length = 0; + }, 2000); + } + + const handleVisibilityChange = () => { + if (document.hidden) { + setIsAnimating(false); + } else if (EMOJI_RAIN_ENABLED && animations) { + setIsAnimating(true); + } + }; + + useEffect(() => { + document.addEventListener('visibilitychange', handleVisibilityChange); + + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + }, []); + + useEffect(() => { + if (EMOJI_RAIN_ENABLED && animations && !isAnimating && !document.hidden) { + setIsAnimating(true); + } else if (!animations) { + setIsAnimating(false); + } + }, [EMOJI_RAIN_ENABLED, animations, isAnimating]); + + useEffect(() => { + if (isAnimating) { + reactions.forEach((reaction) => { + const currentTime = new Date().getTime(); + const secondsSinceCreated = (currentTime - reaction.creationDate.getTime()) / 1000; + if (secondsSinceCreated <= 1 && (reaction.reaction !== 'none')) { + createEmojiRain(reaction.reaction); + } + }); + } + }, [isAnimating, reactions]); + + const containerStyle = { + width: '100vw', + height: '100vh', + position: 'fixed', + top: 0, + left: 0, + overflow: 'hidden', + pointerEvents: 'none', + zIndex: 2, + }; + + return
; +}; + +export default EmojiRain; diff --git a/bigbluebutton-html5/imports/ui/components/emoji-rain/container.jsx b/bigbluebutton-html5/imports/ui/components/emoji-rain/container.jsx new file mode 100644 index 000000000000..d6f24ef79826 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/emoji-rain/container.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { withTracker } from 'meteor/react-meteor-data'; +import EmojiRain from './component'; +import UserReaction from '/imports/api/user-reaction'; +import Auth from '/imports/ui/services/auth'; + +const EmojiRainContainer = (props) => ; + +export default withTracker(() => ({ + reactions: UserReaction.find({ meetingId: Auth.meetingID }).fetch(), +}))(EmojiRainContainer); diff --git a/bigbluebutton-html5/imports/ui/components/emoji-rain/service.js b/bigbluebutton-html5/imports/ui/components/emoji-rain/service.js new file mode 100644 index 000000000000..69b221e8d4aa --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/emoji-rain/service.js @@ -0,0 +1,9 @@ +const getInteractionsButtonCoordenates = () => { + const el = document.getElementById('interactionsButton'); + const coordenada = el.getBoundingClientRect(); + return coordenada; +}; + +export default { + getInteractionsButtonCoordenates, +}; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx index 598191108dbd..5cfb8695b645 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx @@ -4,7 +4,6 @@ import { injectIntl, defineMessages } from 'react-intl'; import TooltipContainer from '/imports/ui/components/common/tooltip/container'; import { Session } from 'meteor/session'; import { findDOMNode } from 'react-dom'; -import { Emoji } from 'emoji-mart'; import UserAvatar from '/imports/ui/components/user-avatar/component'; import Icon from '/imports/ui/components/common/icon/component'; import lockContextContainer from '/imports/ui/components/lock-viewers/context/container'; @@ -634,20 +633,30 @@ class UserListItem extends PureComponent { } = this.props; const emojiProps = { - native: true, size: '1.3rem', }; let userAvatarFiltered = user.avatar; + const emojiIcons = [ + { + id: 'hand', + native: '✋', + }, + { + id: 'clock7', + native: '⏰', + }, + ]; + const getIconUser = () => { if (user.raiseHand === true) { return isReactionsEnabled - ? + ? : ; } if (user.away === true) { return isReactionsEnabled - ? + ? : ; } if (user.emoji !== 'none' && user.emoji !== 'notAway') { return ; diff --git a/bigbluebutton-html5/package-lock.json b/bigbluebutton-html5/package-lock.json index 6c20aca0e76e..070531068e25 100644 --- a/bigbluebutton-html5/package-lock.json +++ b/bigbluebutton-html5/package-lock.json @@ -337,6 +337,16 @@ "kuler": "^2.0.0" } }, + "@emoji-mart/data": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.1.2.tgz", + "integrity": "sha512-1HP8BxD2azjqWJvxIaWAMyTySeZY0Osr83ukYjltPVkNXeJvTz7yDrPLBtnrD5uqJ3tg4CcLuuBW09wahqL/fg==" + }, + "@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.11.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", @@ -2808,13 +2818,9 @@ "integrity": "sha512-ypZHxY+Sf/PXu7LVN+xoeanyisnJeSOy8Ki439L/oLueZb4c72FI45zXcK3gPpmTwyufh9m6NnbMLXnJh/0Fxg==" }, "emoji-mart": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-3.0.1.tgz", - "integrity": "sha512-sxpmMKxqLvcscu6mFn9ITHeZNkGzIvD0BSNFE/LJESPbCA8s1jM6bCDPjWbV31xHq7JXaxgpHxLB54RCbBZSlg==", - "requires": { - "@babel/runtime": "^7.0.0", - "prop-types": "^15.6.0" - } + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.5.2.tgz", + "integrity": "sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==" }, "emoji-regex": { "version": "8.0.0", diff --git a/bigbluebutton-html5/package.json b/bigbluebutton-html5/package.json index 56ca9b2833ac..76057857ecb3 100644 --- a/bigbluebutton-html5/package.json +++ b/bigbluebutton-html5/package.json @@ -31,6 +31,8 @@ "dependencies": { "@babel/runtime": "^7.17.9", "@browser-bunyan/server-stream": "^1.8.0", + "@emoji-mart/data": "^1.1.2", + "@emoji-mart/react": "^1.1.1", "@emotion/react": "^11.10.8", "@emotion/styled": "^11.10.8", "@jitsi/sdp-interop": "0.1.14", @@ -45,7 +47,7 @@ "browser-bunyan": "^1.8.0", "classnames": "^2.2.6", "darkreader": "^4.9.46", - "emoji-mart": "^3.0.1", + "emoji-mart": "^5.5.2", "eventemitter2": "~6.4.6", "fastdom": "^1.0.10", "fibers": "^4.0.2", diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index fe33f90ece7b..4193a4ffc9e3 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -152,6 +152,14 @@ public: # Enables the new raiseHand icon inside of the reaction menu (introduced in BBB 2.7) # If both reactionsButton and raiseHandActionButton are enabled, reactionsButton takes precedence. enabled: true + emojiRain: + # If true, new reactions will be activated + enabled: false + # Can set the throttle, the number of emojis that will be displayed and their size + intervalEmojis: 2000 + numberOfEmojis: 5 + # emojiSize: size of the emoji in 'em' units + emojiSize: 2 # If enabled, before joining microphone the client will perform a trickle # ICE against Kurento and use the information about successfull # candidate-pairs to filter out local candidates in SIP.js's SDP. @@ -589,12 +597,24 @@ public: autoConvertEmoji: true emojiPicker: enable: false - frequentEmojiSortOnClick: false - # e.g.: disableEmojis: ['1F595','1F922'] + # e.g.: disableEmojis: ['grin','laughing'] disableEmojis: [] userReaction: enabled: true expire: 60 + reactions: + - id: 'smiley' + native: '😃' + - id: 'neutral_face' + native: '😐' + - id: 'slightly_frowning_face' + native: '🙁' + - id: '+1' + native: '👍' + - id: '-1' + native: '👎' + - id: 'clap' + native: '👏' userStatus: enabled: false notes: