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.