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..efc1fd658b 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 343552b51c..d8d91d50b9 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 2c630fbda2..0b3b015db4 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 01731e7ee4..0000000000
--- 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 0000000000..fda0dd7df3
--- /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 0000000000..8cca52e402
--- /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 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/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 598191108d..5cfb8695b6 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 6c20aca0e7..070531068e 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 56ca9b2833..76057857ec 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 fe33f90ece..4193a4ffc9 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: