fix(reactions) Batch events before sending

This commit is contained in:
robertpin 2021-07-20 14:31:49 +03:00 committed by GitHub
parent 4276f82c03
commit 251eec19cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 216 additions and 182 deletions

View File

@ -823,8 +823,8 @@
"hangup": "Leave the meeting",
"help": "Help",
"invite": "Invite people",
"joy": "Laugh",
"kick": "Kick participant",
"laugh": "Laugh",
"like": "Thumbs Up",
"lobbyButton": "Enable/disable lobby mode",
"localRecording": "Toggle local recording controls",
@ -892,7 +892,7 @@
"hangup": "Leave the meeting",
"help": "Help",
"invite": "Invite people",
"joy": "Laugh",
"laugh": "Laugh",
"like": "Thumbs Up",
"lobbyButtonDisable": "Disable lobby mode",
"lobbyButtonEnable": "Enable lobby mode",
@ -922,7 +922,7 @@
"raiseYourHand": "Raise your hand",
"reactionBoo": "Send boo reaction",
"reactionClap": "Send clap reaction",
"reactionJoy": "Send laugh reaction",
"reactionLaugh": "Send laugh reaction",
"reactionLike": "Send thumbs up reaction",
"reactionParty": "Send party popper reaction",
"reactionSurprised": "Send surprised reaction",

View File

@ -795,6 +795,23 @@ export function createToolbarEvent(buttonName, attributes = {}) {
};
}
/**
* Creates an event associated with a reaction button being clicked/pressed.
*
* @param {string} buttonName - The identifier of the reaction button which was
* clicked/pressed.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createReactionMenuEvent(buttonName) {
return {
action: 'clicked',
actionSubject: buttonName,
source: 'reaction.button',
type: TYPE_UI
};
}
/**
* Creates an event which indicates that a local track was muted.
*

View File

@ -22,11 +22,9 @@ import {
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { playSound, registerSound, unregisterSound } from '../base/sounds';
import { openDisplayNamePrompt } from '../display-name';
import { ADD_REACTIONS_MESSAGE } from '../reactions/actionTypes';
import {
pushReaction
} from '../reactions/actions.any';
import { REACTIONS } from '../reactions/constants';
import { ADD_REACTION_MESSAGE } from '../reactions/actionTypes';
import { pushReactions } from '../reactions/actions.any';
import { getReactionMessageFromBuffer } from '../reactions/functions.any';
import { endpointMessageReceived } from '../subtitles';
import { showToolbox } from '../toolbox/actions';
import {
@ -158,7 +156,7 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case ADD_REACTIONS_MESSAGE: {
case ADD_REACTION_MESSAGE: {
_handleReceivedMessage(store, {
id: localParticipant.id,
message: action.message,
@ -212,8 +210,6 @@ StateListenerRegistry.register(
* @returns {void}
*/
function _addChatMsgListener(conference, store) {
const reactions = {};
if (store.getState()['features/base/config'].iAmRecorder) {
// We don't register anything on web if we are in iAmRecorder mode
return;
@ -252,30 +248,21 @@ function _addChatMsgListener(conference, store) {
const [ { _id }, eventData ] = args;
if (eventData.name === ENDPOINT_REACTION_NAME) {
reactions[_id] = reactions[_id] ?? {
timeout: null,
message: ''
};
batch(() => {
store.dispatch(pushReaction(eventData.reaction));
store.dispatch(setToolboxVisible(true));
store.dispatch(setToolboxTimeout(
() => store.dispatch(hideToolbox()),
5000)
);
store.dispatch(pushReactions(eventData.reactions));
});
clearTimeout(reactions[_id].timeout);
reactions[_id].message = `${reactions[_id].message}${REACTIONS[eventData.reaction].message}`;
reactions[_id].timeout = setTimeout(() => {
_handleReceivedMessage(store, {
id: _id,
message: reactions[_id].message,
privateMessage: false,
timestamp: eventData.timestamp
}, false);
delete reactions[_id];
}, 500);
_handleReceivedMessage(store, {
id: _id,
message: getReactionMessageFromBuffer(eventData.reactions),
privateMessage: false,
timestamp: eventData.timestamp
}, false);
}
}
});

View File

@ -13,21 +13,31 @@ export const TOGGLE_REACTIONS_VISIBLE = 'TOGGLE_REACTIONS_VISIBLE';
* a new timeout.
*
* {
* type: SET_REACTION_MESSAGE,
* type: ADD_REACTION_BUFFER,
* message: string,
* timeoutID: number
* }
*/
export const SET_REACTIONS_MESSAGE = 'SET_REACTIONS_MESSAGE';
export const ADD_REACTION_BUFFER = 'ADD_REACTION_BUFFER';
/**
* The type of the action which resets the reactions message and timeout.
* The type of the action which sends the reaction buffer and resets it.
*
* {
* type: CLEAR_REACTION_MESSAGE
* type: FLUSH_REACTION_BUFFER
* }
*/
export const CLEAR_REACTIONS_MESSAGE = 'CLEAR_REACTIONS_MESSAGE';
export const FLUSH_REACTION_BUFFER = 'FLUSH_REACTION_BUFFER';
/**
* The type of the action which adds a new reaction message to the chat.
*
* {
* type: ADD_REACTION_MESSAGE,
* message: string,
* }
*/
export const ADD_REACTION_MESSAGE = 'ADD_REACTION_MESSAGE';
/**
* The type of the action which sets the reactions queue.
@ -42,14 +52,9 @@ export const SET_REACTION_QUEUE = 'SET_REACTION_QUEUE';
/**
* The type of the action which signals a send reaction to everyone in the conference.
*/
export const SEND_REACTION = 'SEND_REACTION';
export const SEND_REACTIONS = 'SEND_REACTIONS';
/**
* The type of the action to add a reaction message to the chat.
* The type of action to adds reactions to the queue.
*/
export const ADD_REACTIONS_MESSAGE = 'ADD_REACTIONS_MESSAGE';
/**
* The type of action to add a reaction to the queue.
*/
export const PUSH_REACTION = 'PUSH_REACTION';
export const PUSH_REACTIONS = 'PUSH_REACTIONS';

View File

@ -1,11 +1,11 @@
// @flow
import {
ADD_REACTIONS_MESSAGE,
CLEAR_REACTIONS_MESSAGE,
PUSH_REACTION,
SEND_REACTION,
SET_REACTIONS_MESSAGE,
ADD_REACTION_BUFFER,
ADD_REACTION_MESSAGE,
FLUSH_REACTION_BUFFER,
PUSH_REACTIONS,
SEND_REACTIONS,
SET_REACTION_QUEUE
} from './actionTypes';
import { type ReactionEmojiProps } from './constants';
@ -23,42 +23,6 @@ export function setReactionQueue(value: Array<ReactionEmojiProps>) {
};
}
/**
* Appends the reactions message to the chat and resets the state.
*
* @returns {void}
*/
export function flushReactionsToChat() {
return {
type: CLEAR_REACTIONS_MESSAGE
};
}
/**
* Adds a new reaction to the reactions message.
*
* @param {boolean} value - The new reaction.
* @returns {Object}
*/
export function addReactionsMessage(value: string) {
return {
type: SET_REACTIONS_MESSAGE,
reaction: value
};
}
/**
* Adds a new reaction to the reactions message.
*
* @param {boolean} value - Reaction to be added to queue.
* @returns {Object}
*/
export function pushReaction(value: string) {
return {
type: PUSH_REACTION,
reaction: value
};
}
/**
* Removes a reaction from the queue.
@ -76,33 +40,75 @@ export function removeReaction(uid: number) {
/**
* Sends a reaction message to everyone in the conference.
* Sends the reactions buffer to everyone in the conference.
*
* @param {string} reaction - The reaction to send out.
* @returns {{
* type: SEND_REACTION,
* type: SEND_REACTION
* }}
*/
export function sendReactions() {
return {
type: SEND_REACTIONS
};
}
/**
* Adds a reaction to the local buffer.
*
* @param {string} reaction - The reaction to be added.
* @returns {{
* type: ADD_REACTION_BUFFER,
* reaction: string
* }}
*/
export function sendReaction(reaction: string) {
export function addReactionToBuffer(reaction: string) {
return {
type: SEND_REACTION,
type: ADD_REACTION_BUFFER,
reaction
};
}
/**
* Adds a reactions message to the chat.
* Clears the reaction buffer.
*
* @param {string} message - The reactions message to add to chat.
* @returns {{
* type: ADD_REACTIONS_MESSAGE,
* type: FLUSH_REACTION_BUFFER
* }}
*/
export function flushReactionBuffer() {
return {
type: FLUSH_REACTION_BUFFER
};
}
/**
* Adds a reaction message to the chat.
*
* @param {string} message - The reaction message.
* @returns {{
* type: ADD_REACTION_MESSAGE,
* message: string
* }}
*/
export function addReactionsMessageToChat(message: string) {
export function addReactionsToChat(message: string) {
return {
type: ADD_REACTIONS_MESSAGE,
type: ADD_REACTION_MESSAGE,
message
};
}
/**
* Adds reactions to the animation queue.
*
* @param {Array} reactions - The reactions to be animated.
* @returns {{
* type: PUSH_REACTIONS,
* reactions: Array
* }}
*/
export function pushReactions(reactions: Array<string>) {
return {
type: PUSH_REACTIONS,
reactions
};
}

View File

@ -4,9 +4,10 @@ import React from 'react';
import { Text, TouchableHighlight } from 'react-native';
import { useDispatch } from 'react-redux';
import { createReactionMenuEvent, sendAnalytics } from '../../../analytics';
import { translate } from '../../../base/i18n';
import type { StyleType } from '../../../base/styles';
import { sendReaction } from '../../actions.any';
import { addReactionToBuffer } from '../../actions.any';
import { REACTIONS } from '../../constants';
@ -78,7 +79,8 @@ function ReactionButton({
* @returns {void}
*/
function _onClick() {
dispatch(sendReaction(reaction));
dispatch(addReactionToBuffer(reaction));
sendAnalytics(createReactionMenuEvent(reaction));
}
return (

View File

@ -4,6 +4,7 @@ import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import {
createReactionMenuEvent,
createToolbarEvent,
sendAnalytics
} from '../../../analytics';
@ -11,7 +12,7 @@ import { translate } from '../../../base/i18n';
import { getLocalParticipant, getParticipantCount, participantUpdated } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { dockToolbox } from '../../../toolbox/actions.web';
import { sendReaction } from '../../actions.any';
import { addReactionToBuffer } from '../../actions.any';
import { toggleReactionsMenuVisibility } from '../../actions.web';
import { REACTIONS } from '../../constants';
@ -144,6 +145,13 @@ class ReactionsMenu extends Component<Props> {
*/
_getReactionButtons() {
const { t, dispatch } = this.props;
let modifierKey = 'Alt';
if (window.navigator?.platform) {
if (window.navigator.platform.indexOf('Mac') !== -1) {
modifierKey = '⌥';
}
}
return Object.keys(REACTIONS).map(key => {
/**
@ -151,17 +159,18 @@ class ReactionsMenu extends Component<Props> {
*
* @returns {void}
*/
function sendMessage() {
dispatch(sendReaction(key));
function doSendReaction() {
dispatch(addReactionToBuffer(key));
sendAnalytics(createReactionMenuEvent(key));
}
return (<ReactionButton
accessibilityLabel = { t(`toolbar.accessibilityLabel.${key}`) }
icon = { REACTIONS[key].emoji }
key = { key }
onClick = { sendMessage }
onClick = { doSendReaction }
toggled = { false }
tooltip = { t(`toolbar.${key}`) } />);
tooltip = { `${t(`toolbar.${key}`)} (${modifierKey} + ${REACTIONS[key].shortcutChar})` } />);
});
}

View File

@ -1,15 +1,14 @@
// @flow
import React, { useEffect } from 'react';
import React from 'react';
import { translate } from '../../../base/i18n';
import { IconRaisedHand } from '../../../base/icons';
import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import ToolbarButton from '../../../toolbox/components/web/ToolbarButton';
import { sendReaction } from '../../actions.any';
import { toggleReactionsMenuVisibility } from '../../actions.web';
import { REACTIONS, type ReactionEmojiProps } from '../../constants';
import { type ReactionEmojiProps } from '../../constants';
import { getReactionsQueue } from '../../functions.any';
import { getReactionsMenuVisibility } from '../../functions.web';
@ -65,32 +64,6 @@ function ReactionsMenuButton({
dispatch
}: Props) {
useEffect(() => {
const KEYBOARD_SHORTCUTS = Object.keys(REACTIONS).map(key => {
return {
character: REACTIONS[key].shortcutChar,
exec: () => dispatch(sendReaction(key)),
helpDescription: t(`toolbar.reaction${key.charAt(0).toUpperCase()}${key.slice(1)}`),
altKey: true
};
});
KEYBOARD_SHORTCUTS.forEach(shortcut => {
APP.keyboardshortcut.registerShortcut(
shortcut.character,
null,
shortcut.exec,
shortcut.helpDescription,
shortcut.altKey);
});
return () => {
Object.keys(REACTIONS).map(key => REACTIONS[key].shortcutChar)
.forEach(letter =>
APP.keyboardshortcut.unregisterShortcut(letter, true));
};
}, []);
/**
* Toggles the reactions menu visibility.
*

View File

@ -11,7 +11,7 @@ export const REACTIONS = {
emoji: '👏',
shortcutChar: 'C'
},
joy: {
laugh: {
message: ':grinning_face:',
emoji: '😀',
shortcutChar: 'L'

View File

@ -1,5 +1,7 @@
// @flow
import uuid from 'uuid';
import { getLocalParticipant } from '../base/participants';
import { extractFqnFromPath } from '../dynamic-branding/functions';
@ -17,28 +19,28 @@ export function getReactionsQueue(state: Object) {
}
/**
* Returns reaction key from the reaction message.
* Returns chat message from reactions buffer.
*
* @param {string} message - The reaction message.
* @param {Array} buffer - The reactions buffer.
* @returns {string}
*/
export function getReactionKeyByMessage(message: string): ?string {
return Object.keys(REACTIONS).find(key => REACTIONS[key].message === `:${message}:`);
export function getReactionMessageFromBuffer(buffer: Array<string>) {
return buffer.map(reaction => REACTIONS[reaction].message).reduce((acc, val) => `${acc}${val}`);
}
/**
* Gets reactions key array from concatenated message.
* Returns reactions array with uid.
*
* @param {string} message - The reaction message.
* @param {Array} buffer - The reactions buffer.
* @returns {Array}
*/
export function messageToKeyArray(message: string) {
let formattedMessage = message.replace(/::/g, '-');
formattedMessage = formattedMessage.replace(/:/g, '');
const messageArray = formattedMessage.split('-');
return messageArray.map<?string>(getReactionKeyByMessage);
export function getReactionsWithId(buffer: Array<string>) {
return buffer.map<Object>(reaction => {
return {
reaction,
uid: uuid.v4()
};
});
}
/**

View File

@ -1,24 +1,25 @@
// @flow
import { batch } from 'react-redux';
import { ENDPOINT_REACTION_NAME } from '../../../modules/API/constants';
import { MiddlewareRegistry } from '../base/redux';
import { isVpaasMeeting } from '../jaas/functions';
import {
SET_REACTIONS_MESSAGE,
CLEAR_REACTIONS_MESSAGE,
SEND_REACTION,
PUSH_REACTION
ADD_REACTION_BUFFER,
FLUSH_REACTION_BUFFER,
SEND_REACTIONS,
PUSH_REACTIONS
} from './actionTypes';
import {
addReactionsMessage,
addReactionsMessageToChat,
flushReactionsToChat,
pushReaction,
addReactionsToChat,
flushReactionBuffer,
pushReactions,
sendReactions,
setReactionQueue
} from './actions.any';
import { REACTIONS } from './constants';
import { messageToKeyArray, sendReactionsWebhook } from './functions.any';
import { getReactionMessageFromBuffer, getReactionsWithId, sendReactionsWebhook } from './functions.any';
declare var APP: Object;
@ -34,56 +35,57 @@ MiddlewareRegistry.register(store => next => action => {
const { dispatch, getState } = store;
switch (action.type) {
case SET_REACTIONS_MESSAGE: {
const { timeoutID, message } = getState()['features/reactions'];
case ADD_REACTION_BUFFER: {
const { timeoutID, buffer } = getState()['features/reactions'];
const { reaction } = action;
clearTimeout(timeoutID);
action.message = `${message}${reaction}`;
buffer.push(reaction);
action.buffer = buffer;
action.timeoutID = setTimeout(() => {
dispatch(flushReactionsToChat());
dispatch(flushReactionBuffer());
}, 500);
break;
}
case CLEAR_REACTIONS_MESSAGE: {
case FLUSH_REACTION_BUFFER: {
const state = getState();
const { message } = state['features/reactions'];
const { buffer } = state['features/reactions'];
batch(() => {
dispatch(sendReactions());
dispatch(addReactionsToChat(getReactionMessageFromBuffer(buffer)));
dispatch(pushReactions(buffer));
});
if (isVpaasMeeting(state)) {
sendReactionsWebhook(state, messageToKeyArray(message));
sendReactionsWebhook(state, buffer);
}
dispatch(addReactionsMessageToChat(message));
break;
}
case SEND_REACTION: {
const state = store.getState();
case SEND_REACTIONS: {
const state = getState();
const { buffer } = state['features/reactions'];
const { conference } = state['features/base/conference'];
if (conference) {
conference.sendEndpointMessage('', {
name: ENDPOINT_REACTION_NAME,
reaction: action.reaction,
reactions: buffer,
timestamp: Date.now()
});
dispatch(addReactionsMessage(REACTIONS[action.reaction].message));
dispatch(pushReaction(action.reaction));
}
break;
}
case PUSH_REACTION: {
case PUSH_REACTIONS: {
const queue = store.getState()['features/reactions'].queue;
const reaction = action.reaction;
const reactions = action.reactions;
dispatch(setReactionQueue([ ...queue, {
reaction,
uid: window.Date.now()
} ]));
dispatch(setReactionQueue([ ...queue, ...getReactionsWithId(reactions) ]));
}
}

View File

@ -4,9 +4,9 @@ import { ReducerRegistry } from '../base/redux';
import {
TOGGLE_REACTIONS_VISIBLE,
SET_REACTIONS_MESSAGE,
CLEAR_REACTIONS_MESSAGE,
SET_REACTION_QUEUE
SET_REACTION_QUEUE,
ADD_REACTION_BUFFER,
FLUSH_REACTION_BUFFER
} from './actionTypes';
/**
@ -30,11 +30,11 @@ function _getInitialState() {
visible: false,
/**
* A string that contains the message to be added to the chat.
* An array that contains the reactions buffer to be sent.
*
* @type {string}
* @type {Array}
*/
message: '',
buffer: [],
/**
* A number, non-zero value which identifies the timer created by a call
@ -64,17 +64,17 @@ ReducerRegistry.register(
visible: !state.visible
};
case SET_REACTIONS_MESSAGE:
case ADD_REACTION_BUFFER:
return {
...state,
message: action.message,
buffer: action.buffer,
timeoutID: action.timeoutID
};
case CLEAR_REACTIONS_MESSAGE:
case FLUSH_REACTION_BUFFER:
return {
...state,
message: '',
buffer: [],
timeoutID: null
};

View File

@ -36,7 +36,9 @@ import {
} from '../../../participants-pane/actions';
import ParticipantsPaneButton from '../../../participants-pane/components/ParticipantsPaneButton';
import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
import { addReactionToBuffer } from '../../../reactions/actions.any';
import { ReactionsMenuButton } from '../../../reactions/components';
import { REACTIONS } from '../../../reactions/constants';
import {
LiveStreamButton,
RecordButton
@ -268,7 +270,7 @@ class Toolbox extends Component<Props> {
* @returns {void}
*/
componentDidMount() {
const { _toolbarButtons } = this.props;
const { _toolbarButtons, t, dispatch } = this.props;
const KEYBOARD_SHORTCUTS = [
isToolbarButtonEnabled('videoquality', _toolbarButtons) && {
character: 'A',
@ -316,6 +318,31 @@ class Toolbox extends Component<Props> {
shortcut.helpDescription);
}
});
const REACTION_SHORTCUTS = Object.keys(REACTIONS).map(key => {
const onShortcutSendReaction = () => {
dispatch(addReactionToBuffer(key));
sendAnalytics(createShortcutEvent(
`reaction.${key}`
));
};
return {
character: REACTIONS[key].shortcutChar,
exec: onShortcutSendReaction,
helpDescription: t(`toolbar.reaction${key.charAt(0).toUpperCase()}${key.slice(1)}`),
altKey: true
};
});
REACTION_SHORTCUTS.forEach(shortcut => {
APP.keyboardshortcut.registerShortcut(
shortcut.character,
null,
shortcut.exec,
shortcut.helpDescription,
shortcut.altKey);
});
}
/**
@ -346,6 +373,10 @@ class Toolbox extends Component<Props> {
componentWillUnmount() {
[ 'A', 'C', 'D', 'R', 'S' ].forEach(letter =>
APP.keyboardshortcut.unregisterShortcut(letter));
Object.keys(REACTIONS).map(key => REACTIONS[key].shortcutChar)
.forEach(letter =>
APP.keyboardshortcut.unregisterShortcut(letter, true));
}
/**