fix(reactions) Reactions improvements (#9964)
* Register shortcuts on mount * Changed icon for reactions menu * Enable reactions by default * Fix unreadCount bug When having unread messages and sending a reaction the unread count now shows the correct count * Fix overflow menu bottom color when reactions are enabled * Revert raise hand icon * Update raise hand functionality On desktop show raise button with arrow for reactions. Only show raise hand in the reactions menu on mobile * Fix lint error Add required prop to ToolboxButtonWithIcon * Legacy support for enable reactions If disableReactions is undefined treat it as true * Remove unnecessary code * Fix unread counter showing negative count * Fix unreadCount with reactions UnreadCount ignores all reactions messages * Fixed typo * Fix background color
This commit is contained in:
parent
5f5cac0e01
commit
584ec7c82e
|
@ -77,8 +77,8 @@ var config = {
|
||||||
// Disables moderator indicators.
|
// Disables moderator indicators.
|
||||||
// disableModeratorIndicator: false,
|
// disableModeratorIndicator: false,
|
||||||
|
|
||||||
// Enables reactions feature.
|
// Disables the reactions feature.
|
||||||
// enableReactions: false,
|
// disableReactions: true,
|
||||||
|
|
||||||
// Disables polls feature.
|
// Disables polls feature.
|
||||||
// disablePolls: false,
|
// disablePolls: false,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
.reactions-menu {
|
.reactions-menu {
|
||||||
width: 280px;
|
width: 280px;
|
||||||
background: #292929;
|
background: $menuBG;
|
||||||
box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25);
|
box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
|
@ -100,6 +100,7 @@ export default [
|
||||||
'disableNS',
|
'disableNS',
|
||||||
'disablePolls',
|
'disablePolls',
|
||||||
'disableProfile',
|
'disableProfile',
|
||||||
|
'disableReactions',
|
||||||
'disableRecordAudioNotification',
|
'disableRecordAudioNotification',
|
||||||
'disableRemoteControl',
|
'disableRemoteControl',
|
||||||
'disableRemoteMute',
|
'disableRemoteMute',
|
||||||
|
@ -127,7 +128,6 @@ export default [
|
||||||
'enableLayerSuspension',
|
'enableLayerSuspension',
|
||||||
'enableLipSync',
|
'enableLipSync',
|
||||||
'enableOpusRed',
|
'enableOpusRed',
|
||||||
'enableReactions',
|
|
||||||
'enableRemb',
|
'enableRemb',
|
||||||
'enableSaveLogs',
|
'enableSaveLogs',
|
||||||
'enableScreenshotCapture',
|
'enableScreenshotCapture',
|
||||||
|
|
|
@ -145,6 +145,7 @@ class BottomSheet extends PureComponent<Props> {
|
||||||
renderHeader
|
renderHeader
|
||||||
? _styles.sheetHeader
|
? _styles.sheetHeader
|
||||||
: _styles.sheet,
|
: _styles.sheet,
|
||||||
|
renderFooter && _styles.sheetFooter,
|
||||||
style,
|
style,
|
||||||
{
|
{
|
||||||
maxHeight: _height - 100
|
maxHeight: _height - 100
|
||||||
|
@ -154,7 +155,10 @@ class BottomSheet extends PureComponent<Props> {
|
||||||
<ScrollView
|
<ScrollView
|
||||||
bounces = { false }
|
bounces = { false }
|
||||||
showsVerticalScrollIndicator = { false }
|
showsVerticalScrollIndicator = { false }
|
||||||
style = { addScrollViewPadding && styles.scrollView } >
|
style = { [
|
||||||
|
renderFooter && _styles.sheet,
|
||||||
|
addScrollViewPadding && styles.scrollView
|
||||||
|
] } >
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
{ renderFooter && renderFooter() }
|
{ renderFooter && renderFooter() }
|
||||||
|
|
|
@ -213,6 +213,13 @@ ColorSchemeRegistry.register('BottomSheet', {
|
||||||
*/
|
*/
|
||||||
sheetHeader: {
|
sheetHeader: {
|
||||||
backgroundColor: BaseTheme.palette.ui02
|
backgroundColor: BaseTheme.palette.ui02
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bottom sheet's background color with footer.
|
||||||
|
*/
|
||||||
|
sheetFooter: {
|
||||||
|
backgroundColor: BaseTheme.palette.bottomSheet
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,8 @@ import {
|
||||||
* "error" or "local" or "remote".
|
* "error" or "local" or "remote".
|
||||||
* @param {string} messageDetails.timestamp - A timestamp to display for when
|
* @param {string} messageDetails.timestamp - A timestamp to display for when
|
||||||
* the message was received.
|
* the message was received.
|
||||||
|
* @param {string} messageDetails.isReaction - Whether or not the
|
||||||
|
* message is a reaction message.
|
||||||
* @returns {{
|
* @returns {{
|
||||||
* type: ADD_MESSAGE,
|
* type: ADD_MESSAGE,
|
||||||
* displayName: string,
|
* displayName: string,
|
||||||
|
@ -29,6 +31,7 @@ import {
|
||||||
* message: string,
|
* message: string,
|
||||||
* messageType: string,
|
* messageType: string,
|
||||||
* timestamp: string,
|
* timestamp: string,
|
||||||
|
* isReaction: boolean
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
export function addMessage(messageDetails: Object) {
|
export function addMessage(messageDetails: Object) {
|
||||||
|
|
|
@ -69,14 +69,30 @@ export function getUnreadCount(state: Object) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let reactionMessages = 0;
|
||||||
|
|
||||||
if (navigator.product === 'ReactNative') {
|
if (navigator.product === 'ReactNative') {
|
||||||
// React native stores the messages in a reversed order.
|
// React native stores the messages in a reversed order.
|
||||||
return messages.indexOf(lastReadMessage);
|
const lastReadIndex = messages.indexOf(lastReadMessage);
|
||||||
|
|
||||||
|
for (let i = 0; i < lastReadIndex; i++) {
|
||||||
|
if (messages[i].isReaction) {
|
||||||
|
reactionMessages++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastReadIndex - reactionMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastReadIndex = messages.lastIndexOf(lastReadMessage);
|
const lastReadIndex = messages.lastIndexOf(lastReadMessage);
|
||||||
|
|
||||||
return messagesCount - (lastReadIndex + 1);
|
for (let i = lastReadIndex + 1; i < messagesCount; i++) {
|
||||||
|
if (messages[i].isReaction) {
|
||||||
|
reactionMessages++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return messagesCount - (lastReadIndex + 1) - reactionMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -68,7 +68,12 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ADD_MESSAGE:
|
case ADD_MESSAGE:
|
||||||
unreadCount = action.hasRead ? 0 : getUnreadCount(getState()) + 1;
|
unreadCount = getUnreadCount(getState());
|
||||||
|
if (action.isReaction) {
|
||||||
|
action.hasRead = false;
|
||||||
|
} else {
|
||||||
|
unreadCount = action.hasRead ? 0 : unreadCount + 1;
|
||||||
|
}
|
||||||
isOpen = getState()['features/chat'].isOpen;
|
isOpen = getState()['features/chat'].isOpen;
|
||||||
|
|
||||||
if (typeof APP !== 'undefined') {
|
if (typeof APP !== 'undefined') {
|
||||||
|
@ -171,7 +176,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
message: action.message,
|
message: action.message,
|
||||||
privateMessage: false,
|
privateMessage: false,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
}, false);
|
}, false, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,7 +275,7 @@ function _addChatMsgListener(conference, store) {
|
||||||
message: getReactionMessageFromBuffer(eventData.reactions),
|
message: getReactionMessageFromBuffer(eventData.reactions),
|
||||||
privateMessage: false,
|
privateMessage: false,
|
||||||
timestamp: eventData.timestamp
|
timestamp: eventData.timestamp
|
||||||
}, false);
|
}, false, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -304,11 +309,13 @@ function _handleChatError({ dispatch }, error) {
|
||||||
* @param {Store} store - The Redux store.
|
* @param {Store} store - The Redux store.
|
||||||
* @param {Object} message - The message object.
|
* @param {Object} message - The message object.
|
||||||
* @param {boolean} shouldPlaySound - Whether or not to play the incoming message sound.
|
* @param {boolean} shouldPlaySound - Whether or not to play the incoming message sound.
|
||||||
|
* @param {boolean} isReaction - Whether or not the message is a reaction message.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function _handleReceivedMessage({ dispatch, getState },
|
function _handleReceivedMessage({ dispatch, getState },
|
||||||
{ id, message, privateMessage, timestamp },
|
{ id, message, privateMessage, timestamp },
|
||||||
shouldPlaySound = true
|
shouldPlaySound = true,
|
||||||
|
isReaction = false
|
||||||
) {
|
) {
|
||||||
// Logic for all platforms:
|
// Logic for all platforms:
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
@ -337,7 +344,8 @@ function _handleReceivedMessage({ dispatch, getState },
|
||||||
message,
|
message,
|
||||||
privateMessage,
|
privateMessage,
|
||||||
recipient: getParticipantDisplayName(state, localParticipant.id),
|
recipient: getParticipantDisplayName(state, localParticipant.id),
|
||||||
timestamp: millisecondsTimestamp
|
timestamp: millisecondsTimestamp,
|
||||||
|
isReaction
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (typeof APP !== 'undefined') {
|
if (typeof APP !== 'undefined') {
|
||||||
|
|
|
@ -28,6 +28,7 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
||||||
displayName: action.displayName,
|
displayName: action.displayName,
|
||||||
error: action.error,
|
error: action.error,
|
||||||
id: action.id,
|
id: action.id,
|
||||||
|
isReaction: action.isReaction,
|
||||||
messageType: action.messageType,
|
messageType: action.messageType,
|
||||||
message: action.message,
|
message: action.message,
|
||||||
privateMessage: action.privateMessage,
|
privateMessage: action.privateMessage,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
createToolbarEvent,
|
createToolbarEvent,
|
||||||
sendAnalytics
|
sendAnalytics
|
||||||
} from '../../../analytics';
|
} from '../../../analytics';
|
||||||
|
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
import { getLocalParticipant, participantUpdated } from '../../../base/participants';
|
import { getLocalParticipant, participantUpdated } from '../../../base/participants';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
|
@ -21,34 +22,39 @@ import ReactionButton from './ReactionButton';
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used for translation.
|
* Docks the toolbox
|
||||||
*/
|
*/
|
||||||
t: Function,
|
_dockToolbox: Function,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the local participant's hand is raised.
|
* Whether or not it's a mobile browser.
|
||||||
*/
|
*/
|
||||||
_raisedHand: boolean,
|
_isMobile: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ID of the local participant.
|
* The ID of the local participant.
|
||||||
*/
|
*/
|
||||||
_localParticipantID: String,
|
_localParticipantID: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the local participant's hand is raised.
|
||||||
|
*/
|
||||||
|
_raisedHand: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Redux Dispatch function.
|
* The Redux Dispatch function.
|
||||||
*/
|
*/
|
||||||
dispatch: Function,
|
dispatch: Function,
|
||||||
|
|
||||||
/**
|
|
||||||
* Docks the toolbox
|
|
||||||
*/
|
|
||||||
_dockToolbox: Function,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not it's displayed in the overflow menu.
|
* Whether or not it's displayed in the overflow menu.
|
||||||
*/
|
*/
|
||||||
overflowMenu: boolean
|
overflowMenu: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for translation.
|
||||||
|
*/
|
||||||
|
t: Function
|
||||||
};
|
};
|
||||||
|
|
||||||
declare var APP: Object;
|
declare var APP: Object;
|
||||||
|
@ -177,25 +183,27 @@ class ReactionsMenu extends Component<Props> {
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { _raisedHand, t, overflowMenu } = this.props;
|
const { _raisedHand, t, overflowMenu, _isMobile } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className = { `reactions-menu ${overflowMenu ? 'overflow' : ''}` }>
|
<div className = { `reactions-menu ${overflowMenu ? 'overflow' : ''}` }>
|
||||||
<div className = 'reactions-row'>
|
<div className = 'reactions-row'>
|
||||||
{ this._getReactionButtons() }
|
{ this._getReactionButtons() }
|
||||||
</div>
|
</div>
|
||||||
<div className = 'raise-hand-row'>
|
{_isMobile && (
|
||||||
<ReactionButton
|
<div className = 'raise-hand-row'>
|
||||||
accessibilityLabel = { t('toolbar.accessibilityLabel.raiseHand') }
|
<ReactionButton
|
||||||
icon = '✋'
|
accessibilityLabel = { t('toolbar.accessibilityLabel.raiseHand') }
|
||||||
key = 'raisehand'
|
icon = '✋'
|
||||||
label = {
|
key = 'raisehand'
|
||||||
`${t(`toolbar.${_raisedHand ? 'lowerYourHand' : 'raiseYourHand'}`)}
|
label = {
|
||||||
${overflowMenu ? '' : ' (R)'}`
|
`${t(`toolbar.${_raisedHand ? 'lowerYourHand' : 'raiseYourHand'}`)}
|
||||||
}
|
${overflowMenu ? '' : ' (R)'}`
|
||||||
onClick = { this._onToolbarToggleRaiseHand }
|
}
|
||||||
toggled = { true } />
|
onClick = { this._onToolbarToggleRaiseHand }
|
||||||
</div>
|
toggled = { true } />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -212,6 +220,7 @@ function mapStateToProps(state) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_localParticipantID: localParticipant.id,
|
_localParticipantID: localParticipant.id,
|
||||||
|
_isMobile: isMobileBrowser(),
|
||||||
_raisedHand: localParticipant.raisedHand
|
_raisedHand: localParticipant.raisedHand
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,14 +2,16 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
import { IconRaisedHand } from '../../../base/icons';
|
import { IconArrowUp, IconRaisedHand } from '../../../base/icons';
|
||||||
import { getLocalParticipant } from '../../../base/participants';
|
import { getLocalParticipant } from '../../../base/participants';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
|
import { ToolboxButtonWithIcon } from '../../../base/toolbox/components';
|
||||||
import ToolbarButton from '../../../toolbox/components/web/ToolbarButton';
|
import ToolbarButton from '../../../toolbox/components/web/ToolbarButton';
|
||||||
import { toggleReactionsMenuVisibility } from '../../actions.web';
|
import { toggleReactionsMenuVisibility } from '../../actions.web';
|
||||||
import { type ReactionEmojiProps } from '../../constants';
|
import { type ReactionEmojiProps } from '../../constants';
|
||||||
import { getReactionsQueue } from '../../functions.any';
|
import { getReactionsQueue, isReactionsEnabled } from '../../functions.any';
|
||||||
import { getReactionsMenuVisibility } from '../../functions.web';
|
import { getReactionsMenuVisibility } from '../../functions.web';
|
||||||
|
|
||||||
import ReactionEmoji from './ReactionEmoji';
|
import ReactionEmoji from './ReactionEmoji';
|
||||||
|
@ -18,34 +20,44 @@ import ReactionsMenuPopup from './ReactionsMenuPopup';
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used for translation.
|
* Whether or not reactions are enabled.
|
||||||
*/
|
*/
|
||||||
t: Function,
|
_reactionsEnabled: Boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the local participant's hand is raised.
|
* Redux dispatch function.
|
||||||
*/
|
*/
|
||||||
raisedHand: boolean,
|
dispatch: Function,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Click handler for the reaction button. Toggles the reactions menu.
|
* Click handler for raise hand functionality.
|
||||||
*/
|
*/
|
||||||
onReactionsClick: Function,
|
handleClick: Function,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the reactions menu is open.
|
* Whether or not the reactions menu is open.
|
||||||
*/
|
*/
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not it's a mobile browser.
|
||||||
|
*/
|
||||||
|
isMobile: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the local participant's hand is raised.
|
||||||
|
*/
|
||||||
|
raisedHand: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The array of reactions to be displayed.
|
* The array of reactions to be displayed.
|
||||||
*/
|
*/
|
||||||
reactionsQueue: Array<ReactionEmojiProps>,
|
reactionsQueue: Array<ReactionEmojiProps>,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redux dispatch function.
|
* Used for translation.
|
||||||
*/
|
*/
|
||||||
dispatch: Function
|
t: Function
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,11 +69,14 @@ declare var APP: Object;
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
function ReactionsMenuButton({
|
function ReactionsMenuButton({
|
||||||
t,
|
_reactionsEnabled,
|
||||||
raisedHand,
|
dispatch,
|
||||||
|
handleClick,
|
||||||
isOpen,
|
isOpen,
|
||||||
|
isMobile,
|
||||||
|
raisedHand,
|
||||||
reactionsQueue,
|
reactionsQueue,
|
||||||
dispatch
|
t
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -73,16 +88,32 @@ function ReactionsMenuButton({
|
||||||
dispatch(toggleReactionsMenuVisibility());
|
dispatch(toggleReactionsMenuVisibility());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const raiseHandButton = (<ToolbarButton
|
||||||
|
accessibilityLabel = { t('toolbar.accessibilityLabel.raiseHand') }
|
||||||
|
icon = { IconRaisedHand }
|
||||||
|
key = 'raise-hand'
|
||||||
|
onClick = { handleClick }
|
||||||
|
toggled = { raisedHand }
|
||||||
|
tooltip = { t('toolbar.raiseHand') } />);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className = 'reactions-menu-popup-container'>
|
<div className = 'reactions-menu-popup-container'>
|
||||||
<ReactionsMenuPopup>
|
<ReactionsMenuPopup>
|
||||||
<ToolbarButton
|
{!_reactionsEnabled || isMobile ? raiseHandButton
|
||||||
accessibilityLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
|
: (
|
||||||
icon = { IconRaisedHand }
|
<ToolboxButtonWithIcon
|
||||||
key = 'reactions'
|
ariaControls = 'reactions-menu-dialog'
|
||||||
onClick = { toggleReactionsMenu }
|
ariaExpanded = { isOpen }
|
||||||
toggled = { raisedHand }
|
ariaHasPopup = { true }
|
||||||
tooltip = { t(`toolbar.${isOpen ? 'closeReactionsMenu' : 'openReactionsMenu'}`) } />
|
ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
|
||||||
|
icon = { IconArrowUp }
|
||||||
|
iconDisabled = { false }
|
||||||
|
iconId = 'reactions-menu-button'
|
||||||
|
iconTooltip = { t(`toolbar.${isOpen ? 'closeReactionsMenu' : 'openReactionsMenu'}`) }
|
||||||
|
onIconClick = { toggleReactionsMenu }>
|
||||||
|
{raiseHandButton}
|
||||||
|
</ToolboxButtonWithIcon>
|
||||||
|
)}
|
||||||
</ReactionsMenuPopup>
|
</ReactionsMenuPopup>
|
||||||
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
|
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
|
||||||
index = { index }
|
index = { index }
|
||||||
|
@ -103,7 +134,9 @@ function mapStateToProps(state) {
|
||||||
const localParticipant = getLocalParticipant(state);
|
const localParticipant = getLocalParticipant(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
_reactionsEnabled: isReactionsEnabled(state),
|
||||||
isOpen: getReactionsMenuVisibility(state),
|
isOpen: getReactionsMenuVisibility(state),
|
||||||
|
isMobile: isMobileBrowser(),
|
||||||
reactionsQueue: getReactionsQueue(state),
|
reactionsQueue: getReactionsQueue(state),
|
||||||
raisedHand: localParticipant?.raisedHand
|
raisedHand: localParticipant?.raisedHand
|
||||||
};
|
};
|
||||||
|
|
|
@ -151,11 +151,11 @@ export function getReactionsSoundsThresholds(reactions: Array<string>) {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function isReactionsEnabled(state: Object) {
|
export function isReactionsEnabled(state: Object) {
|
||||||
const { enableReactions } = state['features/base/config'];
|
const { disableReactions } = state['features/base/config'];
|
||||||
|
|
||||||
if (navigator.product === 'ReactNative') {
|
if (navigator.product === 'ReactNative') {
|
||||||
return enableReactions && getFeatureFlag(state, REACTIONS_ENABLED, true);
|
return !disableReactions && getFeatureFlag(state, REACTIONS_ENABLED, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return enableReactions;
|
return !disableReactions;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,11 @@ declare var APP: Object;
|
||||||
export type Props = {
|
export type Props = {
|
||||||
...$Exact<AbstractDialogTabProps>,
|
...$Exact<AbstractDialogTabProps>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the reactions feature is enabled.
|
||||||
|
*/
|
||||||
|
enableReactions: Boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the sound for the incoming message should play.
|
* Whether or not the sound for the incoming message should play.
|
||||||
*/
|
*/
|
||||||
|
@ -40,11 +45,6 @@ export type Props = {
|
||||||
*/
|
*/
|
||||||
soundsReactions: Boolean,
|
soundsReactions: Boolean,
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the reactions feature is enabled.
|
|
||||||
*/
|
|
||||||
enableReactions: Boolean,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked to obtain translated strings.
|
* Invoked to obtain translated strings.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
import { toState } from '../base/redux';
|
import { toState } from '../base/redux';
|
||||||
import { parseStandardURIString } from '../base/util';
|
import { parseStandardURIString } from '../base/util';
|
||||||
import { isFollowMeActive } from '../follow-me';
|
import { isFollowMeActive } from '../follow-me';
|
||||||
|
import { isReactionsEnabled } from '../reactions/functions.any';
|
||||||
|
|
||||||
import { SS_DEFAULT_FRAME_RATE, SS_SUPPORTED_FRAMERATES } from './constants';
|
import { SS_DEFAULT_FRAME_RATE, SS_SUPPORTED_FRAMERATES } from './constants';
|
||||||
|
|
||||||
|
@ -174,7 +175,7 @@ export function getSoundsTabProps(stateful: Object | Function) {
|
||||||
soundsTalkWhileMuted,
|
soundsTalkWhileMuted,
|
||||||
soundsReactions
|
soundsReactions
|
||||||
} = state['features/base/settings'];
|
} = state['features/base/settings'];
|
||||||
const { enableReactions } = state['features/base/config'];
|
const enableReactions = isReactionsEnabled(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
soundsIncomingMessage,
|
soundsIncomingMessage,
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
// @flow
|
|
||||||
|
|
||||||
import { translate } from '../../../base/i18n';
|
|
||||||
import { IconRaisedHand } from '../../../base/icons';
|
|
||||||
import { getLocalParticipant } from '../../../base/participants';
|
|
||||||
import { connect } from '../../../base/redux';
|
|
||||||
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
|
|
||||||
|
|
||||||
type Props = AbstractButtonProps & {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the local participant's hand is raised.
|
|
||||||
*/
|
|
||||||
_raisedHand: boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of a button for toggling raise hand functionality.
|
|
||||||
*/
|
|
||||||
class RaiseHandButton extends AbstractButton<Props, *> {
|
|
||||||
accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
|
|
||||||
icon = IconRaisedHand
|
|
||||||
label = 'toolbar.raiseYourHand';
|
|
||||||
toggledLabel = 'toolbar.lowerYourHand'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves tooltip dynamically.
|
|
||||||
*/
|
|
||||||
get tooltip() {
|
|
||||||
return this.props._raisedHand ? 'toolbar.lowerYourHand' : 'toolbar.raiseYourHand';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Required by linter due to AbstractButton overwritten prop being writable.
|
|
||||||
*
|
|
||||||
* @param {string} value - The value.
|
|
||||||
*/
|
|
||||||
set tooltip(value) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles clicking / pressing the button, and opens the appropriate dialog.
|
|
||||||
*
|
|
||||||
* @protected
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
_handleClick() {
|
|
||||||
const { handleClick } = this.props;
|
|
||||||
|
|
||||||
if (handleClick) {
|
|
||||||
handleClick();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether this button is in toggled state or not.
|
|
||||||
*
|
|
||||||
* @override
|
|
||||||
* @protected
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
_isToggled() {
|
|
||||||
return this.props._raisedHand;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function that maps parts of Redux state tree into component props.
|
|
||||||
*
|
|
||||||
* @param {Object} state - Redux state.
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
const localParticipant = getLocalParticipant(state);
|
|
||||||
|
|
||||||
return {
|
|
||||||
_raisedHand: localParticipant.raisedHand
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default translate(connect(mapStateToProps)(RaiseHandButton));
|
|
|
@ -17,7 +17,6 @@ import { translate } from '../../../base/i18n';
|
||||||
import JitsiMeetJS from '../../../base/lib-jitsi-meet';
|
import JitsiMeetJS from '../../../base/lib-jitsi-meet';
|
||||||
import {
|
import {
|
||||||
getLocalParticipant,
|
getLocalParticipant,
|
||||||
getParticipantCount,
|
|
||||||
haveParticipantWithScreenSharingFeature,
|
haveParticipantWithScreenSharingFeature,
|
||||||
raiseHand
|
raiseHand
|
||||||
} from '../../../base/participants';
|
} from '../../../base/participants';
|
||||||
|
@ -87,7 +86,6 @@ import AudioSettingsButton from './AudioSettingsButton';
|
||||||
import FullscreenButton from './FullscreenButton';
|
import FullscreenButton from './FullscreenButton';
|
||||||
import OverflowMenuButton from './OverflowMenuButton';
|
import OverflowMenuButton from './OverflowMenuButton';
|
||||||
import ProfileButton from './ProfileButton';
|
import ProfileButton from './ProfileButton';
|
||||||
import RaiseHandButton from './RaiseHandButton';
|
|
||||||
import Separator from './Separator';
|
import Separator from './Separator';
|
||||||
import ShareDesktopButton from './ShareDesktopButton';
|
import ShareDesktopButton from './ShareDesktopButton';
|
||||||
import ToggleCameraButton from './ToggleCameraButton';
|
import ToggleCameraButton from './ToggleCameraButton';
|
||||||
|
@ -180,11 +178,6 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
_overflowMenuVisible: boolean,
|
_overflowMenuVisible: boolean,
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of participants in the conference.
|
|
||||||
*/
|
|
||||||
_participantCount: number,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the participants pane is open.
|
* Whether or not the participants pane is open.
|
||||||
*/
|
*/
|
||||||
|
@ -254,16 +247,12 @@ type Props = {
|
||||||
|
|
||||||
declare var APP: Object;
|
declare var APP: Object;
|
||||||
|
|
||||||
type State = {
|
|
||||||
reactionsShortcutsRegistered: boolean
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements the conference toolbox on React/Web.
|
* Implements the conference toolbox on React/Web.
|
||||||
*
|
*
|
||||||
* @extends Component
|
* @extends Component
|
||||||
*/
|
*/
|
||||||
class Toolbox extends Component<Props, State> {
|
class Toolbox extends Component<Props> {
|
||||||
/**
|
/**
|
||||||
* Initializes a new {@code Toolbox} instance.
|
* Initializes a new {@code Toolbox} instance.
|
||||||
*
|
*
|
||||||
|
@ -273,10 +262,6 @@ class Toolbox extends Component<Props, State> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
|
||||||
reactionsShortcutsRegistered: false
|
|
||||||
};
|
|
||||||
|
|
||||||
// Bind event handlers so they are only bound once per instance.
|
// Bind event handlers so they are only bound once per instance.
|
||||||
this._onMouseOut = this._onMouseOut.bind(this);
|
this._onMouseOut = this._onMouseOut.bind(this);
|
||||||
this._onMouseOver = this._onMouseOver.bind(this);
|
this._onMouseOver = this._onMouseOver.bind(this);
|
||||||
|
@ -306,7 +291,7 @@ class Toolbox extends Component<Props, State> {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { _toolbarButtons, t, dispatch, _reactionsEnabled, _participantCount } = this.props;
|
const { _toolbarButtons, t, dispatch, _reactionsEnabled } = this.props;
|
||||||
const KEYBOARD_SHORTCUTS = [
|
const KEYBOARD_SHORTCUTS = [
|
||||||
isToolbarButtonEnabled('videoquality', _toolbarButtons) && {
|
isToolbarButtonEnabled('videoquality', _toolbarButtons) && {
|
||||||
character: 'A',
|
character: 'A',
|
||||||
|
@ -355,7 +340,7 @@ class Toolbox extends Component<Props, State> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (_reactionsEnabled && _participantCount > 1) {
|
if (_reactionsEnabled) {
|
||||||
const REACTION_SHORTCUTS = Object.keys(REACTIONS).map(key => {
|
const REACTION_SHORTCUTS = Object.keys(REACTIONS).map(key => {
|
||||||
const onShortcutSendReaction = () => {
|
const onShortcutSendReaction = () => {
|
||||||
dispatch(addReactionToBuffer(key));
|
dispatch(addReactionToBuffer(key));
|
||||||
|
@ -389,7 +374,7 @@ class Toolbox extends Component<Props, State> {
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { _dialog, _reactionsEnabled, _participantCount, dispatch, t } = this.props;
|
const { _dialog, dispatch } = this.props;
|
||||||
|
|
||||||
|
|
||||||
if (prevProps._overflowMenuVisible
|
if (prevProps._overflowMenuVisible
|
||||||
|
@ -398,41 +383,6 @@ class Toolbox extends Component<Props, State> {
|
||||||
this._onSetOverflowVisible(false);
|
this._onSetOverflowVisible(false);
|
||||||
dispatch(setToolbarHovered(false));
|
dispatch(setToolbarHovered(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.state.reactionsShortcutsRegistered
|
|
||||||
&& (prevProps._reactionsEnabled !== _reactionsEnabled
|
|
||||||
|| prevProps._participantCount !== _participantCount)) {
|
|
||||||
if (_reactionsEnabled && _participantCount > 1) {
|
|
||||||
// eslint-disable-next-line react/no-did-update-set-state
|
|
||||||
this.setState({
|
|
||||||
reactionsShortcutsRegistered: true
|
|
||||||
});
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -445,7 +395,7 @@ class Toolbox extends Component<Props, State> {
|
||||||
[ 'A', 'C', 'D', 'R', 'S' ].forEach(letter =>
|
[ 'A', 'C', 'D', 'R', 'S' ].forEach(letter =>
|
||||||
APP.keyboardshortcut.unregisterShortcut(letter));
|
APP.keyboardshortcut.unregisterShortcut(letter));
|
||||||
|
|
||||||
if (this.props._reactionsEnabled && this.state.reactionsShortcutsRegistered) {
|
if (this.props._reactionsEnabled) {
|
||||||
Object.keys(REACTIONS).map(key => REACTIONS[key].shortcutChar)
|
Object.keys(REACTIONS).map(key => REACTIONS[key].shortcutChar)
|
||||||
.forEach(letter =>
|
.forEach(letter =>
|
||||||
APP.keyboardshortcut.unregisterShortcut(letter, true));
|
APP.keyboardshortcut.unregisterShortcut(letter, true));
|
||||||
|
@ -613,8 +563,7 @@ class Toolbox extends Component<Props, State> {
|
||||||
const {
|
const {
|
||||||
_feedbackConfigured,
|
_feedbackConfigured,
|
||||||
_isMobile,
|
_isMobile,
|
||||||
_screenSharing,
|
_screenSharing
|
||||||
_reactionsEnabled
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const microphone = {
|
const microphone = {
|
||||||
|
@ -651,8 +600,8 @@ class Toolbox extends Component<Props, State> {
|
||||||
|
|
||||||
const raisehand = {
|
const raisehand = {
|
||||||
key: 'raisehand',
|
key: 'raisehand',
|
||||||
Content: _reactionsEnabled ? ReactionsMenuButton : RaiseHandButton,
|
Content: ReactionsMenuButton,
|
||||||
handleClick: _reactionsEnabled ? null : this._onToolbarToggleRaiseHand,
|
handleClick: this._onToolbarToggleRaiseHand,
|
||||||
group: 2
|
group: 2
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1388,7 +1337,6 @@ function _mapStateToProps(state, ownProps) {
|
||||||
_localParticipantID: localParticipant?.id,
|
_localParticipantID: localParticipant?.id,
|
||||||
_localVideo: localVideo,
|
_localVideo: localVideo,
|
||||||
_overflowMenuVisible: overflowMenuVisible,
|
_overflowMenuVisible: overflowMenuVisible,
|
||||||
_participantCount: getParticipantCount(state),
|
|
||||||
_participantsPaneOpen: getParticipantsPaneOpen(state),
|
_participantsPaneOpen: getParticipantsPaneOpen(state),
|
||||||
_raisedHand: localParticipant?.raisedHand,
|
_raisedHand: localParticipant?.raisedHand,
|
||||||
_reactionsEnabled: isReactionsEnabled(state),
|
_reactionsEnabled: isReactionsEnabled(state),
|
||||||
|
|
Loading…
Reference in New Issue