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 d39e8cd2e5..da65ee50ec 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 @@ -140,7 +140,7 @@ const ReactionsButton = (props) => { return ( + {this.renderActionsBar()} + {customStyleUrl ? : null} {customStyle ? : null} {isRandomUserSelectModalOpen ? { + 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) => { + if (Date() == reaction.creationDate && (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 0000000000..d6f24ef798 --- /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 0000000000..69b221e8d4 --- /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/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index fe33f90ece..1f2fd1bae4 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.