feat(reactions): port new reactions and fix emojiRain

This commit is contained in:
Lucas Fialho Zawacki 2023-11-13 15:36:49 -03:00 committed by André
parent 6a10a27f2a
commit f575ecd4c8
16 changed files with 264 additions and 214 deletions

View File

@ -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: <Styled.ButtonWrapper active={currentUserReaction === native}><Emoji key={id} emoji={{ id }} {...emojiProps} /></Styled.ButtonWrapper>,
label: <Styled.ButtonWrapper active={currentUserReaction === native}><em-emoji key={native} native={native} {...emojiProps} /></Styled.ButtonWrapper>,
key: id,
onClick: () => handleReactionSelect(native),
customStyles: actionCustomStyles,
@ -118,29 +101,29 @@ const ReactionsButton = (props) => {
});
actions.push({
label: <Styled.RaiseHandButtonWrapper isMobile={isMobile} data-test={raiseHand ? 'lowerHandBtn' : 'raiseHandBtn'} active={raiseHand}><Emoji key="hand" emoji={{ id: 'hand' }} {...emojiProps} />{RaiseHandButtonLabel()}</Styled.RaiseHandButtonWrapper>,
label: <Styled.RaiseHandButtonWrapper isMobile={isMobile} data-test={raiseHand ? 'lowerHandBtn' : 'raiseHandBtn'} active={raiseHand}><em-emoji key={handReaction.id} native={handReaction.native} emoji={{ id: handReaction.id }} {...emojiProps} />{RaiseHandButtonLabel()}</Styled.RaiseHandButtonWrapper>,
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 = <Emoji key="hand" emoji={{ id: 'hand' }} {...emojiProps} />;
customIcon = <em-emoji key={handReaction.id} native={handReaction.native} emoji={handReaction} {...emojiProps} />;
} else {
if (!icon) {
customIcon = <Emoji key={currentUserReactionEmoji?.id} emoji={{ id: currentUserReactionEmoji?.id }} {...emojiProps} />;
customIcon = <em-emoji key={currentUserReactionEmoji?.id} native={currentUserReactionEmoji?.native} emoji={{ id: currentUserReactionEmoji?.id }} {...emojiProps} />;
}
}
return (
<BBBMenu
trigger={(
<Styled.ReactionsDropdown>
<Styled.ReactionsDropdown id="interactionsButton">
<Styled.RaiseHandButton
data-test="reactionsButton"
icon={icon}

View File

@ -101,28 +101,13 @@ const ReactionsDropdown = styled.div`
`;
const Wrapper = styled.div`
.emoji-mart-bar {
display: none;
}
.emoji-mart-search {
display: none;
}
.emoji-mart-category[aria-label="Frequently Used"] {
display: none;
}
.emoji-mart-category-label{
display: none;
}
.emoji-mart{
border: none;
}
@media(min-width: 600px) {
.emoji-mart-scroll{
overflow:hidden;
padding: 0;
height: 270px;
width: 280px;
}
overflow: hidden;
margin: 0.2em 0.2em 0.2em 0.2em;
text-align: center;
max-height: 270px;
width: 270px;
em-emoji {
cursor: pointer;
}
`;

View File

@ -29,6 +29,7 @@ import WebcamContainer from '../webcam/container';
import PresentationAreaContainer from '../presentation/presentation-area/container';
import ScreenshareContainer from '../screenshare/container';
import ExternalVideoContainer from '../external-video-player/container';
import EmojiRainContainer from '../emoji-rain/container';
import Styled from './styles';
import { DEVICE_TYPE, ACTIONS, SMALL_VIEWPORT_BREAKPOINT, PANELS } from '../layout/enums';
import {
@ -660,6 +661,7 @@ class App extends Component {
<PadsSessionsContainer />
<WakeLockContainer />
{this.renderActionsBar()}
<EmojiRainContainer />
{customStyleUrl ? <link rel="stylesheet" type="text/css" href={customStyleUrl} /> : null}
{customStyle ? <link rel="stylesheet" type="text/css" href={`data:text/css;charset=UTF-8,${encodeURIComponent(customStyle)}`} /> : null}
{isRandomUserSelectModalOpen ? <RandomUserSelectContainer

View File

@ -251,7 +251,7 @@ class MessageForm extends PureComponent {
stopUserTyping,
} = this.props;
const { message } = this.state;
let msg = message.trim();
const msg = message.trim();
if (msg.length < minMessageLength) return;
@ -291,8 +291,6 @@ class MessageForm extends PureComponent {
<Styled.EmojiPickerWrapper>
<Styled.EmojiPicker
onEmojiSelect={(emojiObject) => this.handleEmojiSelect(emojiObject)}
showPreview={false}
showSkinTones={false}
/>
</Styled.EmojiPickerWrapper>
);

View File

@ -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)``;

View File

@ -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 (
<Picker
emoji=""
onSelect={(emojiObject, event) => 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);

View File

@ -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 (
<Picker
onSelect={(emojiObject, event) => 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);

View File

@ -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,
};

View File

@ -1,27 +0,0 @@
.dropdownContent {
section[class^="emoji-mart-search"] {
display: none !important;
}
section[class^="emoji-mart emoji-mart-light"] {
display: unset;
border: unset;
}
div[class^="emoji-mart-bar"] {
display: none !important;
}
section[aria-label^="Frequently Used"] {
display: none !important;
}
div[class^="emoji-mart-category-label"] {
display: none !important;
}
div[class^="emoji-mart-scroll"] {
overflow-y: unset;
height: unset;
}
ul[class^="emoji-mart-category-list"] {
span {
cursor: pointer !important;
}
}
}

View File

@ -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 <div ref={containerRef} style={containerStyle} />;
};
export default EmojiRain;

View File

@ -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) => <EmojiRain {...props} />;
export default withTracker(() => ({
reactions: UserReaction.find({ meetingId: Auth.meetingID }).fetch(),
}))(EmojiRainContainer);

View File

@ -0,0 +1,9 @@
const getInteractionsButtonCoordenates = () => {
const el = document.getElementById('interactionsButton');
const coordenada = el.getBoundingClientRect();
return coordenada;
};
export default {
getInteractionsButtonCoordenates,
};

View File

@ -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';
@ -363,6 +362,7 @@ class UserListItem extends PureComponent {
label: intl.formatMessage(messages.backTriggerLabel),
onClick: () => this.setState({ showNestedOptions: false }),
icon: 'left_arrow',
divider: true,
},
{
allowed: showNestedOptions && isMeteorConnected && allowedToChangeStatus,
@ -638,20 +638,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
? <Emoji key="hand" emoji={{ id: 'hand' }} {...emojiProps} />
? <em-emoji key={emojiIcons[0].id} native={emojiIcons[0].native} emoji={emojiIcons[0]} {...emojiProps} />
: <Icon iconName={normalizeEmojiName('raiseHand')} />;
} if (user.away === true) {
return isReactionsEnabled
? <Emoji key="away" emoji={{ id: 'clock7' }} {...emojiProps} />
? <em-emoji key="away" native={emojiIcons[1].native} emoji={emojiIcons[1]} {...emojiProps} />
: <Icon iconName={normalizeEmojiName('away')} />;
} if (user.emoji !== 'none' && user.emoji !== 'notAway') {
return <Icon iconName={normalizeEmojiName(user.emoji)} />;

View File

@ -828,6 +828,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",
@ -4395,13 +4405,9 @@
"integrity": "sha512-ZzPqGKghdVzlQJ+qpfE+r6EB321zed7e5JsvHIlMM4zPFF8okXUkF5Of7h7F3l3cltPL0rG7YVmlp5Qro7RQLA=="
},
"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",

View File

@ -32,6 +32,8 @@
"@apollo/client": "^3.7.10",
"@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",
@ -50,7 +52,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",
"fast-json-patch": "^3.1.1",
"fastdom": "^1.0.10",

View File

@ -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.
@ -592,13 +600,25 @@ public:
autoConvertEmoji: true
emojiPicker:
enable: false
frequentEmojiSortOnClick: false
# e.g.: disableEmojis: ['1F595','1F922']
# e.g.: disableEmojis: ['grin','laughing']
disableEmojis: []
allowedElements: ['a', 'code', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'ol', 'ul', 'p', 'strong']
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: