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),
|
.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;
|
margin-bottom: 4px;
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: 0;
|
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
|
* The following selectors keep the chat modal full-size anywhere between 100px
|
||||||
* and 580px for desktop or 680px for mobile.
|
* and 580px for desktop or 680px for mobile.
|
||||||
|
|
|
@ -4,17 +4,16 @@
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: $drawerZ;
|
z-index: $drawerZ;
|
||||||
|
background-color: #141414;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-menu {
|
.drawer-menu {
|
||||||
max-height: 50vh;
|
max-height: calc(80vh - 64px);
|
||||||
background: #242528;
|
background: #242528;
|
||||||
border-radius: 16px 16px 0 0;
|
border-radius: 16px 16px 0 0;
|
||||||
overflow-y: auto;
|
overflow-y: hidden;
|
||||||
|
margin-bottom: env(safe-area-inset-bottom, 0);
|
||||||
&.expanded {
|
|
||||||
max-height: 80vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer-toggle {
|
.drawer-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -42,6 +41,8 @@
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
height: calc(80vh - 144px - 64px);
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
.overflow-menu-item {
|
.overflow-menu-item {
|
||||||
box-sizing: border-box;
|
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;
|
margin: 0 auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
pointer-events: all;
|
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 {
|
.toolbox-content-items {
|
||||||
background: $newToolbarBackgroundColor;
|
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;
|
border-radius: 6px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 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
|
* fixed positioning is necessary for remote menus and tooltips to pop
|
||||||
* out of the scrolling filmstrip. AtlasKit dialogs and tooltips use
|
* out of the scrolling filmstrip. AtlasKit dialogs and tooltips use
|
||||||
|
|
|
@ -106,6 +106,7 @@ $flagsImagePath: "../images/";
|
||||||
@import 'connection-status';
|
@import 'connection-status';
|
||||||
@import 'drawer';
|
@import 'drawer';
|
||||||
@import 'participants-pane';
|
@import 'participants-pane';
|
||||||
|
@import 'reactions-menu';
|
||||||
@import 'plan-limit';
|
@import 'plan-limit';
|
||||||
|
|
||||||
/* Modules END */
|
/* Modules END */
|
||||||
|
|
|
@ -808,6 +808,7 @@
|
||||||
"callQuality": "Manage video quality",
|
"callQuality": "Manage video quality",
|
||||||
"cc": "Toggle subtitles",
|
"cc": "Toggle subtitles",
|
||||||
"chat": "Open / Close chat",
|
"chat": "Open / Close chat",
|
||||||
|
"clap": "Clap",
|
||||||
"document": "Toggle shared document",
|
"document": "Toggle shared document",
|
||||||
"download": "Download our apps",
|
"download": "Download our apps",
|
||||||
"embedMeeting": "Embed meeting",
|
"embedMeeting": "Embed meeting",
|
||||||
|
@ -817,7 +818,9 @@
|
||||||
"hangup": "Leave the meeting",
|
"hangup": "Leave the meeting",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"invite": "Invite people",
|
"invite": "Invite people",
|
||||||
|
"joy": "Laughing Crying",
|
||||||
"kick": "Kick participant",
|
"kick": "Kick participant",
|
||||||
|
"like": "Thumbs Up",
|
||||||
"lobbyButton": "Enable/disable lobby mode",
|
"lobbyButton": "Enable/disable lobby mode",
|
||||||
"localRecording": "Toggle local recording controls",
|
"localRecording": "Toggle local recording controls",
|
||||||
"lockRoom": "Toggle meeting password",
|
"lockRoom": "Toggle meeting password",
|
||||||
|
@ -830,10 +833,12 @@
|
||||||
"muteEveryonesVideo": "Disable everyone's camera",
|
"muteEveryonesVideo": "Disable everyone's camera",
|
||||||
"muteEveryoneElsesVideo": "Disable everyone else's camera",
|
"muteEveryoneElsesVideo": "Disable everyone else's camera",
|
||||||
"participants": "Participants",
|
"participants": "Participants",
|
||||||
|
"party": "Party Popper",
|
||||||
"pip": "Toggle Picture-in-Picture mode",
|
"pip": "Toggle Picture-in-Picture mode",
|
||||||
"privateMessage": "Send private message",
|
"privateMessage": "Send private message",
|
||||||
"profile": "Edit your profile",
|
"profile": "Edit your profile",
|
||||||
"raiseHand": "Raise / Lower your hand",
|
"raiseHand": "Raise / Lower your hand",
|
||||||
|
"reactionsMenu": "Open / Close reactions menu",
|
||||||
"recording": "Toggle recording",
|
"recording": "Toggle recording",
|
||||||
"remoteMute": "Mute participant",
|
"remoteMute": "Mute participant",
|
||||||
"remoteVideoMute": "Disable camera of participant",
|
"remoteVideoMute": "Disable camera of participant",
|
||||||
|
@ -845,7 +850,9 @@
|
||||||
"shareYourScreen": "Start / Stop sharing your screen",
|
"shareYourScreen": "Start / Stop sharing your screen",
|
||||||
"shortcuts": "Toggle shortcuts",
|
"shortcuts": "Toggle shortcuts",
|
||||||
"show": "Show on stage",
|
"show": "Show on stage",
|
||||||
|
"smile": "Smile",
|
||||||
"speakerStats": "Toggle speaker statistics",
|
"speakerStats": "Toggle speaker statistics",
|
||||||
|
"surprised": "Surprised",
|
||||||
"tileView": "Toggle tile view",
|
"tileView": "Toggle tile view",
|
||||||
"toggleCamera": "Toggle camera",
|
"toggleCamera": "Toggle camera",
|
||||||
"toggleFilmstrip": "Toggle filmstrip",
|
"toggleFilmstrip": "Toggle filmstrip",
|
||||||
|
@ -864,7 +871,9 @@
|
||||||
"authenticate": "Authenticate",
|
"authenticate": "Authenticate",
|
||||||
"callQuality": "Manage video quality",
|
"callQuality": "Manage video quality",
|
||||||
"chat": "Open / Close chat",
|
"chat": "Open / Close chat",
|
||||||
|
"clap": "Clap",
|
||||||
"closeChat": "Close chat",
|
"closeChat": "Close chat",
|
||||||
|
"closeReactionsMenu": "Close reactions menu",
|
||||||
"documentClose": "Close shared document",
|
"documentClose": "Close shared document",
|
||||||
"documentOpen": "Open shared document",
|
"documentOpen": "Open shared document",
|
||||||
"download": "Download our apps",
|
"download": "Download our apps",
|
||||||
|
@ -878,6 +887,8 @@
|
||||||
"hangup": "Leave the meeting",
|
"hangup": "Leave the meeting",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"invite": "Invite people",
|
"invite": "Invite people",
|
||||||
|
"joy": "Joy",
|
||||||
|
"like": "Thumbs Up",
|
||||||
"lobbyButtonDisable": "Disable lobby mode",
|
"lobbyButtonDisable": "Disable lobby mode",
|
||||||
"lobbyButtonEnable": "Enable lobby mode",
|
"lobbyButtonEnable": "Enable lobby mode",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
|
@ -896,18 +907,27 @@
|
||||||
"noisyAudioInputTitle": "Your microphone appears to be noisy!",
|
"noisyAudioInputTitle": "Your microphone appears to be noisy!",
|
||||||
"noisyAudioInputDesc": "It sounds like your microphone is making noise, please consider muting or changing the device.",
|
"noisyAudioInputDesc": "It sounds like your microphone is making noise, please consider muting or changing the device.",
|
||||||
"openChat": "Open chat",
|
"openChat": "Open chat",
|
||||||
|
"openReactionsMenu": "Open reactions menu",
|
||||||
"participants": "Participants",
|
"participants": "Participants",
|
||||||
|
"party": "Celebration",
|
||||||
"pip": "Enter Picture-in-Picture mode",
|
"pip": "Enter Picture-in-Picture mode",
|
||||||
"privateMessage": "Send private message",
|
"privateMessage": "Send private message",
|
||||||
"profile": "Edit your profile",
|
"profile": "Edit your profile",
|
||||||
"raiseHand": "Raise / Lower your hand",
|
"raiseHand": "Raise / Lower your hand",
|
||||||
"raiseYourHand": "Raise 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",
|
"security": "Security options",
|
||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
"shareaudio": "Share audio",
|
"shareaudio": "Share audio",
|
||||||
"sharedvideo": "Share video",
|
"sharedvideo": "Share video",
|
||||||
"shareRoom": "Invite someone",
|
"shareRoom": "Invite someone",
|
||||||
"shortcuts": "View shortcuts",
|
"shortcuts": "View shortcuts",
|
||||||
|
"smile": "Smile",
|
||||||
"speakerStats": "Speaker stats",
|
"speakerStats": "Speaker stats",
|
||||||
"startScreenSharing": "Start screen sharing",
|
"startScreenSharing": "Start screen sharing",
|
||||||
"startSubtitles": "Start subtitles",
|
"startSubtitles": "Start subtitles",
|
||||||
|
@ -915,6 +935,7 @@
|
||||||
"stopScreenSharing": "Stop screen sharing",
|
"stopScreenSharing": "Stop screen sharing",
|
||||||
"stopSubtitles": "Stop subtitles",
|
"stopSubtitles": "Stop subtitles",
|
||||||
"stopSharedVideo": "Stop video",
|
"stopSharedVideo": "Stop video",
|
||||||
|
"surprised": "Surprised",
|
||||||
"talkWhileMutedPopup": "Trying to speak? You are muted.",
|
"talkWhileMutedPopup": "Trying to speak? You are muted.",
|
||||||
"tileViewToggle": "Toggle tile view",
|
"tileViewToggle": "Toggle tile view",
|
||||||
"toggleCamera": "Toggle camera",
|
"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
|
* The payload name for the datachannel/endpoint text message event
|
||||||
*/
|
*/
|
||||||
export const ENDPOINT_TEXT_MESSAGE_NAME = 'endpoint-text-message';
|
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 exec the function to be executed when the shortcut is pressed
|
||||||
* @param helpDescription the description of the shortcut that would appear
|
* @param helpDescription the description of the shortcut that would appear
|
||||||
* in the help menu
|
* in the help menu
|
||||||
|
* @param altKey whether or not the alt key must be pressed.
|
||||||
*/
|
*/
|
||||||
registerShortcut(// eslint-disable-line max-params
|
registerShortcut(// eslint-disable-line max-params
|
||||||
shortcutChar,
|
shortcutChar,
|
||||||
shortcutAttr,
|
shortcutAttr,
|
||||||
exec,
|
exec,
|
||||||
helpDescription) {
|
helpDescription,
|
||||||
_shortcuts.set(shortcutChar, {
|
altKey = false) {
|
||||||
|
_shortcuts.set(altKey ? `:${shortcutChar}` : shortcutChar, {
|
||||||
character: shortcutChar,
|
character: shortcutChar,
|
||||||
function: exec,
|
function: exec,
|
||||||
shortcutAttr
|
shortcutAttr,
|
||||||
|
altKey
|
||||||
});
|
});
|
||||||
|
|
||||||
if (helpDescription) {
|
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
|
* @param shortcutChar unregisters the given shortcut, which means it will
|
||||||
* no longer be usable
|
* no longer be usable
|
||||||
|
* @param altKey whether or not shortcut is combo with alt key
|
||||||
*/
|
*/
|
||||||
unregisterShortcut(shortcutChar) {
|
unregisterShortcut(shortcutChar, altKey = false) {
|
||||||
_shortcuts.delete(shortcutChar);
|
_shortcuts.delete(altKey ? `:${shortcutChar}` : shortcutChar);
|
||||||
_shortcutsHelp.delete(shortcutChar);
|
_shortcutsHelp.delete(shortcutChar);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -175,6 +179,15 @@ const KeyboardShortcut = {
|
||||||
* @returns {string} e.key or something close if not supported
|
* @returns {string} e.key or something close if not supported
|
||||||
*/
|
*/
|
||||||
_getKeyboardKey(e) {
|
_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
|
// 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
|
// 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
|
// 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 '../lobby/middleware';
|
||||||
import '../notifications/middleware';
|
import '../notifications/middleware';
|
||||||
import '../overlay/middleware';
|
import '../overlay/middleware';
|
||||||
|
import '../reactions/middleware';
|
||||||
import '../recent-list/middleware';
|
import '../recent-list/middleware';
|
||||||
import '../recording/middleware';
|
import '../recording/middleware';
|
||||||
import '../rejoin/middleware';
|
import '../rejoin/middleware';
|
||||||
|
|
|
@ -41,6 +41,7 @@ import '../large-video/reducer';
|
||||||
import '../lobby/reducer';
|
import '../lobby/reducer';
|
||||||
import '../notifications/reducer';
|
import '../notifications/reducer';
|
||||||
import '../overlay/reducer';
|
import '../overlay/reducer';
|
||||||
|
import '../reactions/reducer';
|
||||||
import '../recent-list/reducer';
|
import '../recent-list/reducer';
|
||||||
import '../recording/reducer';
|
import '../recording/reducer';
|
||||||
import '../settings/reducer';
|
import '../settings/reducer';
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { type ReactionEmojiProps } from '../../../reactions/constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the React {@code Component} props of {@link DialogContainer}.
|
* 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.
|
* 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.
|
* 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}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { _styles, renderHeader } = this.props;
|
const { _styles, renderHeader, renderFooter, _height } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SlidingView
|
<SlidingView
|
||||||
|
@ -99,7 +109,10 @@ class BottomSheet extends PureComponent<Props> {
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
style = { [
|
style = { [
|
||||||
styles.sheetItemContainer,
|
styles.sheetItemContainer,
|
||||||
_styles.sheet
|
_styles.sheet,
|
||||||
|
{
|
||||||
|
maxHeight: _height - 100
|
||||||
|
}
|
||||||
] }
|
] }
|
||||||
{ ...this.panResponder.panHandlers }>
|
{ ...this.panResponder.panHandlers }>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
@ -108,6 +121,7 @@ class BottomSheet extends PureComponent<Props> {
|
||||||
style = { styles.scrollView } >
|
style = { styles.scrollView } >
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
{ renderFooter && renderFooter() }
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</View>
|
</View>
|
||||||
</SlidingView>
|
</SlidingView>
|
||||||
|
@ -167,7 +181,8 @@ class BottomSheet extends PureComponent<Props> {
|
||||||
*/
|
*/
|
||||||
function _mapStateToProps(state) {
|
function _mapStateToProps(state) {
|
||||||
return {
|
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 { connect } from '../../../redux';
|
||||||
import AbstractDialogContainer, {
|
import AbstractDialogContainer, {
|
||||||
abstractMapStateToProps
|
abstractMapStateToProps
|
||||||
|
@ -11,6 +15,22 @@ import AbstractDialogContainer, {
|
||||||
* @extends AbstractDialogContainer
|
* @extends AbstractDialogContainer
|
||||||
*/
|
*/
|
||||||
class DialogContainer 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()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -18,8 +38,18 @@ class DialogContainer extends AbstractDialogContainer {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
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: {
|
scrollView: {
|
||||||
paddingHorizontal: MD_ITEM_MARGIN_PADDING
|
paddingHorizontal: 0
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -117,7 +117,7 @@ const brandedDialogText = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const brandedDialogLabelStyle = {
|
const brandedDialogLabelStyle = {
|
||||||
color: schemeColor('text'),
|
color: ColorPalette.white,
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
fontSize: MD_FONT_SIZE,
|
fontSize: MD_FONT_SIZE,
|
||||||
opacity: 0.90
|
opacity: 0.90
|
||||||
|
@ -130,7 +130,7 @@ const brandedDialogItemContainerStyle = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const brandedDialogIconStyle = {
|
const brandedDialogIconStyle = {
|
||||||
color: schemeColor('icon'),
|
color: ColorPalette.white,
|
||||||
fontSize: 24
|
fontSize: 24
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -178,20 +178,24 @@ ColorSchemeRegistry.register('BottomSheet', {
|
||||||
* Container style for a generic item rendered in the menu.
|
* Container style for a generic item rendered in the menu.
|
||||||
*/
|
*/
|
||||||
style: {
|
style: {
|
||||||
...brandedDialogItemContainerStyle
|
...brandedDialogItemContainerStyle,
|
||||||
|
backgroundColor: ColorPalette.darkBackground,
|
||||||
|
paddingHorizontal: MD_ITEM_MARGIN_PADDING
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Additional style that is not directly used as a style object.
|
* Additional style that is not directly used as a style object.
|
||||||
*/
|
*/
|
||||||
underlayColor: ColorPalette.overflowMenuItemUnderlay
|
underlayColor: ColorPalette.toggled
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bottom sheet's base style.
|
* Bottom sheet's base style.
|
||||||
*/
|
*/
|
||||||
sheet: {
|
sheet: {
|
||||||
backgroundColor: schemeColor('background')
|
backgroundColor: ColorPalette.black,
|
||||||
|
borderTopLeftRadius: 16,
|
||||||
|
borderTopRightRadius: 16
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
// @flow
|
// @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 { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
|
||||||
import {
|
import {
|
||||||
CONFERENCE_JOINED,
|
CONFERENCE_JOINED,
|
||||||
|
@ -19,7 +22,18 @@ import {
|
||||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||||
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
||||||
import { openDisplayNamePrompt } from '../display-name';
|
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 { showToolbox } from '../toolbox/actions';
|
||||||
|
import {
|
||||||
|
hideToolbox,
|
||||||
|
setToolboxTimeout,
|
||||||
|
setToolboxVisible
|
||||||
|
} from '../toolbox/actions.web';
|
||||||
|
|
||||||
import { ADD_MESSAGE, SEND_MESSAGE, OPEN_CHAT, CLOSE_CHAT } from './actionTypes';
|
import { ADD_MESSAGE, SEND_MESSAGE, OPEN_CHAT, CLOSE_CHAT } from './actionTypes';
|
||||||
import { addMessage, clearMessages } from './actions';
|
import { addMessage, clearMessages } from './actions';
|
||||||
|
@ -143,6 +157,15 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case ADD_REACTIONS_MESSAGE: {
|
||||||
|
_handleReceivedMessage(store, {
|
||||||
|
id: localParticipant.id,
|
||||||
|
message: action.message,
|
||||||
|
privateMessage: false,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return next(action);
|
return next(action);
|
||||||
|
@ -189,6 +212,7 @@ StateListenerRegistry.register(
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function _addChatMsgListener(conference, store) {
|
function _addChatMsgListener(conference, store) {
|
||||||
|
const reactions = {};
|
||||||
|
|
||||||
if (store.getState()['features/base/config'].iAmRecorder) {
|
if (store.getState()['features/base/config'].iAmRecorder) {
|
||||||
// We don't register anything on web if we are in iAmRecorder mode
|
// 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(
|
conference.on(
|
||||||
JitsiConferenceEvents.CONFERENCE_ERROR, (errorType, error) => {
|
JitsiConferenceEvents.CONFERENCE_ERROR, (errorType, error) => {
|
||||||
errorType === JitsiConferenceErrors.CHAT_ERROR && _handleChatError(store, error);
|
errorType === JitsiConferenceErrors.CHAT_ERROR && _handleChatError(store, error);
|
||||||
|
|
|
@ -67,6 +67,14 @@ class KeyboardShortcutsDialog extends Component<Props> {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
_renderShortcutsListItem(keyboardKey, translationKey) {
|
_renderShortcutsListItem(keyboardKey, translationKey) {
|
||||||
|
let modifierKey = 'Alt';
|
||||||
|
|
||||||
|
if (window.navigator?.platform) {
|
||||||
|
if (window.navigator.platform.indexOf('Mac') !== -1) {
|
||||||
|
modifierKey = '⌥';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className = 'shortcuts-list__item'
|
className = 'shortcuts-list__item'
|
||||||
|
@ -78,7 +86,9 @@ class KeyboardShortcutsDialog extends Component<Props> {
|
||||||
</span>
|
</span>
|
||||||
<span className = 'item-action'>
|
<span className = 'item-action'>
|
||||||
<Lozenge isBold = { true }>
|
<Lozenge isBold = { true }>
|
||||||
{ keyboardKey }
|
{ keyboardKey.startsWith(':')
|
||||||
|
? `${modifierKey} + ${keyboardKey.slice(1)}`
|
||||||
|
: keyboardKey }
|
||||||
</Lozenge>
|
</Lozenge>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</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 { type Dispatch } from 'redux';
|
||||||
|
|
||||||
import {
|
import { isDialogOpen, openDialog } from '../../../base/dialog';
|
||||||
createToolbarEvent,
|
|
||||||
sendAnalytics
|
|
||||||
} from '../../../analytics';
|
|
||||||
import { RAISE_HAND_ENABLED, getFeatureFlag } from '../../../base/flags';
|
import { RAISE_HAND_ENABLED, getFeatureFlag } from '../../../base/flags';
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
import { IconRaisedHand } from '../../../base/icons';
|
import { IconRaisedHand } from '../../../base/icons';
|
||||||
import {
|
import {
|
||||||
getLocalParticipant,
|
getLocalParticipant
|
||||||
raiseHand
|
|
||||||
} from '../../../base/participants';
|
} from '../../../base/participants';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
|
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 & {
|
type Props = AbstractButtonProps & {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The local participant.
|
* Whether the participant raised their hand or not.
|
||||||
*/
|
|
||||||
_localParticipant: Object,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the participant raused their hand or not.
|
|
||||||
*/
|
*/
|
||||||
_raisedHand: boolean,
|
_raisedHand: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the reactions menu is open.
|
||||||
|
*/
|
||||||
|
_reactionsOpen: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The redux {@code dispatch} function.
|
* The redux {@code dispatch} function.
|
||||||
*/
|
*/
|
||||||
|
@ -40,11 +38,11 @@ type Props = AbstractButtonProps & {
|
||||||
/**
|
/**
|
||||||
* An implementation of a button to raise or lower hand.
|
* An implementation of a button to raise or lower hand.
|
||||||
*/
|
*/
|
||||||
class RaiseHandButton extends AbstractButton<Props, *> {
|
class ReactionsMenuButton extends AbstractButton<Props, *> {
|
||||||
accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
|
accessibilityLabel = 'toolbar.accessibilityLabel.reactionsMenu';
|
||||||
icon = IconRaisedHand;
|
icon = IconRaisedHand;
|
||||||
label = 'toolbar.raiseYourHand';
|
label = 'toolbar.openReactionsMenu';
|
||||||
toggledLabel = 'toolbar.lowerYourHand';
|
toggledLabel = 'toolbar.closeReactionsMenu';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles clicking / pressing the button.
|
* Handles clicking / pressing the button.
|
||||||
|
@ -54,7 +52,7 @@ class RaiseHandButton extends AbstractButton<Props, *> {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_handleClick() {
|
_handleClick() {
|
||||||
this._toggleRaisedHand();
|
this.props.dispatch(openDialog(ReactionMenuDialog));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -65,20 +63,7 @@ class RaiseHandButton extends AbstractButton<Props, *> {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
_isToggled() {
|
_isToggled() {
|
||||||
return this.props._raisedHand;
|
return this.props._raisedHand || this.props._reactionsOpen;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,10 +81,10 @@ function _mapStateToProps(state, ownProps): Object {
|
||||||
const { visible = enabled } = ownProps;
|
const { visible = enabled } = ownProps;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_localParticipant,
|
|
||||||
_raisedHand: _localParticipant.raisedHand,
|
_raisedHand: _localParticipant.raisedHand,
|
||||||
|
_reactionsOpen: isDialogOpen(state, ReactionMenuDialog),
|
||||||
visible
|
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
|
// @flow
|
||||||
|
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { TouchableOpacity, View } from 'react-native';
|
|
||||||
import Collapsible from 'react-native-collapsible';
|
|
||||||
|
|
||||||
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||||
import { BottomSheet, hideDialog, isDialogOpen } from '../../../base/dialog';
|
import { BottomSheet, hideDialog, isDialogOpen } from '../../../base/dialog';
|
||||||
import { IconDragHandle } from '../../../base/icons';
|
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
import { StyleType } from '../../../base/styles';
|
import { StyleType } from '../../../base/styles';
|
||||||
import { SharedDocumentButton } from '../../../etherpad';
|
import { SharedDocumentButton } from '../../../etherpad';
|
||||||
import { InviteButton } from '../../../invite';
|
import { InviteButton } from '../../../invite';
|
||||||
import { AudioRouteButton } from '../../../mobile/audio-mode';
|
import { AudioRouteButton } from '../../../mobile/audio-mode';
|
||||||
|
import { ReactionMenu } from '../../../reactions/components';
|
||||||
import { LiveStreamButton, RecordButton } from '../../../recording';
|
import { LiveStreamButton, RecordButton } from '../../../recording';
|
||||||
import SecurityDialogButton from '../../../security/components/security-dialog/SecurityDialogButton';
|
import SecurityDialogButton from '../../../security/components/security-dialog/SecurityDialogButton';
|
||||||
import { SharedVideoButton } from '../../../shared-video/components';
|
import { SharedVideoButton } from '../../../shared-video/components';
|
||||||
|
@ -23,11 +21,8 @@ import MuteEveryoneButton from '../MuteEveryoneButton';
|
||||||
import MuteEveryonesVideoButton from '../MuteEveryonesVideoButton';
|
import MuteEveryonesVideoButton from '../MuteEveryonesVideoButton';
|
||||||
|
|
||||||
import AudioOnlyButton from './AudioOnlyButton';
|
import AudioOnlyButton from './AudioOnlyButton';
|
||||||
import MoreOptionsButton from './MoreOptionsButton';
|
|
||||||
import RaiseHandButton from './RaiseHandButton';
|
|
||||||
import ScreenSharingButton from './ScreenSharingButton.js';
|
import ScreenSharingButton from './ScreenSharingButton.js';
|
||||||
import ToggleCameraButton from './ToggleCameraButton';
|
import ToggleCameraButton from './ToggleCameraButton';
|
||||||
import styles from './styles';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the React {@code Component} props of {@link OverflowMenu}.
|
* 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.
|
* True if the bottom scheet is scrolled to the top.
|
||||||
*/
|
*/
|
||||||
scrolledToTop: boolean,
|
scrolledToTop: boolean
|
||||||
|
|
||||||
/**
|
|
||||||
* True if the 'more' button set needas to be rendered.
|
|
||||||
*/
|
|
||||||
showMore: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -96,15 +86,12 @@ class OverflowMenu extends PureComponent<Props, State> {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
scrolledToTop: true,
|
scrolledToTop: true
|
||||||
showMore: false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bind event handlers so they are only bound once per instance.
|
// Bind event handlers so they are only bound once per instance.
|
||||||
this._onCancel = this._onCancel.bind(this);
|
this._onCancel = this._onCancel.bind(this);
|
||||||
this._onSwipe = this._onSwipe.bind(this);
|
this._renderReactionMenu = this._renderReactionMenu.bind(this);
|
||||||
this._onToggleMenu = this._onToggleMenu.bind(this);
|
|
||||||
this._renderMenuExpandToggle = this._renderMenuExpandToggle.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -115,7 +102,6 @@ class OverflowMenu extends PureComponent<Props, State> {
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { _bottomSheetStyles, _width } = this.props;
|
const { _bottomSheetStyles, _width } = this.props;
|
||||||
const { showMore } = this.state;
|
|
||||||
const toolbarButtons = getMovableButtons(_width);
|
const toolbarButtons = getMovableButtons(_width);
|
||||||
|
|
||||||
const buttonProps = {
|
const buttonProps = {
|
||||||
|
@ -124,63 +110,45 @@ class OverflowMenu extends PureComponent<Props, State> {
|
||||||
styles: _bottomSheetStyles.buttons
|
styles: _bottomSheetStyles.buttons
|
||||||
};
|
};
|
||||||
|
|
||||||
const moreOptionsButtonProps = {
|
const topButtonProps = {
|
||||||
...buttonProps,
|
afterClick: this._onCancel,
|
||||||
afterClick: this._onToggleMenu,
|
showLabel: true,
|
||||||
visible: !showMore
|
styles: {
|
||||||
|
..._bottomSheetStyles.buttons,
|
||||||
|
style: {
|
||||||
|
..._bottomSheetStyles.buttons.style,
|
||||||
|
borderTopLeftRadius: 16,
|
||||||
|
borderTopRightRadius: 16,
|
||||||
|
paddingTop: 16
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BottomSheet
|
<BottomSheet
|
||||||
onCancel = { this._onCancel }
|
onCancel = { this._onCancel }
|
||||||
onSwipe = { this._onSwipe }
|
renderFooter = { toolbarButtons.has('raisehand')
|
||||||
renderHeader = { this._renderMenuExpandToggle }>
|
? null
|
||||||
<AudioRouteButton { ...buttonProps } />
|
: this._renderReactionMenu }>
|
||||||
|
<AudioRouteButton { ...topButtonProps } />
|
||||||
{!toolbarButtons.has('invite') && <InviteButton { ...buttonProps } />}
|
{!toolbarButtons.has('invite') && <InviteButton { ...buttonProps } />}
|
||||||
<AudioOnlyButton { ...buttonProps } />
|
<AudioOnlyButton { ...buttonProps } />
|
||||||
{!toolbarButtons.has('raisehand') && <RaiseHandButton { ...buttonProps } />}
|
|
||||||
<SecurityDialogButton { ...buttonProps } />
|
<SecurityDialogButton { ...buttonProps } />
|
||||||
<ScreenSharingButton { ...buttonProps } />
|
<ScreenSharingButton { ...buttonProps } />
|
||||||
<MoreOptionsButton { ...moreOptionsButtonProps } />
|
{!toolbarButtons.has('togglecamera') && <ToggleCameraButton { ...buttonProps } />}
|
||||||
<Collapsible collapsed = { !showMore }>
|
{!toolbarButtons.has('tileview') && <TileViewButton { ...buttonProps } />}
|
||||||
{!toolbarButtons.has('togglecamera') && <ToggleCameraButton { ...buttonProps } />}
|
<RecordButton { ...buttonProps } />
|
||||||
{!toolbarButtons.has('tileview') && <TileViewButton { ...buttonProps } />}
|
<LiveStreamButton { ...buttonProps } />
|
||||||
<RecordButton { ...buttonProps } />
|
<SharedVideoButton { ...buttonProps } />
|
||||||
<LiveStreamButton { ...buttonProps } />
|
<ClosedCaptionButton { ...buttonProps } />
|
||||||
<SharedVideoButton { ...buttonProps } />
|
<SharedDocumentButton { ...buttonProps } />
|
||||||
<ClosedCaptionButton { ...buttonProps } />
|
<MuteEveryoneButton { ...buttonProps } />
|
||||||
<SharedDocumentButton { ...buttonProps } />
|
<MuteEveryonesVideoButton { ...buttonProps } />
|
||||||
<MuteEveryoneButton { ...buttonProps } />
|
<HelpButton { ...buttonProps } />
|
||||||
<MuteEveryonesVideoButton { ...buttonProps } />
|
|
||||||
<HelpButton { ...buttonProps } />
|
|
||||||
</Collapsible>
|
|
||||||
</BottomSheet>
|
</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;
|
_onCancel: () => boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -199,45 +167,17 @@ class OverflowMenu extends PureComponent<Props, State> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSwipe: string => void;
|
_renderReactionMenu: () => React$Element<any>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback to be invoked when swipe gesture is detected on the menu. Returns true
|
* Functoin to render the reaction menu as the footer of the bottom sheet.
|
||||||
* if the swipe gesture is handled by the menu, false otherwise.
|
|
||||||
*
|
*
|
||||||
* @param {string} direction - Direction of 'up' or 'down'.
|
* @returns {React$Element}
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
*/
|
||||||
_onSwipe(direction) {
|
_renderReactionMenu() {
|
||||||
const { showMore } = this.state;
|
return (<ReactionMenu
|
||||||
|
onCancel = { this._onCancel }
|
||||||
switch (direction) {
|
overflowMenu = { true } />);
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { connect } from '../../../base/redux';
|
||||||
import { StyleType } from '../../../base/styles';
|
import { StyleType } from '../../../base/styles';
|
||||||
import { ChatButton } from '../../../chat';
|
import { ChatButton } from '../../../chat';
|
||||||
import { InviteButton } from '../../../invite';
|
import { InviteButton } from '../../../invite';
|
||||||
|
import { ReactionsMenuButton } from '../../../reactions/components';
|
||||||
import { TileViewButton } from '../../../video-layout';
|
import { TileViewButton } from '../../../video-layout';
|
||||||
import { isToolboxVisible, getMovableButtons } from '../../functions.native';
|
import { isToolboxVisible, getMovableButtons } from '../../functions.native';
|
||||||
import AudioMuteButton from '../AudioMuteButton';
|
import AudioMuteButton from '../AudioMuteButton';
|
||||||
|
@ -15,7 +16,6 @@ import HangupButton from '../HangupButton';
|
||||||
import VideoMuteButton from '../VideoMuteButton';
|
import VideoMuteButton from '../VideoMuteButton';
|
||||||
|
|
||||||
import OverflowMenuButton from './OverflowMenuButton';
|
import OverflowMenuButton from './OverflowMenuButton';
|
||||||
import RaiseHandButton from './RaiseHandButton';
|
|
||||||
import ToggleCameraButton from './ToggleCameraButton';
|
import ToggleCameraButton from './ToggleCameraButton';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
|
||||||
|
@ -87,9 +87,9 @@ function Toolbox(props: Props) {
|
||||||
toggledStyles = { backgroundToggledStyle } />}
|
toggledStyles = { backgroundToggledStyle } />}
|
||||||
|
|
||||||
{ additionalButtons.has('raisehand')
|
{ additionalButtons.has('raisehand')
|
||||||
&& <RaiseHandButton
|
&& <ReactionsMenuButton
|
||||||
styles = { buttonStylesBorderless }
|
styles = { buttonStylesBorderless }
|
||||||
toggledStyles = { backgroundToggledStyle } />}
|
toggledStyles = { backgroundToggledStyle } />}
|
||||||
{additionalButtons.has('tileview') && <TileViewButton styles = { buttonStylesBorderless } />}
|
{additionalButtons.has('tileview') && <TileViewButton styles = { buttonStylesBorderless } />}
|
||||||
{additionalButtons.has('invite') && <InviteButton styles = { buttonStylesBorderless } />}
|
{additionalButtons.has('invite') && <InviteButton styles = { buttonStylesBorderless } />}
|
||||||
{additionalButtons.has('togglecamera')
|
{additionalButtons.has('togglecamera')
|
||||||
|
|
|
@ -40,18 +40,38 @@ const whiteToolbarButtonIcon = {
|
||||||
color: ColorPalette.white
|
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.
|
* The Toolbox and toolbar related styles.
|
||||||
*/
|
*/
|
||||||
const styles = {
|
const styles = {
|
||||||
|
|
||||||
expandMenuContainer: {
|
|
||||||
alignItems: 'center',
|
|
||||||
borderTopLeftRadius: 16,
|
|
||||||
borderTopRightRadius: 16,
|
|
||||||
flexDirection: 'column'
|
|
||||||
},
|
|
||||||
|
|
||||||
sheetGestureRecognizer: {
|
sheetGestureRecognizer: {
|
||||||
alignItems: 'stretch',
|
alignItems: 'stretch',
|
||||||
flexDirection: 'column'
|
flexDirection: 'column'
|
||||||
|
@ -120,6 +140,67 @@ ColorSchemeRegistry.register('Toolbox', {
|
||||||
underlayColor: ColorPalette.buttonUnderlay
|
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.
|
* Styles for toggled buttons in the toolbar.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,36 +1,24 @@
|
||||||
// @flow
|
// @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 = {
|
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.
|
* The component(s) to be displayed within the drawer menu.
|
||||||
*/
|
*/
|
||||||
children: React$Node,
|
children: React$Node,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Whether the drawer should be shown or not.
|
* Whether the drawer should be shown or not.
|
||||||
*/
|
*/
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Function that hides the drawer.
|
* Function that hides the drawer.
|
||||||
*/
|
*/
|
||||||
onClose: Function,
|
onClose: Function
|
||||||
|
|
||||||
/**
|
|
||||||
* Invoked to obtain translated strings.
|
|
||||||
*/
|
|
||||||
t: Function
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,12 +27,9 @@ type Props = {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
function Drawer({
|
function Drawer({
|
||||||
canExpand,
|
|
||||||
children,
|
children,
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose }: Props) {
|
||||||
t }: Props) {
|
|
||||||
const [ expanded, setExpanded ] = useState(false);
|
|
||||||
const drawerRef: Object = useRef(null);
|
const drawerRef: Object = useRef(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -69,53 +54,15 @@ function Drawer({
|
||||||
};
|
};
|
||||||
}, [ drawerRef ]);
|
}, [ 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 (
|
return (
|
||||||
isOpen ? (
|
isOpen ? (
|
||||||
<div
|
<div
|
||||||
className = { `drawer-menu${expanded ? ' expanded' : ''}` }
|
className = 'drawer-menu'
|
||||||
ref = { drawerRef }>
|
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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
) : null
|
) : null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate(Drawer);
|
export default Drawer;
|
||||||
|
|
|
@ -7,6 +7,9 @@ import { createToolbarEvent, sendAnalytics } from '../../../analytics';
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
import { IconHorizontalPoints } from '../../../base/icons';
|
import { IconHorizontalPoints } from '../../../base/icons';
|
||||||
import { connect } from '../../../base/redux';
|
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 Drawer from './Drawer';
|
||||||
import DrawerPortal from './DrawerPortal';
|
import DrawerPortal from './DrawerPortal';
|
||||||
|
@ -45,7 +48,17 @@ type Props = {
|
||||||
/**
|
/**
|
||||||
* Invoked to obtain translated strings.
|
* 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}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { children, isOpen, overflowDrawer } = this.props;
|
const { children, isOpen, overflowDrawer, reactionsQueue, showMobileReactions } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className = 'toolbox-button-wth-dialog'>
|
<div className = 'toolbox-button-wth-dialog'>
|
||||||
|
@ -103,11 +116,18 @@ class OverflowMenuButton extends Component<Props> {
|
||||||
{this._renderToolbarButton()}
|
{this._renderToolbarButton()}
|
||||||
<DrawerPortal>
|
<DrawerPortal>
|
||||||
<Drawer
|
<Drawer
|
||||||
canExpand = { true }
|
|
||||||
isOpen = { isOpen }
|
isOpen = { isOpen }
|
||||||
onClose = { this._onCloseDialog }>
|
onClose = { this._onCloseDialog }>
|
||||||
{children}
|
{children}
|
||||||
|
{showMobileReactions && <ReactionsMenu overflowMenu = { true } />}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
{showMobileReactions && <div className = 'reactions-animations-container'>
|
||||||
|
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
|
||||||
|
index = { index }
|
||||||
|
key = { uid }
|
||||||
|
reaction = { reaction }
|
||||||
|
uid = { uid } />))}
|
||||||
|
</div>}
|
||||||
</DrawerPortal>
|
</DrawerPortal>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -188,7 +208,8 @@ function mapStateToProps(state) {
|
||||||
const { overflowDrawer } = state['features/toolbox'];
|
const { overflowDrawer } = state['features/toolbox'];
|
||||||
|
|
||||||
return {
|
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';
|
} from '../../../participants-pane/actions';
|
||||||
import ParticipantsPaneButton from '../../../participants-pane/components/ParticipantsPaneButton';
|
import ParticipantsPaneButton from '../../../participants-pane/components/ParticipantsPaneButton';
|
||||||
import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
|
import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
|
||||||
|
import { ReactionsMenuButton } from '../../../reactions/components';
|
||||||
import {
|
import {
|
||||||
LiveStreamButton,
|
LiveStreamButton,
|
||||||
RecordButton
|
RecordButton
|
||||||
|
@ -81,7 +82,6 @@ import AudioSettingsButton from './AudioSettingsButton';
|
||||||
import FullscreenButton from './FullscreenButton';
|
import FullscreenButton from './FullscreenButton';
|
||||||
import OverflowMenuButton from './OverflowMenuButton';
|
import OverflowMenuButton from './OverflowMenuButton';
|
||||||
import ProfileButton from './ProfileButton';
|
import ProfileButton from './ProfileButton';
|
||||||
import RaiseHandButton from './RaiseHandButton';
|
|
||||||
import Separator from './Separator';
|
import Separator from './Separator';
|
||||||
import ShareDesktopButton from './ShareDesktopButton';
|
import ShareDesktopButton from './ShareDesktopButton';
|
||||||
import VideoSettingsButton from './VideoSettingsButton';
|
import VideoSettingsButton from './VideoSettingsButton';
|
||||||
|
@ -256,7 +256,6 @@ class Toolbox extends Component<Props> {
|
||||||
this._onToolbarOpenVideoQuality = this._onToolbarOpenVideoQuality.bind(this);
|
this._onToolbarOpenVideoQuality = this._onToolbarOpenVideoQuality.bind(this);
|
||||||
this._onToolbarToggleChat = this._onToolbarToggleChat.bind(this);
|
this._onToolbarToggleChat = this._onToolbarToggleChat.bind(this);
|
||||||
this._onToolbarToggleFullScreen = this._onToolbarToggleFullScreen.bind(this);
|
this._onToolbarToggleFullScreen = this._onToolbarToggleFullScreen.bind(this);
|
||||||
this._onToolbarToggleRaiseHand = this._onToolbarToggleRaiseHand.bind(this);
|
|
||||||
this._onToolbarToggleScreenshare = this._onToolbarToggleScreenshare.bind(this);
|
this._onToolbarToggleScreenshare = this._onToolbarToggleScreenshare.bind(this);
|
||||||
this._onShortcutToggleTileView = this._onShortcutToggleTileView.bind(this);
|
this._onShortcutToggleTileView = this._onShortcutToggleTileView.bind(this);
|
||||||
this._onEscKey = this._onEscKey.bind(this);
|
this._onEscKey = this._onEscKey.bind(this);
|
||||||
|
@ -547,8 +546,7 @@ class Toolbox extends Component<Props> {
|
||||||
|
|
||||||
const raisehand = {
|
const raisehand = {
|
||||||
key: 'raisehand',
|
key: 'raisehand',
|
||||||
Content: RaiseHandButton,
|
Content: ReactionsMenuButton,
|
||||||
handleClick: this._onToolbarToggleRaiseHand,
|
|
||||||
group: 2
|
group: 2
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1024,23 +1022,6 @@ class Toolbox extends Component<Props> {
|
||||||
this._doToggleFullScreen();
|
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;
|
_onToolbarToggleScreenshare: () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1144,7 +1125,10 @@ class Toolbox extends Component<Props> {
|
||||||
ariaControls = 'overflow-menu'
|
ariaControls = 'overflow-menu'
|
||||||
isOpen = { _overflowMenuVisible }
|
isOpen = { _overflowMenuVisible }
|
||||||
key = 'overflow-menu'
|
key = 'overflow-menu'
|
||||||
onVisibilityChange = { this._onSetOverflowVisible }>
|
onVisibilityChange = { this._onSetOverflowVisible }
|
||||||
|
showMobileReactions = {
|
||||||
|
overflowMenuButtons.find(({ key }) => key === 'raisehand')
|
||||||
|
}>
|
||||||
<ul
|
<ul
|
||||||
aria-label = { t(toolbarAccLabel) }
|
aria-label = { t(toolbarAccLabel) }
|
||||||
className = 'overflow-menu'
|
className = 'overflow-menu'
|
||||||
|
@ -1154,15 +1138,15 @@ class Toolbox extends Component<Props> {
|
||||||
{overflowMenuButtons.map(({ group, key, Content, ...rest }, index, arr) => {
|
{overflowMenuButtons.map(({ group, key, Content, ...rest }, index, arr) => {
|
||||||
const showSeparator = index > 0 && arr[index - 1].group !== group;
|
const showSeparator = index > 0 && arr[index - 1].group !== group;
|
||||||
|
|
||||||
return (
|
return key !== 'raisehand'
|
||||||
<>
|
&& <>
|
||||||
{showSeparator && <Separator key = { `hr${group}` } />}
|
{showSeparator && <Separator key = { `hr${group}` } />}
|
||||||
<Content
|
<Content
|
||||||
{ ...rest }
|
{ ...rest }
|
||||||
key = { key }
|
key = { key }
|
||||||
showLabel = { true } />
|
showLabel = { true } />
|
||||||
</>
|
</>
|
||||||
);
|
;
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</OverflowMenuButton>
|
</OverflowMenuButton>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
SET_FULL_SCREEN
|
SET_FULL_SCREEN
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
|
|
||||||
|
|
||||||
declare var APP: Object;
|
declare var APP: Object;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,6 +19,7 @@ declare var APP: Object;
|
||||||
* @returns {Function}
|
* @returns {Function}
|
||||||
*/
|
*/
|
||||||
MiddlewareRegistry.register(store => next => action => {
|
MiddlewareRegistry.register(store => next => action => {
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case CLEAR_TOOLBOX_TIMEOUT: {
|
case CLEAR_TOOLBOX_TIMEOUT: {
|
||||||
const { timeoutID } = store.getState()['features/toolbox'];
|
const { timeoutID } = store.getState()['features/toolbox'];
|
||||||
|
|
Loading…
Reference in New Issue