feat(reactions) Added Reactions (#9465)
* Created desktop reactions menu Moved raise hand functionality to reactions menu * Added reactions to chat * Added animations * Added reactions to the web mobile version Redesigned the overflow menu. Added the reactions menu and reactions animations * Make toolbar visible on animation start * Bug fix * Cleanup * Fixed overflow menu desktop * Revert mobile menu changes * Removed unused CSS * Fixed iOS safari issue * Fixed overflow issue on mobile * Added keyboard shortcuts for reactions * Disabled double tap zoom on reaction buttons * Refactored actions * Updated option symbol for keyboard shortcuts * Actions refactor * Refactor * Fixed linting errors * Updated BottomSheet * Added reactions on native * Code cleanup * Code review refactor * Color fix * Hide reactions on one participant * Removed console log * Lang fix * Update schortcuts
This commit is contained in:
parent
8db3a341b3
commit
601ee219e7
|
@ -100,12 +100,18 @@
|
|||
}
|
||||
|
||||
.audio-preview > div:nth-child(2),
|
||||
.video-preview > div:nth-child(2) {
|
||||
.video-preview > div:nth-child(2),
|
||||
.reactions-menu-popup > div:nth-child(2) {
|
||||
margin-bottom: 4px;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.reactions-menu-popup > div:nth-child(2) {
|
||||
margin-bottom: 6px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* The following selectors keep the chat modal full-size anywhere between 100px
|
||||
* and 580px for desktop or 680px for mobile.
|
||||
|
|
|
@ -4,17 +4,16 @@
|
|||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: $drawerZ;
|
||||
background-color: #141414;
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
.drawer-menu {
|
||||
max-height: 50vh;
|
||||
max-height: calc(80vh - 64px);
|
||||
background: #242528;
|
||||
border-radius: 16px 16px 0 0;
|
||||
overflow-y: auto;
|
||||
|
||||
&.expanded {
|
||||
max-height: 80vh;
|
||||
}
|
||||
overflow-y: hidden;
|
||||
margin-bottom: env(safe-area-inset-bottom, 0);
|
||||
|
||||
.drawer-toggle {
|
||||
display: flex;
|
||||
|
@ -42,6 +41,8 @@
|
|||
font-size: 1.2em;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
height: calc(80vh - 144px - 64px);
|
||||
overflow-y: auto;
|
||||
|
||||
.overflow-menu-item {
|
||||
box-sizing: border-box;
|
||||
|
|
|
@ -0,0 +1,189 @@
|
|||
@use 'sass:math';
|
||||
|
||||
.reactions-menu {
|
||||
width: 280px;
|
||||
background: #292929;
|
||||
box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 3px;
|
||||
padding: 16px;
|
||||
|
||||
&.overflow {
|
||||
width: auto;
|
||||
padding-bottom: max(env(safe-area-inset-bottom, 0), 16px);
|
||||
background-color: #141414;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
position: relative;
|
||||
|
||||
.toolbox-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
span.emoji {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.reactions-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
|
||||
.toolbox-button {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
|
||||
span.emoji {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.reactions-row {
|
||||
.toolbox-button {
|
||||
margin-right: 8px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.toolbox-button:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.raise-hand-row {
|
||||
margin-top: 16px;
|
||||
|
||||
.toolbox-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toolbox-icon {
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
span.text {
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reactions-animations-container {
|
||||
position: absolute;
|
||||
width: 20%;
|
||||
bottom: 0;
|
||||
left: 40%;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.reactions-menu-popup-container,
|
||||
.reactions-menu-popup {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
$reactionCount: 20;
|
||||
|
||||
@function random($min, $max) {
|
||||
@return math.random() * ($max - $min) + $min;
|
||||
}
|
||||
|
||||
.reaction-emoji {
|
||||
position: absolute;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
top: 32px;
|
||||
left: 10px;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
|
||||
&.reaction-0 {
|
||||
animation: flowToRight 5s forwards ease-in-out;
|
||||
}
|
||||
|
||||
@for $i from 1 through $reactionCount {
|
||||
&.reaction-#{$i} {
|
||||
animation: animation-#{$i} 5s forwards ease-in-out;
|
||||
top: #{random(50, 0)}px;
|
||||
left: #{random(-10, 10)}px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flowToRight {
|
||||
0% {
|
||||
transform: translate(0px, 0px) scale(0.6);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: translate(40px, -70vh) scale(1.5);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translate(40px, -70vh) scale(1.5);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(140px, -50vh) scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin animation-list {
|
||||
@for $i from 1 through $reactionCount {
|
||||
$topX: random(-100, 100);
|
||||
$topY: random(65, 75);
|
||||
$bottomX: random(150, 200);
|
||||
$bottomY: random(40, 50);
|
||||
|
||||
@if $topX < 0 {
|
||||
$bottomX: -$bottomX;
|
||||
}
|
||||
|
||||
@keyframes animation-#{$i} {
|
||||
0% {
|
||||
transform: translate(0, 0) scale(0.6);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: translate(#{$topX}px, -#{$topY}vh) scale(1.5);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translate(#{$topX}px, -#{$topY}vh) scale(1.5);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(#{$bottomX}px, -#{$bottomY}vh) scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include animation-list;
|
|
@ -105,11 +105,14 @@
|
|||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
pointer-events: all;
|
||||
background-color: #131519;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||
box-shadow: 0px 2px 8px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.toolbox-content-items {
|
||||
background: $newToolbarBackgroundColor;
|
||||
box-shadow: 0px 2px 8px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 6px;
|
||||
margin: 0 auto;
|
||||
padding: 6px;
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
flex-direction: column-reverse;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: ($desktopAppDragBarHeight - 5px) 5px 10px;
|
||||
padding: ($desktopAppDragBarHeight - 5px) 5px calc(env(safe-area-inset-bottom, 0) + 10px);
|
||||
/**
|
||||
* fixed positioning is necessary for remote menus and tooltips to pop
|
||||
* out of the scrolling filmstrip. AtlasKit dialogs and tooltips use
|
||||
|
|
|
@ -106,6 +106,7 @@ $flagsImagePath: "../images/";
|
|||
@import 'connection-status';
|
||||
@import 'drawer';
|
||||
@import 'participants-pane';
|
||||
@import 'reactions-menu';
|
||||
@import 'plan-limit';
|
||||
|
||||
/* Modules END */
|
||||
|
|
|
@ -808,6 +808,7 @@
|
|||
"callQuality": "Manage video quality",
|
||||
"cc": "Toggle subtitles",
|
||||
"chat": "Open / Close chat",
|
||||
"clap": "Clap",
|
||||
"document": "Toggle shared document",
|
||||
"download": "Download our apps",
|
||||
"embedMeeting": "Embed meeting",
|
||||
|
@ -817,7 +818,9 @@
|
|||
"hangup": "Leave the meeting",
|
||||
"help": "Help",
|
||||
"invite": "Invite people",
|
||||
"joy": "Laughing Crying",
|
||||
"kick": "Kick participant",
|
||||
"like": "Thumbs Up",
|
||||
"lobbyButton": "Enable/disable lobby mode",
|
||||
"localRecording": "Toggle local recording controls",
|
||||
"lockRoom": "Toggle meeting password",
|
||||
|
@ -830,10 +833,12 @@
|
|||
"muteEveryonesVideo": "Disable everyone's camera",
|
||||
"muteEveryoneElsesVideo": "Disable everyone else's camera",
|
||||
"participants": "Participants",
|
||||
"party": "Party Popper",
|
||||
"pip": "Toggle Picture-in-Picture mode",
|
||||
"privateMessage": "Send private message",
|
||||
"profile": "Edit your profile",
|
||||
"raiseHand": "Raise / Lower your hand",
|
||||
"reactionsMenu": "Open / Close reactions menu",
|
||||
"recording": "Toggle recording",
|
||||
"remoteMute": "Mute participant",
|
||||
"remoteVideoMute": "Disable camera of participant",
|
||||
|
@ -845,7 +850,9 @@
|
|||
"shareYourScreen": "Start / Stop sharing your screen",
|
||||
"shortcuts": "Toggle shortcuts",
|
||||
"show": "Show on stage",
|
||||
"smile": "Smile",
|
||||
"speakerStats": "Toggle speaker statistics",
|
||||
"surprised": "Surprised",
|
||||
"tileView": "Toggle tile view",
|
||||
"toggleCamera": "Toggle camera",
|
||||
"toggleFilmstrip": "Toggle filmstrip",
|
||||
|
@ -864,7 +871,9 @@
|
|||
"authenticate": "Authenticate",
|
||||
"callQuality": "Manage video quality",
|
||||
"chat": "Open / Close chat",
|
||||
"clap": "Clap",
|
||||
"closeChat": "Close chat",
|
||||
"closeReactionsMenu": "Close reactions menu",
|
||||
"documentClose": "Close shared document",
|
||||
"documentOpen": "Open shared document",
|
||||
"download": "Download our apps",
|
||||
|
@ -878,6 +887,8 @@
|
|||
"hangup": "Leave the meeting",
|
||||
"help": "Help",
|
||||
"invite": "Invite people",
|
||||
"joy": "Joy",
|
||||
"like": "Thumbs Up",
|
||||
"lobbyButtonDisable": "Disable lobby mode",
|
||||
"lobbyButtonEnable": "Enable lobby mode",
|
||||
"login": "Login",
|
||||
|
@ -896,18 +907,27 @@
|
|||
"noisyAudioInputTitle": "Your microphone appears to be noisy!",
|
||||
"noisyAudioInputDesc": "It sounds like your microphone is making noise, please consider muting or changing the device.",
|
||||
"openChat": "Open chat",
|
||||
"openReactionsMenu": "Open reactions menu",
|
||||
"participants": "Participants",
|
||||
"party": "Celebration",
|
||||
"pip": "Enter Picture-in-Picture mode",
|
||||
"privateMessage": "Send private message",
|
||||
"profile": "Edit your profile",
|
||||
"raiseHand": "Raise / Lower your hand",
|
||||
"raiseYourHand": "Raise your hand",
|
||||
"reactionClap": "Send clap reaction",
|
||||
"reactionJoy": "Send joy reaction",
|
||||
"reactionLike": "Send thumbs up reaction",
|
||||
"reactionParty": "Send party popper reaction",
|
||||
"reactionSmile": "Send smile reaction",
|
||||
"reactionSurprised": "Send surprised reaction",
|
||||
"security": "Security options",
|
||||
"Settings": "Settings",
|
||||
"shareaudio": "Share audio",
|
||||
"sharedvideo": "Share video",
|
||||
"shareRoom": "Invite someone",
|
||||
"shortcuts": "View shortcuts",
|
||||
"smile": "Smile",
|
||||
"speakerStats": "Speaker stats",
|
||||
"startScreenSharing": "Start screen sharing",
|
||||
"startSubtitles": "Start subtitles",
|
||||
|
@ -915,6 +935,7 @@
|
|||
"stopScreenSharing": "Stop screen sharing",
|
||||
"stopSubtitles": "Stop subtitles",
|
||||
"stopSharedVideo": "Stop video",
|
||||
"surprised": "Surprised",
|
||||
"talkWhileMutedPopup": "Trying to speak? You are muted.",
|
||||
"tileViewToggle": "Toggle tile view",
|
||||
"toggleCamera": "Toggle camera",
|
||||
|
|
|
@ -15,3 +15,8 @@ export const API_ID = parseURLParams(window.location).jitsi_meet_external_api_id
|
|||
* The payload name for the datachannel/endpoint text message event
|
||||
*/
|
||||
export const ENDPOINT_TEXT_MESSAGE_NAME = 'endpoint-text-message';
|
||||
|
||||
/**
|
||||
* The payload name for the datachannel/endpoint reaction event
|
||||
*/
|
||||
export const ENDPOINT_REACTION_NAME = 'endpoint-reaction';
|
||||
|
|
|
@ -142,20 +142,23 @@ const KeyboardShortcut = {
|
|||
* @param exec the function to be executed when the shortcut is pressed
|
||||
* @param helpDescription the description of the shortcut that would appear
|
||||
* in the help menu
|
||||
* @param altKey whether or not the alt key must be pressed.
|
||||
*/
|
||||
registerShortcut(// eslint-disable-line max-params
|
||||
shortcutChar,
|
||||
shortcutAttr,
|
||||
exec,
|
||||
helpDescription) {
|
||||
_shortcuts.set(shortcutChar, {
|
||||
helpDescription,
|
||||
altKey = false) {
|
||||
_shortcuts.set(altKey ? `:${shortcutChar}` : shortcutChar, {
|
||||
character: shortcutChar,
|
||||
function: exec,
|
||||
shortcutAttr
|
||||
shortcutAttr,
|
||||
altKey
|
||||
});
|
||||
|
||||
if (helpDescription) {
|
||||
this._addShortcutToHelp(shortcutChar, helpDescription);
|
||||
this._addShortcutToHelp(altKey ? `:${shortcutChar}` : shortcutChar, helpDescription);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -164,9 +167,10 @@ const KeyboardShortcut = {
|
|||
*
|
||||
* @param shortcutChar unregisters the given shortcut, which means it will
|
||||
* no longer be usable
|
||||
* @param altKey whether or not shortcut is combo with alt key
|
||||
*/
|
||||
unregisterShortcut(shortcutChar) {
|
||||
_shortcuts.delete(shortcutChar);
|
||||
unregisterShortcut(shortcutChar, altKey = false) {
|
||||
_shortcuts.delete(altKey ? `:${shortcutChar}` : shortcutChar);
|
||||
_shortcutsHelp.delete(shortcutChar);
|
||||
},
|
||||
|
||||
|
@ -175,6 +179,15 @@ const KeyboardShortcut = {
|
|||
* @returns {string} e.key or something close if not supported
|
||||
*/
|
||||
_getKeyboardKey(e) {
|
||||
// If alt is pressed a different char can be returned so this takes
|
||||
// the char from the code. It also prefixes with a colon to differentiate
|
||||
// alt combo from simple keypress.
|
||||
if (e.altKey) {
|
||||
const key = e.code.replace('Key', '');
|
||||
|
||||
return `:${key}`;
|
||||
}
|
||||
|
||||
// If e.key is a string, then it is assumed it already plainly states
|
||||
// the key pressed. This may not be true in all cases, such as with Edge
|
||||
// and "?", when the browser cannot properly map a key press event to a
|
||||
|
|
|
@ -35,6 +35,7 @@ import '../large-video/middleware';
|
|||
import '../lobby/middleware';
|
||||
import '../notifications/middleware';
|
||||
import '../overlay/middleware';
|
||||
import '../reactions/middleware';
|
||||
import '../recent-list/middleware';
|
||||
import '../recording/middleware';
|
||||
import '../rejoin/middleware';
|
||||
|
|
|
@ -41,6 +41,7 @@ import '../large-video/reducer';
|
|||
import '../lobby/reducer';
|
||||
import '../notifications/reducer';
|
||||
import '../overlay/reducer';
|
||||
import '../reactions/reducer';
|
||||
import '../recent-list/reducer';
|
||||
import '../recording/reducer';
|
||||
import '../settings/reducer';
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { type ReactionEmojiProps } from '../../../reactions/constants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link DialogContainer}.
|
||||
*/
|
||||
|
@ -25,7 +27,12 @@ type Props = {
|
|||
/**
|
||||
* True if the UI is in a compact state where we don't show dialogs.
|
||||
*/
|
||||
_reducedUI: boolean
|
||||
_reducedUI: boolean,
|
||||
|
||||
/**
|
||||
* Array of reactions to be displayed.
|
||||
*/
|
||||
_reactionsQueue: Array<ReactionEmojiProps>
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -49,7 +49,17 @@ type Props = {
|
|||
/**
|
||||
* Function to render a bottom sheet header element, if necessary.
|
||||
*/
|
||||
renderHeader: ?Function
|
||||
renderHeader: ?Function,
|
||||
|
||||
/**
|
||||
* Function to render a bottom sheet footer element, if necessary.
|
||||
*/
|
||||
renderFooter: ?Function,
|
||||
|
||||
/**
|
||||
* The height of the screen.
|
||||
*/
|
||||
_height: number
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -80,7 +90,7 @@ class BottomSheet extends PureComponent<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _styles, renderHeader } = this.props;
|
||||
const { _styles, renderHeader, renderFooter, _height } = this.props;
|
||||
|
||||
return (
|
||||
<SlidingView
|
||||
|
@ -99,7 +109,10 @@ class BottomSheet extends PureComponent<Props> {
|
|||
<SafeAreaView
|
||||
style = { [
|
||||
styles.sheetItemContainer,
|
||||
_styles.sheet
|
||||
_styles.sheet,
|
||||
{
|
||||
maxHeight: _height - 100
|
||||
}
|
||||
] }
|
||||
{ ...this.panResponder.panHandlers }>
|
||||
<ScrollView
|
||||
|
@ -108,6 +121,7 @@ class BottomSheet extends PureComponent<Props> {
|
|||
style = { styles.scrollView } >
|
||||
{ this.props.children }
|
||||
</ScrollView>
|
||||
{ renderFooter && renderFooter() }
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</SlidingView>
|
||||
|
@ -167,7 +181,8 @@ class BottomSheet extends PureComponent<Props> {
|
|||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_styles: ColorSchemeRegistry.get(state, 'BottomSheet')
|
||||
_styles: ColorSchemeRegistry.get(state, 'BottomSheet'),
|
||||
_height: state['features/base/responsive-ui'].clientHeight
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import { ReactionEmoji } from '../../../../reactions/components';
|
||||
import { getReactionsQueue } from '../../../../reactions/functions.any';
|
||||
import { connect } from '../../../redux';
|
||||
import AbstractDialogContainer, {
|
||||
abstractMapStateToProps
|
||||
|
@ -11,6 +15,22 @@ import AbstractDialogContainer, {
|
|||
* @extends AbstractDialogContainer
|
||||
*/
|
||||
class DialogContainer extends AbstractDialogContainer {
|
||||
|
||||
/**
|
||||
* Returns the reactions to be displayed.
|
||||
*
|
||||
* @returns {Array<React$Element>}
|
||||
*/
|
||||
_renderReactions() {
|
||||
const { _reactionsQueue } = this.props;
|
||||
|
||||
return _reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
|
||||
index = { index }
|
||||
key = { uid }
|
||||
reaction = { reaction }
|
||||
uid = { uid } />));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
|
@ -18,8 +38,18 @@ class DialogContainer extends AbstractDialogContainer {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return this._renderDialogContent();
|
||||
return (<React.Fragment>
|
||||
{this._renderReactions()}
|
||||
{this._renderDialogContent()}
|
||||
</React.Fragment>);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(abstractMapStateToProps)(DialogContainer);
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
...abstractMapStateToProps(state),
|
||||
_reactionsQueue: getReactionsQueue(state)
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(DialogContainer);
|
||||
|
|
|
@ -33,7 +33,7 @@ export const bottomSheetStyles = {
|
|||
},
|
||||
|
||||
scrollView: {
|
||||
paddingHorizontal: MD_ITEM_MARGIN_PADDING
|
||||
paddingHorizontal: 0
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -117,7 +117,7 @@ const brandedDialogText = {
|
|||
};
|
||||
|
||||
const brandedDialogLabelStyle = {
|
||||
color: schemeColor('text'),
|
||||
color: ColorPalette.white,
|
||||
flexShrink: 1,
|
||||
fontSize: MD_FONT_SIZE,
|
||||
opacity: 0.90
|
||||
|
@ -130,7 +130,7 @@ const brandedDialogItemContainerStyle = {
|
|||
};
|
||||
|
||||
const brandedDialogIconStyle = {
|
||||
color: schemeColor('icon'),
|
||||
color: ColorPalette.white,
|
||||
fontSize: 24
|
||||
};
|
||||
|
||||
|
@ -178,20 +178,24 @@ ColorSchemeRegistry.register('BottomSheet', {
|
|||
* Container style for a generic item rendered in the menu.
|
||||
*/
|
||||
style: {
|
||||
...brandedDialogItemContainerStyle
|
||||
...brandedDialogItemContainerStyle,
|
||||
backgroundColor: ColorPalette.darkBackground,
|
||||
paddingHorizontal: MD_ITEM_MARGIN_PADDING
|
||||
},
|
||||
|
||||
/**
|
||||
* Additional style that is not directly used as a style object.
|
||||
*/
|
||||
underlayColor: ColorPalette.overflowMenuItemUnderlay
|
||||
underlayColor: ColorPalette.toggled
|
||||
},
|
||||
|
||||
/**
|
||||
* Bottom sheet's base style.
|
||||
*/
|
||||
sheet: {
|
||||
backgroundColor: schemeColor('background')
|
||||
backgroundColor: ColorPalette.black,
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
// @flow
|
||||
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
import { ENDPOINT_REACTION_NAME } from '../../../modules/API/constants';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
|
||||
import {
|
||||
CONFERENCE_JOINED,
|
||||
|
@ -19,7 +22,18 @@ 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 { endpointMessageReceived } from '../subtitles';
|
||||
import { showToolbox } from '../toolbox/actions';
|
||||
import {
|
||||
hideToolbox,
|
||||
setToolboxTimeout,
|
||||
setToolboxVisible
|
||||
} from '../toolbox/actions.web';
|
||||
|
||||
import { ADD_MESSAGE, SEND_MESSAGE, OPEN_CHAT, CLOSE_CHAT } from './actionTypes';
|
||||
import { addMessage, clearMessages } from './actions';
|
||||
|
@ -143,6 +157,15 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ADD_REACTIONS_MESSAGE: {
|
||||
_handleReceivedMessage(store, {
|
||||
id: localParticipant.id,
|
||||
message: action.message,
|
||||
privateMessage: false,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
|
@ -189,6 +212,7 @@ 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
|
||||
|
@ -219,6 +243,43 @@ function _addChatMsgListener(conference, store) {
|
|||
}
|
||||
);
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||
(...args) => {
|
||||
store.dispatch(endpointMessageReceived(...args));
|
||||
|
||||
if (args && args.length >= 2) {
|
||||
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)
|
||||
);
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
delete reactions[_id];
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.CONFERENCE_ERROR, (errorType, error) => {
|
||||
errorType === JitsiConferenceErrors.CHAT_ERROR && _handleChatError(store, error);
|
||||
|
|
|
@ -67,6 +67,14 @@ class KeyboardShortcutsDialog extends Component<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderShortcutsListItem(keyboardKey, translationKey) {
|
||||
let modifierKey = 'Alt';
|
||||
|
||||
if (window.navigator?.platform) {
|
||||
if (window.navigator.platform.indexOf('Mac') !== -1) {
|
||||
modifierKey = '⌥';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
className = 'shortcuts-list__item'
|
||||
|
@ -78,7 +86,9 @@ class KeyboardShortcutsDialog extends Component<Props> {
|
|||
</span>
|
||||
<span className = 'item-action'>
|
||||
<Lozenge isBold = { true }>
|
||||
{ keyboardKey }
|
||||
{ keyboardKey.startsWith(':')
|
||||
? `${modifierKey} + ${keyboardKey.slice(1)}`
|
||||
: keyboardKey }
|
||||
</Lozenge>
|
||||
</span>
|
||||
</li>
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* The type of the (redux) action which shows/hides the reactions menu.
|
||||
*
|
||||
* {
|
||||
* type: TOGGLE_REACTIONS_VISIBLE,
|
||||
* visible: boolean
|
||||
* }
|
||||
*/
|
||||
export const TOGGLE_REACTIONS_VISIBLE = 'TOGGLE_REACTIONS_VISIBLE';
|
||||
|
||||
/**
|
||||
* The type of the action which adds a new reaction to the reactions message and sets
|
||||
* a new timeout.
|
||||
*
|
||||
* {
|
||||
* type: SET_REACTION_MESSAGE,
|
||||
* message: string,
|
||||
* timeoutID: number
|
||||
* }
|
||||
*/
|
||||
export const SET_REACTIONS_MESSAGE = 'SET_REACTIONS_MESSAGE';
|
||||
|
||||
/**
|
||||
* The type of the action which resets the reactions message and timeout.
|
||||
*
|
||||
* {
|
||||
* type: CLEAR_REACTION_MESSAGE
|
||||
* }
|
||||
*/
|
||||
export const CLEAR_REACTIONS_MESSAGE = 'CLEAR_REACTIONS_MESSAGE';
|
||||
|
||||
/**
|
||||
* The type of the action which sets the reactions queue.
|
||||
*
|
||||
* {
|
||||
* type: SET_REACTION_QUEUE,
|
||||
* value: Array
|
||||
* }
|
||||
*/
|
||||
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';
|
||||
|
||||
/**
|
||||
* The type of the action to add a reaction message to the chat.
|
||||
*/
|
||||
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';
|
|
@ -0,0 +1,108 @@
|
|||
// @flow
|
||||
|
||||
import {
|
||||
ADD_REACTIONS_MESSAGE,
|
||||
CLEAR_REACTIONS_MESSAGE,
|
||||
PUSH_REACTION,
|
||||
SEND_REACTION,
|
||||
SET_REACTIONS_MESSAGE,
|
||||
SET_REACTION_QUEUE
|
||||
} from './actionTypes';
|
||||
import { type ReactionEmojiProps } from './constants';
|
||||
|
||||
/**
|
||||
* Sets the reaction queue.
|
||||
*
|
||||
* @param {Array} value - The new queue.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setReactionQueue(value: Array<ReactionEmojiProps>) {
|
||||
return {
|
||||
type: SET_REACTION_QUEUE,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {number} uid - Id of the reaction to be removed.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function removeReaction(uid: number) {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const queue = getState()['features/reactions'].queue;
|
||||
|
||||
dispatch(setReactionQueue(queue.filter(reaction => reaction.uid !== uid)));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sends a reaction message to everyone in the conference.
|
||||
*
|
||||
* @param {string} reaction - The reaction to send out.
|
||||
* @returns {{
|
||||
* type: SEND_REACTION,
|
||||
* reaction: string
|
||||
* }}
|
||||
*/
|
||||
export function sendReaction(reaction: string) {
|
||||
return {
|
||||
type: SEND_REACTION,
|
||||
reaction
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a reactions message to the chat.
|
||||
*
|
||||
* @param {string} message - The reactions message to add to chat.
|
||||
* @returns {{
|
||||
* type: ADD_REACTIONS_MESSAGE,
|
||||
* message: string
|
||||
* }}
|
||||
*/
|
||||
export function addReactionsMessageToChat(message: string) {
|
||||
return {
|
||||
type: ADD_REACTIONS_MESSAGE,
|
||||
message
|
||||
};
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// @flow
|
||||
|
||||
import {
|
||||
TOGGLE_REACTIONS_VISIBLE
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Toggles the visibility of the reactions menu.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function toggleReactionsMenuVisibility() {
|
||||
return {
|
||||
type: TOGGLE_REACTIONS_VISIBLE
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './native';
|
|
@ -0,0 +1 @@
|
|||
export * from './web';
|
|
@ -0,0 +1 @@
|
|||
export * from './_';
|
|
@ -0,0 +1,165 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Text, TouchableHighlight, View } from 'react-native';
|
||||
import { type Dispatch } from 'redux';
|
||||
|
||||
import {
|
||||
createToolbarEvent,
|
||||
sendAnalytics
|
||||
} from '../../../analytics';
|
||||
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
raiseHand
|
||||
} from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { type AbstractButtonProps } from '../../../base/toolbox/components';
|
||||
|
||||
import { type ReactionStyles } from './ReactionButton';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link RaiseHandButton}.
|
||||
*/
|
||||
type Props = AbstractButtonProps & {
|
||||
|
||||
/**
|
||||
* The local participant.
|
||||
*/
|
||||
_localParticipant: Object,
|
||||
|
||||
/**
|
||||
* Whether the participant raused their hand or not.
|
||||
*/
|
||||
_raisedHand: boolean,
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
dispatch: Dispatch<any>,
|
||||
|
||||
/**
|
||||
* Used for translation
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* Used to close the overflow menu after raise hand is clicked.
|
||||
*/
|
||||
onCancel: Function,
|
||||
|
||||
/**
|
||||
* Styles for the button.
|
||||
*/
|
||||
_styles: ReactionStyles
|
||||
};
|
||||
|
||||
/**
|
||||
* An implementation of a button to raise or lower hand.
|
||||
*/
|
||||
class RaiseHandButton extends Component<Props, *> {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
|
||||
label = 'toolbar.raiseYourHand';
|
||||
toggledLabel = 'toolbar.lowerYourHand';
|
||||
|
||||
/**
|
||||
* Initializes a new {@code RaiseHandButton} instance.
|
||||
*
|
||||
* @param {Props} props - The React {@code Component} props to initialize
|
||||
* the new {@code RaiseHandButton} instance with.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onClick = this._onClick.bind(this);
|
||||
this._toggleRaisedHand = this._toggleRaisedHand.bind(this);
|
||||
this._getLabel = this._getLabel.bind(this);
|
||||
}
|
||||
|
||||
_onClick: () => void;
|
||||
|
||||
_toggleRaisedHand: () => void;
|
||||
|
||||
_getLabel: () => string;
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
this._toggleRaisedHand();
|
||||
this.props.onCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the rased hand status of the local participant.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_toggleRaisedHand() {
|
||||
const enable = !this.props._raisedHand;
|
||||
|
||||
sendAnalytics(createToolbarEvent('raise.hand', { enable }));
|
||||
|
||||
this.props.dispatch(raiseHand(enable));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current label, taking the toggled state into account. If no
|
||||
* toggled label is provided, the regular label will also be used in the
|
||||
* toggled state.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
_getLabel() {
|
||||
const { _raisedHand, t } = this.props;
|
||||
|
||||
return t(_raisedHand ? this.toggledLabel : this.label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _styles, t } = this.props;
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
accessibilityLabel = { t(this.accessibilityLabel) }
|
||||
accessibilityRole = 'button'
|
||||
onPress = { this._onClick }
|
||||
style = { _styles.style }
|
||||
underlayColor = { _styles.underlayColor }>
|
||||
<View style = { _styles.container }>
|
||||
<Text style = { _styles.emoji }>✋</Text>
|
||||
<Text style = { _styles.text }>{this._getLabel()}</Text>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state): Object {
|
||||
const _localParticipant = getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
_localParticipant,
|
||||
_raisedHand: _localParticipant.raisedHand,
|
||||
_styles: ColorSchemeRegistry.get(state, 'Toolbox').raiseHandButton
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(RaiseHandButton));
|
|
@ -0,0 +1,96 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { Text, TouchableHighlight } from 'react-native';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import type { StyleType } from '../../../base/styles';
|
||||
import { sendReaction } from '../../actions.any';
|
||||
import { REACTIONS } from '../../constants';
|
||||
|
||||
|
||||
export type ReactionStyles = {
|
||||
|
||||
/**
|
||||
* Style for the button.
|
||||
*/
|
||||
style: StyleType,
|
||||
|
||||
/**
|
||||
* Underlay color for the button.
|
||||
*/
|
||||
underlayColor: StyleType,
|
||||
|
||||
/**
|
||||
* Style for the emoji text on the button.
|
||||
*/
|
||||
emoji: StyleType,
|
||||
|
||||
/**
|
||||
* Style for the label text on the button.
|
||||
*/
|
||||
text?: StyleType,
|
||||
|
||||
/**
|
||||
* Style for text container. Used on raise hand button.
|
||||
*/
|
||||
container?: StyleType
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ReactionButton}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Collection of styles for the button.
|
||||
*/
|
||||
styles: ReactionStyles,
|
||||
|
||||
/**
|
||||
* The reaction to be sent
|
||||
*/
|
||||
reaction: string,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* An implementation of a button to send a reaction.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function ReactionButton({
|
||||
styles,
|
||||
reaction,
|
||||
t
|
||||
}: Props) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onClick() {
|
||||
dispatch(sendReaction(reaction));
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
accessibilityLabel = { t(`toolbar.accessibilityLabel.${reaction}`) }
|
||||
accessibilityRole = 'button'
|
||||
onPress = { _onClick }
|
||||
style = { styles.style }
|
||||
underlayColor = { styles.underlayColor }>
|
||||
<Text style = { styles.emoji }>{REACTIONS[reaction].emoji}</Text>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
|
||||
export default translate(ReactionButton);
|
|
@ -0,0 +1,96 @@
|
|||
// @flow
|
||||
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Animated } from 'react-native';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||
import { removeReaction } from '../../actions.any';
|
||||
import { REACTIONS, type ReactionEmojiProps } from '../../constants';
|
||||
|
||||
|
||||
type Props = ReactionEmojiProps & {
|
||||
|
||||
/**
|
||||
* Index of reaction on the queue.
|
||||
* Used to differentiate between first and other animations.
|
||||
*/
|
||||
index: number
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Animated reaction emoji.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function ReactionEmoji({ reaction, uid, index }: Props) {
|
||||
const _styles = useSelector(state => ColorSchemeRegistry.get(state, 'Toolbox'));
|
||||
const _height = useSelector(state => state['features/base/responsive-ui'].clientHeight);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const animationVal = useRef(new Animated.Value(0)).current;
|
||||
|
||||
const vh = useState(_height / 100)[0];
|
||||
|
||||
const randomInt = (min, max) => Math.floor((Math.random() * (max - min + 1)) + min);
|
||||
|
||||
const animationIndex = useMemo(() => index % 21, [ index ]);
|
||||
|
||||
const coordinates = useState({
|
||||
topX: animationIndex === 0 ? 40 : randomInt(-100, 100),
|
||||
topY: animationIndex === 0 ? -70 : randomInt(-65, -75),
|
||||
bottomX: animationIndex === 0 ? 140 : randomInt(150, 200),
|
||||
bottomY: animationIndex === 0 ? -50 : randomInt(-40, -50)
|
||||
})[0];
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => dispatch(removeReaction(uid)), 5000);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
Animated.timing(
|
||||
animationVal,
|
||||
{
|
||||
toValue: 1,
|
||||
duration: 5000,
|
||||
useNativeDriver: true
|
||||
}
|
||||
).start();
|
||||
}, [ animationVal ]);
|
||||
|
||||
|
||||
return (
|
||||
<Animated.Text
|
||||
style = {{
|
||||
..._styles.emojiAnimation,
|
||||
transform: [
|
||||
{ translateY: animationVal.interpolate({
|
||||
inputRange: [ 0, 0.70, 0.75, 1 ],
|
||||
outputRange: [ 0, coordinates.topY * vh, coordinates.topY * vh, coordinates.bottomY * vh ]
|
||||
})
|
||||
}, {
|
||||
translateX: animationVal.interpolate({
|
||||
inputRange: [ 0, 0.70, 0.75, 1 ],
|
||||
outputRange: [ 0, coordinates.topX, coordinates.topX,
|
||||
coordinates.topX < 0 ? -coordinates.bottomX : coordinates.bottomX ]
|
||||
})
|
||||
}, {
|
||||
scale: animationVal.interpolate({
|
||||
inputRange: [ 0, 0.70, 0.75, 1 ],
|
||||
outputRange: [ 0.6, 1.5, 1.5, 1 ]
|
||||
})
|
||||
}
|
||||
],
|
||||
opacity: animationVal.interpolate({
|
||||
inputRange: [ 0, 0.7, 0.75, 1 ],
|
||||
outputRange: [ 1, 1, 1, 0 ]
|
||||
})
|
||||
}}>
|
||||
{REACTIONS[reaction].emoji}
|
||||
</Animated.Text>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReactionEmoji;
|
|
@ -0,0 +1,59 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||
import { getParticipantCount } from '../../../base/participants';
|
||||
import { REACTIONS } from '../../constants';
|
||||
|
||||
import RaiseHandButton from './RaiseHandButton';
|
||||
import ReactionButton from './ReactionButton';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ReactionMenu}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Used to close the overflow menu after raise hand is clicked.
|
||||
*/
|
||||
onCancel: Function,
|
||||
|
||||
/**
|
||||
* Whether or not it's displayed in the overflow menu.
|
||||
*/
|
||||
overflowMenu: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
* Animated reaction emoji.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function ReactionMenu({
|
||||
onCancel,
|
||||
overflowMenu
|
||||
}: Props) {
|
||||
const _styles = useSelector(state => ColorSchemeRegistry.get(state, 'Toolbox'));
|
||||
const _participantCount = useSelector(state => getParticipantCount(state));
|
||||
|
||||
return (
|
||||
<View style = { overflowMenu ? _styles.overflowReactionMenu : _styles.reactionMenu }>
|
||||
{_participantCount > 1
|
||||
&& <View style = { _styles.reactionRow }>
|
||||
{Object.keys(REACTIONS).map(key => (
|
||||
<ReactionButton
|
||||
key = { key }
|
||||
reaction = { key }
|
||||
styles = { _styles.reactionButton } />
|
||||
))}
|
||||
</View>
|
||||
}
|
||||
<RaiseHandButton onCancel = { onCancel } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReactionMenu;
|
|
@ -0,0 +1,143 @@
|
|||
// @flow
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
import { SafeAreaView, TouchableWithoutFeedback, View } from 'react-native';
|
||||
|
||||
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||
import { hideDialog, isDialogOpen } from '../../../base/dialog';
|
||||
import { getParticipantCount } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import type { StyleType } from '../../../base/styles';
|
||||
|
||||
import ReactionMenu from './ReactionMenu';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ReactionMenuDialog}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The color-schemed stylesheet of the feature.
|
||||
*/
|
||||
_styles: StyleType,
|
||||
|
||||
/**
|
||||
* True if the dialog is currently visible, false otherwise.
|
||||
*/
|
||||
_isOpen: boolean,
|
||||
|
||||
/**
|
||||
* The width of the screen.
|
||||
*/
|
||||
_width: number,
|
||||
|
||||
/**
|
||||
* The height of the screen.
|
||||
*/
|
||||
_height: number,
|
||||
|
||||
/**
|
||||
* Number of conference participants.
|
||||
*/
|
||||
_participantCount: number,
|
||||
|
||||
/**
|
||||
* Used for hiding the dialog when the selection was completed.
|
||||
*/
|
||||
dispatch: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* The exported React {@code Component}. We need it to execute
|
||||
* {@link hideDialog}.
|
||||
*
|
||||
* XXX It does not break our coding style rule to not utilize globals for state,
|
||||
* because it is merely another name for {@code export}'s {@code default}.
|
||||
*/
|
||||
let ReactionMenu_; // eslint-disable-line prefer-const
|
||||
|
||||
/**
|
||||
* Implements a React {@code Component} with some extra actions in addition to
|
||||
* those in the toolbar.
|
||||
*/
|
||||
class ReactionMenuDialog extends PureComponent<Props> {
|
||||
/**
|
||||
* Initializes a new {@code ReactionMenuDialog} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _styles, _width, _height, _participantCount } = this.props;
|
||||
|
||||
return (
|
||||
<SafeAreaView style = { _styles }>
|
||||
<TouchableWithoutFeedback
|
||||
onPress = { this._onCancel }>
|
||||
<View style = { _styles }>
|
||||
<View
|
||||
style = {{
|
||||
left: (_width - 360) / 2,
|
||||
top: _height - (_participantCount > 1 ? 144 : 80) - 80
|
||||
}}>
|
||||
<ReactionMenu
|
||||
onCancel = { this._onCancel }
|
||||
overflowMenu = { false } />
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
_onCancel: () => boolean;
|
||||
|
||||
/**
|
||||
* Hides this {@code ReactionMenuDialog}.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onCancel() {
|
||||
if (this.props._isOpen) {
|
||||
this.props.dispatch(hideDialog(ReactionMenu_));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that maps parts of Redux state tree into component props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_isOpen: isDialogOpen(state, ReactionMenu_),
|
||||
_styles: ColorSchemeRegistry.get(state, 'Toolbox').reactionDialog,
|
||||
_width: state['features/base/responsive-ui'].clientWidth,
|
||||
_height: state['features/base/responsive-ui'].clientHeight,
|
||||
_participantCount: getParticipantCount(state)
|
||||
};
|
||||
}
|
||||
|
||||
ReactionMenu_ = connect(_mapStateToProps)(ReactionMenuDialog);
|
||||
|
||||
export default ReactionMenu_;
|
|
@ -2,35 +2,33 @@
|
|||
|
||||
import { type Dispatch } from 'redux';
|
||||
|
||||
import {
|
||||
createToolbarEvent,
|
||||
sendAnalytics
|
||||
} from '../../../analytics';
|
||||
import { isDialogOpen, openDialog } from '../../../base/dialog';
|
||||
import { RAISE_HAND_ENABLED, getFeatureFlag } from '../../../base/flags';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { IconRaisedHand } from '../../../base/icons';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
raiseHand
|
||||
getLocalParticipant
|
||||
} from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
|
||||
|
||||
import ReactionMenuDialog from './ReactionMenuDialog';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link RaiseHandButton}.
|
||||
* The type of the React {@code Component} props of {@link ReactionsMenuButton}.
|
||||
*/
|
||||
type Props = AbstractButtonProps & {
|
||||
|
||||
/**
|
||||
* The local participant.
|
||||
*/
|
||||
_localParticipant: Object,
|
||||
|
||||
/**
|
||||
* Whether the participant raused their hand or not.
|
||||
* Whether the participant raised their hand or not.
|
||||
*/
|
||||
_raisedHand: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the reactions menu is open.
|
||||
*/
|
||||
_reactionsOpen: boolean,
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
|
@ -40,11 +38,11 @@ type Props = AbstractButtonProps & {
|
|||
/**
|
||||
* An implementation of a button to raise or lower hand.
|
||||
*/
|
||||
class RaiseHandButton extends AbstractButton<Props, *> {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
|
||||
class ReactionsMenuButton extends AbstractButton<Props, *> {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.reactionsMenu';
|
||||
icon = IconRaisedHand;
|
||||
label = 'toolbar.raiseYourHand';
|
||||
toggledLabel = 'toolbar.lowerYourHand';
|
||||
label = 'toolbar.openReactionsMenu';
|
||||
toggledLabel = 'toolbar.closeReactionsMenu';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button.
|
||||
|
@ -54,7 +52,7 @@ class RaiseHandButton extends AbstractButton<Props, *> {
|
|||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
this._toggleRaisedHand();
|
||||
this.props.dispatch(openDialog(ReactionMenuDialog));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -65,20 +63,7 @@ class RaiseHandButton extends AbstractButton<Props, *> {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
_isToggled() {
|
||||
return this.props._raisedHand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the rased hand status of the local participant.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_toggleRaisedHand() {
|
||||
const enable = !this.props._raisedHand;
|
||||
|
||||
sendAnalytics(createToolbarEvent('raise.hand', { enable }));
|
||||
|
||||
this.props.dispatch(raiseHand(enable));
|
||||
return this.props._raisedHand || this.props._reactionsOpen;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,10 +81,10 @@ function _mapStateToProps(state, ownProps): Object {
|
|||
const { visible = enabled } = ownProps;
|
||||
|
||||
return {
|
||||
_localParticipant,
|
||||
_raisedHand: _localParticipant.raisedHand,
|
||||
_reactionsOpen: isDialogOpen(state, ReactionMenuDialog),
|
||||
visible
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(RaiseHandButton));
|
||||
export default translate(connect(_mapStateToProps)(ReactionsMenuButton));
|
|
@ -0,0 +1,3 @@
|
|||
export { default as ReactionsMenuButton } from './ReactionsMenuButton';
|
||||
export { default as ReactionEmoji } from './ReactionEmoji';
|
||||
export { default as ReactionMenu } from './ReactionMenu';
|
|
@ -0,0 +1,125 @@
|
|||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Tooltip } from '../../../base/tooltip';
|
||||
import AbstractToolbarButton from '../../../toolbox/components/AbstractToolbarButton';
|
||||
import type { Props as AbstractToolbarButtonProps } from '../../../toolbox/components/AbstractToolbarButton';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ReactionButton}.
|
||||
*/
|
||||
type Props = AbstractToolbarButtonProps & {
|
||||
|
||||
/**
|
||||
* Optional text to display in the tooltip.
|
||||
*/
|
||||
tooltip?: string,
|
||||
|
||||
/**
|
||||
* From which direction the tooltip should appear, relative to the
|
||||
* button.
|
||||
*/
|
||||
tooltipPosition: string,
|
||||
|
||||
/**
|
||||
* Optional label for the button
|
||||
*/
|
||||
label?: string
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a button in the reactions menu.
|
||||
*
|
||||
* @extends AbstractToolbarButton
|
||||
*/
|
||||
class ReactionButton extends AbstractToolbarButton<Props> {
|
||||
/**
|
||||
* Default values for {@code ReactionButton} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
tooltipPosition: 'top'
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code ReactionButton} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onKeyDown = this._onKeyDown.bind(this);
|
||||
}
|
||||
|
||||
_onKeyDown: (Object) => void;
|
||||
|
||||
/**
|
||||
* Handles 'Enter' key on the button to trigger onClick for accessibility.
|
||||
* We should be handling Space onKeyUp but it conflicts with PTT.
|
||||
*
|
||||
* @param {Object} event - The key event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyDown(event) {
|
||||
// If the event coming to the dialog has been subject to preventDefault
|
||||
// we don't handle it here.
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.props.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the button of this {@code ReactionButton}.
|
||||
*
|
||||
* @param {Object} children - The children, if any, to be rendered inside
|
||||
* the button. Presumably, contains the emoji of this {@code ReactionButton}.
|
||||
* @protected
|
||||
* @returns {ReactElement} The button of this {@code ReactionButton}.
|
||||
*/
|
||||
_renderButton(children) {
|
||||
return (
|
||||
<div
|
||||
aria-label = { this.props.accessibilityLabel }
|
||||
aria-pressed = { this.props.toggled }
|
||||
className = 'toolbox-button'
|
||||
onClick = { this.props.onClick }
|
||||
onKeyDown = { this._onKeyDown }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
{ this.props.tooltip
|
||||
? <Tooltip
|
||||
content = { this.props.tooltip }
|
||||
position = { this.props.tooltipPosition }>
|
||||
{ children }
|
||||
</Tooltip>
|
||||
: children }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the icon (emoji) of this {@code reactionButton}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderIcon() {
|
||||
return (
|
||||
<div className = { `toolbox-icon ${this.props.toggled ? 'toggled' : ''}` }>
|
||||
<span className = 'emoji'>{this.props.icon}</span>
|
||||
{this.props.label && <span className = 'text'>{this.props.label}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ReactionButton;
|
|
@ -0,0 +1,96 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { connect } from '../../../base/redux';
|
||||
import { removeReaction } from '../../actions.any';
|
||||
import { REACTIONS } from '../../constants';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Reaction to be displayed.
|
||||
*/
|
||||
reaction: string,
|
||||
|
||||
/**
|
||||
* Id of the reaction.
|
||||
*/
|
||||
uid: Number,
|
||||
|
||||
/**
|
||||
* Removes reaction from redux state.
|
||||
*/
|
||||
removeReaction: Function,
|
||||
|
||||
/**
|
||||
* Index of the reaction in the queue.
|
||||
*/
|
||||
index: number
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* Index of CSS animation. Number between 0-20.
|
||||
*/
|
||||
index: number
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Used to display animated reactions.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
class ReactionEmoji extends Component<Props, State> {
|
||||
/**
|
||||
* Initializes a new {@code ReactionEmoji} instance.
|
||||
*
|
||||
* @param {Props} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
index: props.index % 21
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React Component's componentDidMount.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
setTimeout(() => this.props.removeReaction(this.props.uid), 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { reaction, uid } = this.props;
|
||||
const { index } = this.state;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { `reaction-emoji reaction-${index}` }
|
||||
id = { uid }>
|
||||
{ REACTIONS[reaction].emoji }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
removeReaction
|
||||
};
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps,
|
||||
)(ReactionEmoji);
|
|
@ -0,0 +1,233 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import {
|
||||
createToolbarEvent,
|
||||
sendAnalytics
|
||||
} from '../../../analytics';
|
||||
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 { toggleReactionsMenuVisibility } from '../../actions.web';
|
||||
import { REACTIONS } from '../../constants';
|
||||
|
||||
import ReactionButton from './ReactionButton';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The number of conference participants.
|
||||
*/
|
||||
_participantCount: number,
|
||||
|
||||
/**
|
||||
* Used for translation.
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* Whether or not the local participant's hand is raised.
|
||||
*/
|
||||
_raisedHand: boolean,
|
||||
|
||||
/**
|
||||
* The ID of the local participant.
|
||||
*/
|
||||
_localParticipantID: String,
|
||||
|
||||
/**
|
||||
* The Redux Dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* Docks the toolbox
|
||||
*/
|
||||
_dockToolbox: Function,
|
||||
|
||||
/**
|
||||
* Whether or not it's displayed in the overflow menu.
|
||||
*/
|
||||
overflowMenu: boolean
|
||||
};
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Implements the reactions menu.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
class ReactionsMenu extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code ReactionsMenu} instance.
|
||||
*
|
||||
* @param {Props} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onToolbarToggleRaiseHand = this._onToolbarToggleRaiseHand.bind(this);
|
||||
this._getReactionButtons = this._getReactionButtons.bind(this);
|
||||
}
|
||||
|
||||
_onToolbarToggleRaiseHand: () => void;
|
||||
|
||||
_getReactionButtons: () => Array<React$Element<*>>;
|
||||
|
||||
/**
|
||||
* Implements React Component's componentDidMount.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this.props._dockToolbox(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React Component's componentWillUnmount.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this.props._dockToolbox(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an analytics toolbar event and dispatches an action for toggling
|
||||
* raise hand.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToolbarToggleRaiseHand() {
|
||||
sendAnalytics(createToolbarEvent(
|
||||
'raise.hand',
|
||||
{ enable: !this.props._raisedHand }));
|
||||
this._doToggleRaiseHand();
|
||||
this.props.dispatch(toggleReactionsMenuVisibility());
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to toggle the local participant's raised hand state.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_doToggleRaiseHand() {
|
||||
const { _localParticipantID, _raisedHand } = this.props;
|
||||
const newRaisedStatus = !_raisedHand;
|
||||
|
||||
this.props.dispatch(participantUpdated({
|
||||
// XXX Only the local participant is allowed to update without
|
||||
// stating the JitsiConference instance (i.e. participant property
|
||||
// `conference` for a remote participant) because the local
|
||||
// participant is uniquely identified by the very fact that there is
|
||||
// only one local participant.
|
||||
|
||||
id: _localParticipantID,
|
||||
local: true,
|
||||
raisedHand: newRaisedStatus
|
||||
}));
|
||||
|
||||
APP.API.notifyRaiseHandUpdated(_localParticipantID, newRaisedStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the emoji reaction buttons.
|
||||
*
|
||||
* @returns {Array}
|
||||
*/
|
||||
_getReactionButtons() {
|
||||
const { t, dispatch } = this.props;
|
||||
|
||||
return Object.keys(REACTIONS).map(key => {
|
||||
/**
|
||||
* Sends reaction message.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function sendMessage() {
|
||||
dispatch(sendReaction(key));
|
||||
}
|
||||
|
||||
return (<ReactionButton
|
||||
accessibilityLabel = { t(`toolbar.accessibilityLabel.${key}`) }
|
||||
icon = { REACTIONS[key].emoji }
|
||||
key = { key }
|
||||
onClick = { sendMessage }
|
||||
toggled = { false }
|
||||
tooltip = { t(`toolbar.${key}`) } />);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { _participantCount, _raisedHand, t, overflowMenu } = this.props;
|
||||
|
||||
return (
|
||||
<div className = { `reactions-menu ${overflowMenu ? 'overflow' : ''}` }>
|
||||
{ _participantCount > 1 && <div className = 'reactions-row'>
|
||||
{ this._getReactionButtons() }
|
||||
</div> }
|
||||
<div className = 'raise-hand-row'>
|
||||
<ReactionButton
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.raiseHand') }
|
||||
icon = '✋'
|
||||
key = 'raisehand'
|
||||
label = {
|
||||
`${t(`toolbar.${_raisedHand ? 'lowerYourHand' : 'raiseYourHand'}`)}
|
||||
${overflowMenu ? '' : ' (R)'}`
|
||||
}
|
||||
onClick = { this._onToolbarToggleRaiseHand }
|
||||
toggled = { true } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that maps parts of Redux state tree into component props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
_localParticipantID: localParticipant.id,
|
||||
_raisedHand: localParticipant.raisedHand,
|
||||
_participantCount: getParticipantCount(state)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that maps parts of Redux actions into component props.
|
||||
*
|
||||
* @param {Object} dispatch - Redux dispatch.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
dispatch,
|
||||
...bindActionCreators(
|
||||
{
|
||||
_dockToolbox: dockToolbox
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(ReactionsMenu));
|
|
@ -0,0 +1,139 @@
|
|||
// @flow
|
||||
|
||||
import React, { useEffect } 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 { getReactionsQueue } from '../../functions.any';
|
||||
import { getReactionsMenuVisibility } from '../../functions.web';
|
||||
|
||||
import ReactionEmoji from './ReactionEmoji';
|
||||
import ReactionsMenuPopup from './ReactionsMenuPopup';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Used for translation.
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* Whether or not the local participant's hand is raised.
|
||||
*/
|
||||
raisedHand: boolean,
|
||||
|
||||
/**
|
||||
* Click handler for the reaction button. Toggles the reactions menu.
|
||||
*/
|
||||
onReactionsClick: Function,
|
||||
|
||||
/**
|
||||
* Whether or not the reactions menu is open.
|
||||
*/
|
||||
isOpen: boolean,
|
||||
|
||||
/**
|
||||
* The array of reactions to be displayed.
|
||||
*/
|
||||
reactionsQueue: Array<ReactionEmojiProps>,
|
||||
|
||||
/**
|
||||
* Redux dispatch function.
|
||||
*/
|
||||
dispatch: Function
|
||||
};
|
||||
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Button used for the reactions menu.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function ReactionsMenuButton({
|
||||
t,
|
||||
raisedHand,
|
||||
isOpen,
|
||||
reactionsQueue,
|
||||
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.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function toggleReactionsMenu() {
|
||||
dispatch(toggleReactionsMenuVisibility());
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = 'reactions-menu-popup-container'>
|
||||
<ReactionsMenuPopup>
|
||||
<ToolbarButton
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
|
||||
icon = { IconRaisedHand }
|
||||
key = 'reactions'
|
||||
onClick = { toggleReactionsMenu }
|
||||
toggled = { raisedHand }
|
||||
tooltip = { t(`toolbar.${isOpen ? 'closeReactionsMenu' : 'openReactionsMenu'}`) } />
|
||||
</ReactionsMenuPopup>
|
||||
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
|
||||
index = { index }
|
||||
key = { uid }
|
||||
reaction = { reaction }
|
||||
uid = { uid } />))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that maps parts of Redux state tree into component props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
isOpen: getReactionsMenuVisibility(state),
|
||||
reactionsQueue: getReactionsQueue(state),
|
||||
raisedHand: localParticipant?.raisedHand
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(ReactionsMenuButton));
|
|
@ -0,0 +1,58 @@
|
|||
// @flow
|
||||
|
||||
import InlineDialog from '@atlaskit/inline-dialog';
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { toggleReactionsMenuVisibility } from '../../actions.web';
|
||||
import { getReactionsMenuVisibility } from '../../functions.web';
|
||||
|
||||
import ReactionsMenu from './ReactionsMenu';
|
||||
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Component's children (the reactions menu button).
|
||||
*/
|
||||
children: React$Node
|
||||
}
|
||||
|
||||
/**
|
||||
* Popup with reactions menu.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function ReactionsMenuPopup({
|
||||
children
|
||||
}: Props) {
|
||||
/**
|
||||
* Flag controlling the visibility of the popup.
|
||||
*/
|
||||
const isOpen = useSelector(state => getReactionsMenuVisibility(state));
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
/**
|
||||
* Toggles reactions menu visibility.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function onClose() {
|
||||
dispatch(toggleReactionsMenuVisibility());
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = 'reactions-menu-popup'>
|
||||
<InlineDialog
|
||||
content = { <ReactionsMenu /> }
|
||||
isOpen = { isOpen }
|
||||
onClose = { onClose }
|
||||
placement = 'top'>
|
||||
{children}
|
||||
</InlineDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReactionsMenuPopup;
|
|
@ -0,0 +1,7 @@
|
|||
// @flow
|
||||
|
||||
export { default as ReactionButton } from './ReactionButton';
|
||||
export { default as ReactionEmoji } from './ReactionEmoji';
|
||||
export { default as ReactionsMenu } from './ReactionsMenu';
|
||||
export { default as ReactionsMenuButton } from './ReactionsMenuButton';
|
||||
export { default as ReactionsMenuPopup } from './ReactionsMenuPopup';
|
|
@ -0,0 +1,47 @@
|
|||
// @flow
|
||||
|
||||
export const REACTIONS = {
|
||||
clap: {
|
||||
message: ':clap:',
|
||||
emoji: '👏',
|
||||
shortcutChar: 'C'
|
||||
},
|
||||
like: {
|
||||
message: ':thumbs_up:',
|
||||
emoji: '👍',
|
||||
shortcutChar: 'T'
|
||||
},
|
||||
smile: {
|
||||
message: ':smile:',
|
||||
emoji: '😀',
|
||||
shortcutChar: 'S'
|
||||
},
|
||||
joy: {
|
||||
message: ':joy:',
|
||||
emoji: '😂',
|
||||
shortcutChar: 'L'
|
||||
},
|
||||
surprised: {
|
||||
message: ':face_with_open_mouth:',
|
||||
emoji: '😮',
|
||||
shortcutChar: 'O'
|
||||
},
|
||||
party: {
|
||||
message: ':party_popper:',
|
||||
emoji: '🎉',
|
||||
shortcutChar: 'P'
|
||||
}
|
||||
};
|
||||
|
||||
export type ReactionEmojiProps = {
|
||||
|
||||
/**
|
||||
* Reaction to be displayed.
|
||||
*/
|
||||
reaction: string,
|
||||
|
||||
/**
|
||||
* Id of the reaction.
|
||||
*/
|
||||
uid: number
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// @flow
|
||||
|
||||
/**
|
||||
* Returns the queue of reactions.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getReactionsQueue(state: Object) {
|
||||
return state['features/reactions'].queue;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// @flow
|
||||
|
||||
/**
|
||||
* Returns the visibility state of the reactions menu.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getReactionsMenuVisibility(state: Object) {
|
||||
return state['features/reactions'].visible;
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
// @flow
|
||||
|
||||
import { ENDPOINT_REACTION_NAME } from '../../../modules/API/constants';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
SET_REACTIONS_MESSAGE,
|
||||
CLEAR_REACTIONS_MESSAGE,
|
||||
SEND_REACTION,
|
||||
PUSH_REACTION
|
||||
} from './actionTypes';
|
||||
import {
|
||||
addReactionsMessage,
|
||||
addReactionsMessageToChat,
|
||||
flushReactionsToChat,
|
||||
pushReaction,
|
||||
setReactionQueue
|
||||
} from './actions.any';
|
||||
import { REACTIONS } from './constants';
|
||||
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Middleware which intercepts Reactions actions to handle changes to the
|
||||
* visibility timeout of the Reactions.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const { dispatch, getState } = store;
|
||||
|
||||
switch (action.type) {
|
||||
case SET_REACTIONS_MESSAGE: {
|
||||
const { timeoutID, message } = getState()['features/reactions'];
|
||||
const { reaction } = action;
|
||||
|
||||
clearTimeout(timeoutID);
|
||||
action.message = `${message}${reaction}`;
|
||||
action.timeoutID = setTimeout(() => {
|
||||
dispatch(flushReactionsToChat());
|
||||
}, 500);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case CLEAR_REACTIONS_MESSAGE: {
|
||||
const { message } = getState()['features/reactions'];
|
||||
|
||||
dispatch(addReactionsMessageToChat(message));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SEND_REACTION: {
|
||||
const state = store.getState();
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
if (conference) {
|
||||
conference.sendEndpointMessage('', {
|
||||
name: ENDPOINT_REACTION_NAME,
|
||||
reaction: action.reaction,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
dispatch(addReactionsMessage(REACTIONS[action.reaction].message));
|
||||
dispatch(pushReaction(action.reaction));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case PUSH_REACTION: {
|
||||
const queue = store.getState()['features/reactions'].queue;
|
||||
const reaction = action.reaction;
|
||||
|
||||
dispatch(setReactionQueue([ ...queue, {
|
||||
reaction,
|
||||
uid: window.Date.now()
|
||||
} ]));
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
|
@ -0,0 +1,90 @@
|
|||
// @flow
|
||||
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
TOGGLE_REACTIONS_VISIBLE,
|
||||
SET_REACTIONS_MESSAGE,
|
||||
CLEAR_REACTIONS_MESSAGE,
|
||||
SET_REACTION_QUEUE
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Returns initial state for reactions' part of Redux store.
|
||||
*
|
||||
* @private
|
||||
* @returns {{
|
||||
* visible: boolean,
|
||||
* message: string,
|
||||
* timeoutID: number,
|
||||
* queue: Array
|
||||
* }}
|
||||
*/
|
||||
function _getInitialState() {
|
||||
return {
|
||||
/**
|
||||
* The indicator that determines whether the reactions menu is visible.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
visible: false,
|
||||
|
||||
/**
|
||||
* A string that contains the message to be added to the chat.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
message: '',
|
||||
|
||||
/**
|
||||
* A number, non-zero value which identifies the timer created by a call
|
||||
* to setTimeout().
|
||||
*
|
||||
* @type {number|null}
|
||||
*/
|
||||
timeoutID: null,
|
||||
|
||||
/**
|
||||
* The array of reactions to animate
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
queue: []
|
||||
};
|
||||
}
|
||||
|
||||
ReducerRegistry.register(
|
||||
'features/reactions',
|
||||
(state: Object = _getInitialState(), action: Object) => {
|
||||
switch (action.type) {
|
||||
|
||||
case TOGGLE_REACTIONS_VISIBLE:
|
||||
return {
|
||||
...state,
|
||||
visible: !state.visible
|
||||
};
|
||||
|
||||
case SET_REACTIONS_MESSAGE:
|
||||
return {
|
||||
...state,
|
||||
message: action.message,
|
||||
timeoutID: action.timeoutID
|
||||
};
|
||||
|
||||
case CLEAR_REACTIONS_MESSAGE:
|
||||
return {
|
||||
...state,
|
||||
message: '',
|
||||
timeoutID: null
|
||||
};
|
||||
|
||||
case SET_REACTION_QUEUE: {
|
||||
return {
|
||||
...state,
|
||||
queue: action.value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { IconMenu } from '../../../base/icons';
|
||||
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
|
||||
|
||||
|
||||
type Props = AbstractButtonProps;
|
||||
|
||||
/**
|
||||
* An implementation of a button to show more menu options.
|
||||
*/
|
||||
class MoreOptionsButton extends AbstractButton<Props, any> {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.moreOptions';
|
||||
icon = IconMenu;
|
||||
label = 'toolbar.moreOptions';
|
||||
}
|
||||
|
||||
|
||||
export default translate(MoreOptionsButton);
|
|
@ -1,17 +1,15 @@
|
|||
// @flow
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
import { TouchableOpacity, View } from 'react-native';
|
||||
import Collapsible from 'react-native-collapsible';
|
||||
|
||||
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||
import { BottomSheet, hideDialog, isDialogOpen } from '../../../base/dialog';
|
||||
import { IconDragHandle } from '../../../base/icons';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { StyleType } from '../../../base/styles';
|
||||
import { SharedDocumentButton } from '../../../etherpad';
|
||||
import { InviteButton } from '../../../invite';
|
||||
import { AudioRouteButton } from '../../../mobile/audio-mode';
|
||||
import { ReactionMenu } from '../../../reactions/components';
|
||||
import { LiveStreamButton, RecordButton } from '../../../recording';
|
||||
import SecurityDialogButton from '../../../security/components/security-dialog/SecurityDialogButton';
|
||||
import { SharedVideoButton } from '../../../shared-video/components';
|
||||
|
@ -23,11 +21,8 @@ import MuteEveryoneButton from '../MuteEveryoneButton';
|
|||
import MuteEveryonesVideoButton from '../MuteEveryonesVideoButton';
|
||||
|
||||
import AudioOnlyButton from './AudioOnlyButton';
|
||||
import MoreOptionsButton from './MoreOptionsButton';
|
||||
import RaiseHandButton from './RaiseHandButton';
|
||||
import ScreenSharingButton from './ScreenSharingButton.js';
|
||||
import ToggleCameraButton from './ToggleCameraButton';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link OverflowMenu}.
|
||||
|
@ -65,12 +60,7 @@ type State = {
|
|||
/**
|
||||
* True if the bottom scheet is scrolled to the top.
|
||||
*/
|
||||
scrolledToTop: boolean,
|
||||
|
||||
/**
|
||||
* True if the 'more' button set needas to be rendered.
|
||||
*/
|
||||
showMore: boolean
|
||||
scrolledToTop: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -96,15 +86,12 @@ class OverflowMenu extends PureComponent<Props, State> {
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
scrolledToTop: true,
|
||||
showMore: false
|
||||
scrolledToTop: true
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onSwipe = this._onSwipe.bind(this);
|
||||
this._onToggleMenu = this._onToggleMenu.bind(this);
|
||||
this._renderMenuExpandToggle = this._renderMenuExpandToggle.bind(this);
|
||||
this._renderReactionMenu = this._renderReactionMenu.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -115,7 +102,6 @@ class OverflowMenu extends PureComponent<Props, State> {
|
|||
*/
|
||||
render() {
|
||||
const { _bottomSheetStyles, _width } = this.props;
|
||||
const { showMore } = this.state;
|
||||
const toolbarButtons = getMovableButtons(_width);
|
||||
|
||||
const buttonProps = {
|
||||
|
@ -124,63 +110,45 @@ class OverflowMenu extends PureComponent<Props, State> {
|
|||
styles: _bottomSheetStyles.buttons
|
||||
};
|
||||
|
||||
const moreOptionsButtonProps = {
|
||||
...buttonProps,
|
||||
afterClick: this._onToggleMenu,
|
||||
visible: !showMore
|
||||
const topButtonProps = {
|
||||
afterClick: this._onCancel,
|
||||
showLabel: true,
|
||||
styles: {
|
||||
..._bottomSheetStyles.buttons,
|
||||
style: {
|
||||
..._bottomSheetStyles.buttons.style,
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
paddingTop: 16
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
onCancel = { this._onCancel }
|
||||
onSwipe = { this._onSwipe }
|
||||
renderHeader = { this._renderMenuExpandToggle }>
|
||||
<AudioRouteButton { ...buttonProps } />
|
||||
renderFooter = { toolbarButtons.has('raisehand')
|
||||
? null
|
||||
: this._renderReactionMenu }>
|
||||
<AudioRouteButton { ...topButtonProps } />
|
||||
{!toolbarButtons.has('invite') && <InviteButton { ...buttonProps } />}
|
||||
<AudioOnlyButton { ...buttonProps } />
|
||||
{!toolbarButtons.has('raisehand') && <RaiseHandButton { ...buttonProps } />}
|
||||
<SecurityDialogButton { ...buttonProps } />
|
||||
<ScreenSharingButton { ...buttonProps } />
|
||||
<MoreOptionsButton { ...moreOptionsButtonProps } />
|
||||
<Collapsible collapsed = { !showMore }>
|
||||
{!toolbarButtons.has('togglecamera') && <ToggleCameraButton { ...buttonProps } />}
|
||||
{!toolbarButtons.has('tileview') && <TileViewButton { ...buttonProps } />}
|
||||
<RecordButton { ...buttonProps } />
|
||||
<LiveStreamButton { ...buttonProps } />
|
||||
<SharedVideoButton { ...buttonProps } />
|
||||
<ClosedCaptionButton { ...buttonProps } />
|
||||
<SharedDocumentButton { ...buttonProps } />
|
||||
<MuteEveryoneButton { ...buttonProps } />
|
||||
<MuteEveryonesVideoButton { ...buttonProps } />
|
||||
<HelpButton { ...buttonProps } />
|
||||
</Collapsible>
|
||||
{!toolbarButtons.has('togglecamera') && <ToggleCameraButton { ...buttonProps } />}
|
||||
{!toolbarButtons.has('tileview') && <TileViewButton { ...buttonProps } />}
|
||||
<RecordButton { ...buttonProps } />
|
||||
<LiveStreamButton { ...buttonProps } />
|
||||
<SharedVideoButton { ...buttonProps } />
|
||||
<ClosedCaptionButton { ...buttonProps } />
|
||||
<SharedDocumentButton { ...buttonProps } />
|
||||
<MuteEveryoneButton { ...buttonProps } />
|
||||
<MuteEveryonesVideoButton { ...buttonProps } />
|
||||
<HelpButton { ...buttonProps } />
|
||||
</BottomSheet>
|
||||
);
|
||||
}
|
||||
|
||||
_renderMenuExpandToggle: () => React$Element<any>;
|
||||
|
||||
/**
|
||||
* Function to render the menu toggle in the bottom sheet header area.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderMenuExpandToggle() {
|
||||
return (
|
||||
<View
|
||||
style = { [
|
||||
this.props._bottomSheetStyles.sheet,
|
||||
styles.expandMenuContainer
|
||||
] }>
|
||||
<TouchableOpacity onPress = { this._onToggleMenu }>
|
||||
{ /* $FlowFixMe */ }
|
||||
<IconDragHandle
|
||||
fill = { this.props._bottomSheetStyles.buttons.iconStyle.color } />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_onCancel: () => boolean;
|
||||
|
||||
/**
|
||||
|
@ -199,45 +167,17 @@ class OverflowMenu extends PureComponent<Props, State> {
|
|||
return false;
|
||||
}
|
||||
|
||||
_onSwipe: string => void;
|
||||
_renderReactionMenu: () => React$Element<any>;
|
||||
|
||||
/**
|
||||
* Callback to be invoked when swipe gesture is detected on the menu. Returns true
|
||||
* if the swipe gesture is handled by the menu, false otherwise.
|
||||
* Functoin to render the reaction menu as the footer of the bottom sheet.
|
||||
*
|
||||
* @param {string} direction - Direction of 'up' or 'down'.
|
||||
* @returns {boolean}
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_onSwipe(direction) {
|
||||
const { showMore } = this.state;
|
||||
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
!showMore && this.setState({
|
||||
showMore: true
|
||||
});
|
||||
|
||||
return !showMore;
|
||||
case 'down':
|
||||
showMore && this.setState({
|
||||
showMore: false
|
||||
});
|
||||
|
||||
return showMore;
|
||||
}
|
||||
}
|
||||
|
||||
_onToggleMenu: () => void;
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the expand menu button is pressed.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleMenu() {
|
||||
this.setState({
|
||||
showMore: !this.state.showMore
|
||||
});
|
||||
_renderReactionMenu() {
|
||||
return (<ReactionMenu
|
||||
onCancel = { this._onCancel }
|
||||
overflowMenu = { true } />);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { connect } from '../../../base/redux';
|
|||
import { StyleType } from '../../../base/styles';
|
||||
import { ChatButton } from '../../../chat';
|
||||
import { InviteButton } from '../../../invite';
|
||||
import { ReactionsMenuButton } from '../../../reactions/components';
|
||||
import { TileViewButton } from '../../../video-layout';
|
||||
import { isToolboxVisible, getMovableButtons } from '../../functions.native';
|
||||
import AudioMuteButton from '../AudioMuteButton';
|
||||
|
@ -15,7 +16,6 @@ import HangupButton from '../HangupButton';
|
|||
import VideoMuteButton from '../VideoMuteButton';
|
||||
|
||||
import OverflowMenuButton from './OverflowMenuButton';
|
||||
import RaiseHandButton from './RaiseHandButton';
|
||||
import ToggleCameraButton from './ToggleCameraButton';
|
||||
import styles from './styles';
|
||||
|
||||
|
@ -87,9 +87,9 @@ function Toolbox(props: Props) {
|
|||
toggledStyles = { backgroundToggledStyle } />}
|
||||
|
||||
{ additionalButtons.has('raisehand')
|
||||
&& <RaiseHandButton
|
||||
styles = { buttonStylesBorderless }
|
||||
toggledStyles = { backgroundToggledStyle } />}
|
||||
&& <ReactionsMenuButton
|
||||
styles = { buttonStylesBorderless }
|
||||
toggledStyles = { backgroundToggledStyle } />}
|
||||
{additionalButtons.has('tileview') && <TileViewButton styles = { buttonStylesBorderless } />}
|
||||
{additionalButtons.has('invite') && <InviteButton styles = { buttonStylesBorderless } />}
|
||||
{additionalButtons.has('togglecamera')
|
||||
|
|
|
@ -40,18 +40,38 @@ const whiteToolbarButtonIcon = {
|
|||
color: ColorPalette.white
|
||||
};
|
||||
|
||||
/**
|
||||
* The style of reaction buttons.
|
||||
*/
|
||||
const reactionButton = {
|
||||
...toolbarButton,
|
||||
backgroundColor: 'transparent',
|
||||
alignItems: 'center',
|
||||
marginTop: 0,
|
||||
marginHorizontal: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* The style of the emoji on the reaction buttons.
|
||||
*/
|
||||
const reactionEmoji = {
|
||||
fontSize: 20,
|
||||
color: ColorPalette.white
|
||||
};
|
||||
|
||||
const reactionMenu = {
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: ColorPalette.black,
|
||||
padding: 16
|
||||
};
|
||||
|
||||
/**
|
||||
* The Toolbox and toolbar related styles.
|
||||
*/
|
||||
const styles = {
|
||||
|
||||
expandMenuContainer: {
|
||||
alignItems: 'center',
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
sheetGestureRecognizer: {
|
||||
alignItems: 'stretch',
|
||||
flexDirection: 'column'
|
||||
|
@ -120,6 +140,67 @@ ColorSchemeRegistry.register('Toolbox', {
|
|||
underlayColor: ColorPalette.buttonUnderlay
|
||||
},
|
||||
|
||||
reactionDialog: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
|
||||
overflowReactionMenu: reactionMenu,
|
||||
|
||||
reactionMenu: {
|
||||
...reactionMenu,
|
||||
borderRadius: 3,
|
||||
width: 360
|
||||
},
|
||||
|
||||
reactionRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
marginBottom: 16
|
||||
},
|
||||
|
||||
reactionButton: {
|
||||
style: reactionButton,
|
||||
underlayColor: ColorPalette.toggled,
|
||||
emoji: reactionEmoji
|
||||
},
|
||||
|
||||
raiseHandButton: {
|
||||
style: {
|
||||
...reactionButton,
|
||||
backgroundColor: ColorPalette.toggled,
|
||||
width: '100%',
|
||||
borderRadius: 6
|
||||
},
|
||||
underlayColor: ColorPalette.toggled,
|
||||
emoji: reactionEmoji,
|
||||
text: {
|
||||
color: ColorPalette.white,
|
||||
fontWeight: '600',
|
||||
marginLeft: 8,
|
||||
lineHeight: 24
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
},
|
||||
|
||||
emojiAnimation: {
|
||||
color: ColorPalette.white,
|
||||
position: 'absolute',
|
||||
zIndex: 1001,
|
||||
elevation: 2,
|
||||
fontSize: 20,
|
||||
left: '50%',
|
||||
top: '100%'
|
||||
},
|
||||
|
||||
/**
|
||||
* Styles for toggled buttons in the toolbar.
|
||||
*/
|
||||
|
|
|
@ -1,36 +1,24 @@
|
|||
// @flow
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { Icon, IconArrowUpWide, IconArrowDownWide } from '../../../base/icons';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Whether the drawer should have a button that expands its size or not.
|
||||
*/
|
||||
canExpand: ?boolean,
|
||||
|
||||
/**
|
||||
* The component(s) to be displayed within the drawer menu.
|
||||
*/
|
||||
children: React$Node,
|
||||
|
||||
/**
|
||||
Whether the drawer should be shown or not.
|
||||
* Whether the drawer should be shown or not.
|
||||
*/
|
||||
isOpen: boolean,
|
||||
|
||||
/**
|
||||
Function that hides the drawer.
|
||||
* Function that hides the drawer.
|
||||
*/
|
||||
onClose: Function,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
onClose: Function
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -39,12 +27,9 @@ type Props = {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
function Drawer({
|
||||
canExpand,
|
||||
children,
|
||||
isOpen,
|
||||
onClose,
|
||||
t }: Props) {
|
||||
const [ expanded, setExpanded ] = useState(false);
|
||||
onClose }: Props) {
|
||||
const drawerRef: Object = useRef(null);
|
||||
|
||||
/**
|
||||
|
@ -69,53 +54,15 @@ function Drawer({
|
|||
};
|
||||
}, [ drawerRef ]);
|
||||
|
||||
/**
|
||||
* Toggles the menu state between expanded/collapsed.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function toggleExpanded() {
|
||||
setExpanded(!expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
* KeyPress handler for accessibility.
|
||||
*
|
||||
* @param {React.KeyboardEventHandler<HTMLDivElement>} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function onKeyPress(e) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
toggleExpanded();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
isOpen ? (
|
||||
<div
|
||||
className = { `drawer-menu${expanded ? ' expanded' : ''}` }
|
||||
className = 'drawer-menu'
|
||||
ref = { drawerRef }>
|
||||
{canExpand && (
|
||||
<div
|
||||
aria-expanded = { expanded }
|
||||
aria-label = { expanded ? t('toolbar.accessibilityLabel.collapse')
|
||||
: t('toolbar.accessibilityLabel.expand') }
|
||||
className = 'drawer-toggle'
|
||||
onClick = { toggleExpanded }
|
||||
onKeyPress = { onKeyPress }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
<Icon
|
||||
size = { 24 }
|
||||
src = { expanded ? IconArrowDownWide : IconArrowUpWide } />
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
) : null
|
||||
);
|
||||
}
|
||||
|
||||
export default translate(Drawer);
|
||||
export default Drawer;
|
||||
|
|
|
@ -7,6 +7,9 @@ import { createToolbarEvent, sendAnalytics } from '../../../analytics';
|
|||
import { translate } from '../../../base/i18n';
|
||||
import { IconHorizontalPoints } from '../../../base/icons';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { ReactionEmoji, ReactionsMenu } from '../../../reactions/components';
|
||||
import { type ReactionEmojiProps } from '../../../reactions/constants';
|
||||
import { getReactionsQueue } from '../../../reactions/functions.any';
|
||||
|
||||
import Drawer from './Drawer';
|
||||
import DrawerPortal from './DrawerPortal';
|
||||
|
@ -45,7 +48,17 @@ type Props = {
|
|||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* The array of reactions to be displayed.
|
||||
*/
|
||||
reactionsQueue: Array<ReactionEmojiProps>,
|
||||
|
||||
/**
|
||||
* Whether or not to display the reactions in the mobile menu.
|
||||
*/
|
||||
showMobileReactions: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -93,7 +106,7 @@ class OverflowMenuButton extends Component<Props> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { children, isOpen, overflowDrawer } = this.props;
|
||||
const { children, isOpen, overflowDrawer, reactionsQueue, showMobileReactions } = this.props;
|
||||
|
||||
return (
|
||||
<div className = 'toolbox-button-wth-dialog'>
|
||||
|
@ -103,11 +116,18 @@ class OverflowMenuButton extends Component<Props> {
|
|||
{this._renderToolbarButton()}
|
||||
<DrawerPortal>
|
||||
<Drawer
|
||||
canExpand = { true }
|
||||
isOpen = { isOpen }
|
||||
onClose = { this._onCloseDialog }>
|
||||
{children}
|
||||
{showMobileReactions && <ReactionsMenu overflowMenu = { true } />}
|
||||
</Drawer>
|
||||
{showMobileReactions && <div className = 'reactions-animations-container'>
|
||||
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
|
||||
index = { index }
|
||||
key = { uid }
|
||||
reaction = { reaction }
|
||||
uid = { uid } />))}
|
||||
</div>}
|
||||
</DrawerPortal>
|
||||
</>
|
||||
) : (
|
||||
|
@ -188,7 +208,8 @@ function mapStateToProps(state) {
|
|||
const { overflowDrawer } = state['features/toolbox'];
|
||||
|
||||
return {
|
||||
overflowDrawer
|
||||
overflowDrawer,
|
||||
reactionsQueue: getReactionsQueue(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,83 +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,
|
||||
|
||||
/**
|
||||
* External handler for click action.
|
||||
*/
|
||||
handleClick: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
this.props.handleClick();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
|
@ -36,6 +36,7 @@ import {
|
|||
} from '../../../participants-pane/actions';
|
||||
import ParticipantsPaneButton from '../../../participants-pane/components/ParticipantsPaneButton';
|
||||
import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
|
||||
import { ReactionsMenuButton } from '../../../reactions/components';
|
||||
import {
|
||||
LiveStreamButton,
|
||||
RecordButton
|
||||
|
@ -81,7 +82,6 @@ import AudioSettingsButton from './AudioSettingsButton';
|
|||
import FullscreenButton from './FullscreenButton';
|
||||
import OverflowMenuButton from './OverflowMenuButton';
|
||||
import ProfileButton from './ProfileButton';
|
||||
import RaiseHandButton from './RaiseHandButton';
|
||||
import Separator from './Separator';
|
||||
import ShareDesktopButton from './ShareDesktopButton';
|
||||
import VideoSettingsButton from './VideoSettingsButton';
|
||||
|
@ -256,7 +256,6 @@ class Toolbox extends Component<Props> {
|
|||
this._onToolbarOpenVideoQuality = this._onToolbarOpenVideoQuality.bind(this);
|
||||
this._onToolbarToggleChat = this._onToolbarToggleChat.bind(this);
|
||||
this._onToolbarToggleFullScreen = this._onToolbarToggleFullScreen.bind(this);
|
||||
this._onToolbarToggleRaiseHand = this._onToolbarToggleRaiseHand.bind(this);
|
||||
this._onToolbarToggleScreenshare = this._onToolbarToggleScreenshare.bind(this);
|
||||
this._onShortcutToggleTileView = this._onShortcutToggleTileView.bind(this);
|
||||
this._onEscKey = this._onEscKey.bind(this);
|
||||
|
@ -547,8 +546,7 @@ class Toolbox extends Component<Props> {
|
|||
|
||||
const raisehand = {
|
||||
key: 'raisehand',
|
||||
Content: RaiseHandButton,
|
||||
handleClick: this._onToolbarToggleRaiseHand,
|
||||
Content: ReactionsMenuButton,
|
||||
group: 2
|
||||
};
|
||||
|
||||
|
@ -1024,23 +1022,6 @@ class Toolbox extends Component<Props> {
|
|||
this._doToggleFullScreen();
|
||||
}
|
||||
|
||||
_onToolbarToggleRaiseHand: () => void;
|
||||
|
||||
/**
|
||||
* Creates an analytics toolbar event and dispatches an action for toggling
|
||||
* raise hand.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToolbarToggleRaiseHand() {
|
||||
sendAnalytics(createToolbarEvent(
|
||||
'raise.hand',
|
||||
{ enable: !this.props._raisedHand }));
|
||||
|
||||
this._doToggleRaiseHand();
|
||||
}
|
||||
|
||||
_onToolbarToggleScreenshare: () => void;
|
||||
|
||||
/**
|
||||
|
@ -1144,7 +1125,10 @@ class Toolbox extends Component<Props> {
|
|||
ariaControls = 'overflow-menu'
|
||||
isOpen = { _overflowMenuVisible }
|
||||
key = 'overflow-menu'
|
||||
onVisibilityChange = { this._onSetOverflowVisible }>
|
||||
onVisibilityChange = { this._onSetOverflowVisible }
|
||||
showMobileReactions = {
|
||||
overflowMenuButtons.find(({ key }) => key === 'raisehand')
|
||||
}>
|
||||
<ul
|
||||
aria-label = { t(toolbarAccLabel) }
|
||||
className = 'overflow-menu'
|
||||
|
@ -1154,15 +1138,15 @@ class Toolbox extends Component<Props> {
|
|||
{overflowMenuButtons.map(({ group, key, Content, ...rest }, index, arr) => {
|
||||
const showSeparator = index > 0 && arr[index - 1].group !== group;
|
||||
|
||||
return (
|
||||
<>
|
||||
return key !== 'raisehand'
|
||||
&& <>
|
||||
{showSeparator && <Separator key = { `hr${group}` } />}
|
||||
<Content
|
||||
{ ...rest }
|
||||
key = { key }
|
||||
showLabel = { true } />
|
||||
</>
|
||||
);
|
||||
;
|
||||
})}
|
||||
</ul>
|
||||
</OverflowMenuButton>
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
SET_FULL_SCREEN
|
||||
} from './actionTypes';
|
||||
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
|
@ -18,6 +19,7 @@ declare var APP: Object;
|
|||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
switch (action.type) {
|
||||
case CLEAR_TOOLBOX_TIMEOUT: {
|
||||
const { timeoutID } = store.getState()['features/toolbox'];
|
||||
|
|
Loading…
Reference in New Issue