Browse Source

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
j8
robertpin 3 years ago
parent
commit
601ee219e7
No account linked to committer's email address
50 changed files with 2231 additions and 361 deletions
  1. 7
    1
      css/_atlaskit_overrides.scss
  2. 7
    6
      css/_drawer.scss
  3. 189
    0
      css/_reactions-menu.scss
  4. 4
    1
      css/_toolbars.scss
  5. 1
    1
      css/filmstrip/_vertical_filmstrip.scss
  6. 1
    0
      css/main.scss
  7. 21
    0
      lang/main.json
  8. 5
    0
      modules/API/constants.js
  9. 19
    6
      modules/keyboardshortcut/keyboardshortcut.js
  10. 1
    0
      react/features/app/middlewares.any.js
  11. 1
    0
      react/features/app/reducers.any.js
  12. 8
    1
      react/features/base/dialog/components/AbstractDialogContainer.js
  13. 19
    4
      react/features/base/dialog/components/native/BottomSheet.js
  14. 32
    2
      react/features/base/dialog/components/native/DialogContainer.js
  15. 10
    6
      react/features/base/dialog/components/native/styles.js
  16. 61
    0
      react/features/chat/middleware.js
  17. 11
    1
      react/features/keyboard-shortcuts/components/KeyboardShortcutsDialog.web.js
  18. 55
    0
      react/features/reactions/actionTypes.js
  19. 108
    0
      react/features/reactions/actions.any.js
  20. 16
    0
      react/features/reactions/actions.web.js
  21. 1
    0
      react/features/reactions/components/_.native.js
  22. 1
    0
      react/features/reactions/components/_.web.js
  23. 1
    0
      react/features/reactions/components/index.js
  24. 165
    0
      react/features/reactions/components/native/RaiseHandButton.js
  25. 96
    0
      react/features/reactions/components/native/ReactionButton.js
  26. 96
    0
      react/features/reactions/components/native/ReactionEmoji.js
  27. 59
    0
      react/features/reactions/components/native/ReactionMenu.js
  28. 143
    0
      react/features/reactions/components/native/ReactionMenuDialog.js
  29. 17
    32
      react/features/reactions/components/native/ReactionsMenuButton.js
  30. 3
    0
      react/features/reactions/components/native/index.js
  31. 125
    0
      react/features/reactions/components/web/ReactionButton.js
  32. 96
    0
      react/features/reactions/components/web/ReactionEmoji.js
  33. 233
    0
      react/features/reactions/components/web/ReactionsMenu.js
  34. 139
    0
      react/features/reactions/components/web/ReactionsMenuButton.js
  35. 58
    0
      react/features/reactions/components/web/ReactionsMenuPopup.js
  36. 7
    0
      react/features/reactions/components/web/index.js
  37. 47
    0
      react/features/reactions/constants.js
  38. 11
    0
      react/features/reactions/functions.any.js
  39. 11
    0
      react/features/reactions/functions.web.js
  40. 84
    0
      react/features/reactions/middleware.js
  41. 90
    0
      react/features/reactions/reducer.js
  42. 0
    20
      react/features/toolbox/components/native/MoreOptionsButton.js
  43. 37
    97
      react/features/toolbox/components/native/OverflowMenu.js
  44. 4
    4
      react/features/toolbox/components/native/Toolbox.js
  45. 88
    7
      react/features/toolbox/components/native/styles.js
  46. 7
    60
      react/features/toolbox/components/web/Drawer.js
  47. 25
    4
      react/features/toolbox/components/web/OverflowMenuButton.js
  48. 0
    83
      react/features/toolbox/components/web/RaiseHandButton.js
  49. 9
    25
      react/features/toolbox/components/web/Toolbox.js
  50. 2
    0
      react/features/toolbox/middleware.js

+ 7
- 1
css/_atlaskit_overrides.scss View File

@@ -100,12 +100,18 @@
100 100
 }
101 101
 
102 102
 .audio-preview > div:nth-child(2),
103
-.video-preview > div:nth-child(2) {
103
+.video-preview > div:nth-child(2),
104
+.reactions-menu-popup > div:nth-child(2) {
104 105
     margin-bottom: 4px;
105 106
     outline: none;
106 107
     padding: 0;
107 108
 }
108 109
 
110
+.reactions-menu-popup > div:nth-child(2) {
111
+    margin-bottom: 6px;
112
+    box-shadow: none;
113
+}
114
+
109 115
 /**
110 116
  * The following selectors keep the chat modal full-size anywhere between 100px
111 117
  * and 580px for desktop or 680px for mobile.

+ 7
- 6
css/_drawer.scss View File

@@ -4,17 +4,16 @@
4 4
     right: 0;
5 5
     bottom: 0;
6 6
     z-index: $drawerZ;
7
+    background-color: #141414;
8
+    border-radius: 16px 16px 0 0;
7 9
 }
8 10
 
9 11
 .drawer-menu {
10
-    max-height: 50vh;
12
+    max-height: calc(80vh - 64px);
11 13
     background: #242528;
12 14
     border-radius: 16px 16px 0 0;
13
-    overflow-y: auto;
14
-
15
-    &.expanded {
16
-        max-height: 80vh;
17
-    }
15
+    overflow-y: hidden;
16
+    margin-bottom: env(safe-area-inset-bottom, 0);
18 17
 
19 18
     .drawer-toggle {
20 19
         display: flex;
@@ -42,6 +41,8 @@
42 41
         font-size: 1.2em;
43 42
         list-style-type: none;
44 43
         padding: 0;
44
+        height: calc(80vh - 144px - 64px);
45
+        overflow-y: auto;
45 46
 
46 47
         .overflow-menu-item {
47 48
             box-sizing: border-box;

+ 189
- 0
css/_reactions-menu.scss View File

@@ -0,0 +1,189 @@
1
+@use 'sass:math';
2
+
3
+.reactions-menu {
4
+	width: 280px;
5
+	background: #292929;
6
+	box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25);
7
+	border-radius: 3px;
8
+	padding: 16px;
9
+
10
+	&.overflow {
11
+		width: auto;
12
+		padding-bottom: max(env(safe-area-inset-bottom, 0), 16px);
13
+		background-color: #141414;
14
+		box-shadow: none;
15
+		border-radius: 0;
16
+		position: relative;
17
+
18
+		.toolbox-icon {
19
+			width: 48px;
20
+			height: 48px;
21
+
22
+			span.emoji {
23
+				width: 48px;
24
+				height: 48px;
25
+			}
26
+		}
27
+
28
+		.reactions-row {
29
+			display: flex;
30
+			flex-direction: row;
31
+			justify-content: space-around;
32
+
33
+			.toolbox-button {
34
+				margin-right: 0;
35
+			}
36
+		}
37
+	}
38
+
39
+	.toolbox-icon {
40
+		width: 40px;
41
+		height: 40px;
42
+		border-radius: 6px;
43
+
44
+		span.emoji {
45
+			width: 40px;
46
+			height: 40px;
47
+			font-size: 22px;
48
+			display: flex;
49
+			align-items: center;
50
+			justify-content: center;
51
+		}
52
+	}
53
+
54
+	.reactions-row {
55
+		.toolbox-button {
56
+			margin-right: 8px;
57
+			touch-action: manipulation;
58
+		}
59
+
60
+		.toolbox-button:last-of-type {
61
+			margin-right: 0;
62
+		}
63
+	}
64
+
65
+	.raise-hand-row {
66
+		margin-top: 16px;
67
+
68
+		.toolbox-button {
69
+			width: 100%;
70
+		}
71
+
72
+		.toolbox-icon {
73
+			width: 100%;
74
+			flex-direction: row;
75
+			align-items: center;
76
+
77
+			span.text {
78
+				font-style: normal;
79
+				font-weight: 600;
80
+				font-size: 14px;
81
+				line-height: 24px;
82
+				margin-left: 8px;
83
+			}
84
+		}
85
+	}
86
+}
87
+
88
+.reactions-animations-container {
89
+	position: absolute;
90
+	width: 20%;
91
+	bottom: 0;
92
+	left: 40%;
93
+	height: 48px;
94
+}
95
+
96
+.reactions-menu-popup-container,
97
+.reactions-menu-popup {
98
+	display: inline-block;
99
+	position: relative;
100
+}
101
+
102
+$reactionCount: 20;
103
+
104
+@function random($min, $max) {
105
+  @return math.random() * ($max - $min) + $min;
106
+}
107
+
108
+.reaction-emoji {
109
+	position: absolute;
110
+	font-size: 24px;
111
+	line-height: 32px;
112
+	width: 32px;
113
+	height: 32px;
114
+	top: 32px;
115
+	left: 10px;
116
+	opacity: 0;
117
+	z-index: 1;
118
+
119
+	&.reaction-0 {
120
+		animation: flowToRight 5s forwards ease-in-out;
121
+	}
122
+
123
+	@for $i from 1 through $reactionCount {
124
+	&.reaction-#{$i} {
125
+		animation: animation-#{$i} 5s forwards ease-in-out;
126
+		top: #{random(50, 0)}px;
127
+		left: #{random(-10, 10)}px;
128
+	}
129
+}
130
+}
131
+
132
+@keyframes flowToRight {
133
+	0% {
134
+		transform: translate(0px, 0px) scale(0.6);
135
+		opacity: 1;
136
+	}
137
+
138
+	70% {
139
+		transform: translate(40px, -70vh) scale(1.5);
140
+		opacity: 1;
141
+	}
142
+
143
+	75% {
144
+		transform: translate(40px, -70vh) scale(1.5);
145
+		opacity: 1;
146
+	}
147
+
148
+	100% {
149
+		transform: translate(140px, -50vh) scale(1);
150
+		opacity: 0;
151
+	}
152
+}
153
+
154
+@mixin animation-list {
155
+	@for $i from 1 through $reactionCount {
156
+		$topX: random(-100, 100);
157
+		$topY: random(65, 75);
158
+		$bottomX: random(150, 200);
159
+		$bottomY: random(40, 50);
160
+
161
+		@if $topX < 0 {
162
+			$bottomX: -$bottomX;
163
+		}
164
+
165
+		@keyframes animation-#{$i} {
166
+			0% {
167
+				transform: translate(0, 0) scale(0.6);
168
+				opacity: 1;
169
+			}
170
+
171
+			70% {
172
+				transform: translate(#{$topX}px, -#{$topY}vh) scale(1.5);
173
+				opacity: 1;
174
+			}
175
+
176
+			75% {
177
+				transform: translate(#{$topX}px, -#{$topY}vh) scale(1.5);
178
+				opacity: 1;
179
+			}
180
+
181
+			100% {
182
+				transform: translate(#{$bottomX}px, -#{$bottomY}vh) scale(1);
183
+				opacity: 0;
184
+			}
185
+		}
186
+	}
187
+}
188
+
189
+@include animation-list;

+ 4
- 1
css/_toolbars.scss View File

@@ -105,11 +105,14 @@
105 105
     margin: 0 auto;
106 106
     max-width: 100%;
107 107
     pointer-events: all;
108
+    background-color: #131519;
109
+    padding-bottom: env(safe-area-inset-bottom, 0);
110
+    box-shadow: 0px 2px 8px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.15);
111
+    border-radius: 6px;
108 112
 }
109 113
 
110 114
 .toolbox-content-items {
111 115
     background: $newToolbarBackgroundColor;
112
-    box-shadow: 0px 2px 8px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.15);
113 116
     border-radius: 6px;
114 117
     margin: 0 auto;
115 118
     padding: 6px;

+ 1
- 1
css/filmstrip/_vertical_filmstrip.scss View File

@@ -28,7 +28,7 @@
28 28
     flex-direction: column-reverse;
29 29
     height: 100%;
30 30
     width: 100%;
31
-    padding: ($desktopAppDragBarHeight - 5px) 5px 10px;
31
+    padding: ($desktopAppDragBarHeight - 5px) 5px calc(env(safe-area-inset-bottom, 0) + 10px);
32 32
     /**
33 33
      * fixed positioning is necessary for remote menus and tooltips to pop
34 34
      * out of the scrolling filmstrip. AtlasKit dialogs and tooltips use

+ 1
- 0
css/main.scss View File

@@ -106,6 +106,7 @@ $flagsImagePath: "../images/";
106 106
 @import 'connection-status';
107 107
 @import 'drawer';
108 108
 @import 'participants-pane';
109
+@import 'reactions-menu';
109 110
 @import 'plan-limit';
110 111
 
111 112
 /* Modules END */

+ 21
- 0
lang/main.json View File

@@ -808,6 +808,7 @@
808 808
             "callQuality": "Manage video quality",
809 809
             "cc": "Toggle subtitles",
810 810
             "chat": "Open / Close chat",
811
+            "clap": "Clap",
811 812
             "document": "Toggle shared document",
812 813
             "download": "Download our apps",
813 814
             "embedMeeting": "Embed meeting",
@@ -817,7 +818,9 @@
817 818
             "hangup": "Leave the meeting",
818 819
             "help": "Help",
819 820
             "invite": "Invite people",
821
+            "joy": "Laughing Crying",
820 822
             "kick": "Kick participant",
823
+            "like": "Thumbs Up",
821 824
             "lobbyButton": "Enable/disable lobby mode",
822 825
             "localRecording": "Toggle local recording controls",
823 826
             "lockRoom": "Toggle meeting password",
@@ -830,10 +833,12 @@
830 833
             "muteEveryonesVideo": "Disable everyone's camera",
831 834
             "muteEveryoneElsesVideo": "Disable everyone else's camera",
832 835
             "participants": "Participants",
836
+            "party": "Party Popper",
833 837
             "pip": "Toggle Picture-in-Picture mode",
834 838
             "privateMessage": "Send private message",
835 839
             "profile": "Edit your profile",
836 840
             "raiseHand": "Raise / Lower your hand",
841
+            "reactionsMenu": "Open / Close reactions menu",
837 842
             "recording": "Toggle recording",
838 843
             "remoteMute": "Mute participant",
839 844
             "remoteVideoMute": "Disable camera of participant",
@@ -845,7 +850,9 @@
845 850
             "shareYourScreen": "Start / Stop sharing your screen",
846 851
             "shortcuts": "Toggle shortcuts",
847 852
             "show": "Show on stage",
853
+            "smile": "Smile",
848 854
             "speakerStats": "Toggle speaker statistics",
855
+            "surprised": "Surprised",
849 856
             "tileView": "Toggle tile view",
850 857
             "toggleCamera": "Toggle camera",
851 858
             "toggleFilmstrip": "Toggle filmstrip",
@@ -864,7 +871,9 @@
864 871
         "authenticate": "Authenticate",
865 872
         "callQuality": "Manage video quality",
866 873
         "chat": "Open / Close chat",
874
+        "clap": "Clap",
867 875
         "closeChat": "Close chat",
876
+        "closeReactionsMenu": "Close reactions menu",
868 877
         "documentClose": "Close shared document",
869 878
         "documentOpen": "Open shared document",
870 879
         "download": "Download our apps",
@@ -878,6 +887,8 @@
878 887
         "hangup": "Leave the meeting",
879 888
         "help": "Help",
880 889
         "invite": "Invite people",
890
+        "joy": "Joy",
891
+        "like": "Thumbs Up",
881 892
         "lobbyButtonDisable": "Disable lobby mode",
882 893
         "lobbyButtonEnable": "Enable lobby mode",
883 894
         "login": "Login",
@@ -896,18 +907,27 @@
896 907
         "noisyAudioInputTitle": "Your microphone appears to be noisy!",
897 908
         "noisyAudioInputDesc": "It sounds like your microphone is making noise, please consider muting or changing the device.",
898 909
         "openChat": "Open chat",
910
+        "openReactionsMenu": "Open reactions menu",
899 911
         "participants": "Participants",
912
+        "party": "Celebration",
900 913
         "pip": "Enter Picture-in-Picture mode",
901 914
         "privateMessage": "Send private message",
902 915
         "profile": "Edit your profile",
903 916
         "raiseHand": "Raise / Lower your hand",
904 917
         "raiseYourHand": "Raise your hand",
918
+        "reactionClap": "Send clap reaction",
919
+        "reactionJoy": "Send joy reaction",
920
+        "reactionLike": "Send thumbs up reaction",
921
+        "reactionParty": "Send party popper reaction",
922
+        "reactionSmile": "Send smile reaction",
923
+        "reactionSurprised": "Send surprised reaction",
905 924
         "security": "Security options",
906 925
         "Settings": "Settings",
907 926
         "shareaudio": "Share audio",
908 927
         "sharedvideo": "Share video",
909 928
         "shareRoom": "Invite someone",
910 929
         "shortcuts": "View shortcuts",
930
+        "smile": "Smile",
911 931
         "speakerStats": "Speaker stats",
912 932
         "startScreenSharing": "Start screen sharing",
913 933
         "startSubtitles": "Start subtitles",
@@ -915,6 +935,7 @@
915 935
         "stopScreenSharing": "Stop screen sharing",
916 936
         "stopSubtitles": "Stop subtitles",
917 937
         "stopSharedVideo": "Stop video",
938
+        "surprised": "Surprised",
918 939
         "talkWhileMutedPopup": "Trying to speak? You are muted.",
919 940
         "tileViewToggle": "Toggle tile view",
920 941
         "toggleCamera": "Toggle camera",

+ 5
- 0
modules/API/constants.js View File

@@ -15,3 +15,8 @@ export const API_ID = parseURLParams(window.location).jitsi_meet_external_api_id
15 15
  * The payload name for the datachannel/endpoint text message event
16 16
  */
17 17
 export const ENDPOINT_TEXT_MESSAGE_NAME = 'endpoint-text-message';
18
+
19
+/**
20
+ * The payload name for the datachannel/endpoint reaction event
21
+ */
22
+export const ENDPOINT_REACTION_NAME = 'endpoint-reaction';

+ 19
- 6
modules/keyboardshortcut/keyboardshortcut.js View File

@@ -142,20 +142,23 @@ const KeyboardShortcut = {
142 142
      * @param exec the function to be executed when the shortcut is pressed
143 143
      * @param helpDescription the description of the shortcut that would appear
144 144
      * in the help menu
145
+     * @param altKey whether or not the alt key must be pressed.
145 146
      */
146 147
     registerShortcut(// eslint-disable-line max-params
147 148
             shortcutChar,
148 149
             shortcutAttr,
149 150
             exec,
150
-            helpDescription) {
151
-        _shortcuts.set(shortcutChar, {
151
+            helpDescription,
152
+            altKey = false) {
153
+        _shortcuts.set(altKey ? `:${shortcutChar}` : shortcutChar, {
152 154
             character: shortcutChar,
153 155
             function: exec,
154
-            shortcutAttr
156
+            shortcutAttr,
157
+            altKey
155 158
         });
156 159
 
157 160
         if (helpDescription) {
158
-            this._addShortcutToHelp(shortcutChar, helpDescription);
161
+            this._addShortcutToHelp(altKey ? `:${shortcutChar}` : shortcutChar, helpDescription);
159 162
         }
160 163
     },
161 164
 
@@ -164,9 +167,10 @@ const KeyboardShortcut = {
164 167
      *
165 168
      * @param shortcutChar unregisters the given shortcut, which means it will
166 169
      * no longer be usable
170
+     * @param altKey whether or not shortcut is combo with alt key
167 171
      */
168
-    unregisterShortcut(shortcutChar) {
169
-        _shortcuts.delete(shortcutChar);
172
+    unregisterShortcut(shortcutChar, altKey = false) {
173
+        _shortcuts.delete(altKey ? `:${shortcutChar}` : shortcutChar);
170 174
         _shortcutsHelp.delete(shortcutChar);
171 175
     },
172 176
 
@@ -175,6 +179,15 @@ const KeyboardShortcut = {
175 179
      * @returns {string} e.key or something close if not supported
176 180
      */
177 181
     _getKeyboardKey(e) {
182
+        // If alt is pressed a different char can be returned so this takes
183
+        // the char from the code. It also prefixes with a colon to differentiate
184
+        // alt combo from simple keypress.
185
+        if (e.altKey) {
186
+            const key = e.code.replace('Key', '');
187
+
188
+            return `:${key}`;
189
+        }
190
+
178 191
         // If e.key is a string, then it is assumed it already plainly states
179 192
         // the key pressed. This may not be true in all cases, such as with Edge
180 193
         // and "?", when the browser cannot properly map a key press event to a

+ 1
- 0
react/features/app/middlewares.any.js View File

@@ -35,6 +35,7 @@ import '../large-video/middleware';
35 35
 import '../lobby/middleware';
36 36
 import '../notifications/middleware';
37 37
 import '../overlay/middleware';
38
+import '../reactions/middleware';
38 39
 import '../recent-list/middleware';
39 40
 import '../recording/middleware';
40 41
 import '../rejoin/middleware';

+ 1
- 0
react/features/app/reducers.any.js View File

@@ -41,6 +41,7 @@ import '../large-video/reducer';
41 41
 import '../lobby/reducer';
42 42
 import '../notifications/reducer';
43 43
 import '../overlay/reducer';
44
+import '../reactions/reducer';
44 45
 import '../recent-list/reducer';
45 46
 import '../recording/reducer';
46 47
 import '../settings/reducer';

+ 8
- 1
react/features/base/dialog/components/AbstractDialogContainer.js View File

@@ -2,6 +2,8 @@
2 2
 
3 3
 import React, { Component } from 'react';
4 4
 
5
+import { type ReactionEmojiProps } from '../../../reactions/constants';
6
+
5 7
 /**
6 8
  * The type of the React {@code Component} props of {@link DialogContainer}.
7 9
  */
@@ -25,7 +27,12 @@ type Props = {
25 27
     /**
26 28
      * True if the UI is in a compact state where we don't show dialogs.
27 29
      */
28
-    _reducedUI: boolean
30
+    _reducedUI: boolean,
31
+
32
+    /**
33
+     * Array of reactions to be displayed.
34
+     */
35
+    _reactionsQueue: Array<ReactionEmojiProps>
29 36
 };
30 37
 
31 38
 /**

+ 19
- 4
react/features/base/dialog/components/native/BottomSheet.js View File

@@ -49,7 +49,17 @@ type Props = {
49 49
     /**
50 50
      * Function to render a bottom sheet header element, if necessary.
51 51
      */
52
-    renderHeader: ?Function
52
+    renderHeader: ?Function,
53
+
54
+    /**
55
+     * Function to render a bottom sheet footer element, if necessary.
56
+     */
57
+    renderFooter: ?Function,
58
+
59
+    /**
60
+    * The height of the screen.
61
+    */
62
+    _height: number
53 63
 };
54 64
 
55 65
 /**
@@ -80,7 +90,7 @@ class BottomSheet extends PureComponent<Props> {
80 90
      * @returns {ReactElement}
81 91
      */
82 92
     render() {
83
-        const { _styles, renderHeader } = this.props;
93
+        const { _styles, renderHeader, renderFooter, _height } = this.props;
84 94
 
85 95
         return (
86 96
             <SlidingView
@@ -99,7 +109,10 @@ class BottomSheet extends PureComponent<Props> {
99 109
                     <SafeAreaView
100 110
                         style = { [
101 111
                             styles.sheetItemContainer,
102
-                            _styles.sheet
112
+                            _styles.sheet,
113
+                            {
114
+                                maxHeight: _height - 100
115
+                            }
103 116
                         ] }
104 117
                         { ...this.panResponder.panHandlers }>
105 118
                         <ScrollView
@@ -108,6 +121,7 @@ class BottomSheet extends PureComponent<Props> {
108 121
                             style = { styles.scrollView } >
109 122
                             { this.props.children }
110 123
                         </ScrollView>
124
+                        { renderFooter && renderFooter() }
111 125
                     </SafeAreaView>
112 126
                 </View>
113 127
             </SlidingView>
@@ -167,7 +181,8 @@ class BottomSheet extends PureComponent<Props> {
167 181
  */
168 182
 function _mapStateToProps(state) {
169 183
     return {
170
-        _styles: ColorSchemeRegistry.get(state, 'BottomSheet')
184
+        _styles: ColorSchemeRegistry.get(state, 'BottomSheet'),
185
+        _height: state['features/base/responsive-ui'].clientHeight
171 186
     };
172 187
 }
173 188
 

+ 32
- 2
react/features/base/dialog/components/native/DialogContainer.js View File

@@ -1,3 +1,7 @@
1
+import React from 'react';
2
+
3
+import { ReactionEmoji } from '../../../../reactions/components';
4
+import { getReactionsQueue } from '../../../../reactions/functions.any';
1 5
 import { connect } from '../../../redux';
2 6
 import AbstractDialogContainer, {
3 7
     abstractMapStateToProps
@@ -11,6 +15,22 @@ import AbstractDialogContainer, {
11 15
  * @extends AbstractDialogContainer
12 16
  */
13 17
 class DialogContainer extends AbstractDialogContainer {
18
+
19
+    /**
20
+     * Returns the reactions to be displayed.
21
+     *
22
+     * @returns {Array<React$Element>}
23
+     */
24
+    _renderReactions() {
25
+        const { _reactionsQueue } = this.props;
26
+
27
+        return _reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
28
+            index = { index }
29
+            key = { uid }
30
+            reaction = { reaction }
31
+            uid = { uid } />));
32
+    }
33
+
14 34
     /**
15 35
      * Implements React's {@link Component#render()}.
16 36
      *
@@ -18,8 +38,18 @@ class DialogContainer extends AbstractDialogContainer {
18 38
      * @returns {ReactElement}
19 39
      */
20 40
     render() {
21
-        return this._renderDialogContent();
41
+        return (<React.Fragment>
42
+            {this._renderReactions()}
43
+            {this._renderDialogContent()}
44
+        </React.Fragment>);
22 45
     }
23 46
 }
24 47
 
25
-export default connect(abstractMapStateToProps)(DialogContainer);
48
+const mapStateToProps = state => {
49
+    return {
50
+        ...abstractMapStateToProps(state),
51
+        _reactionsQueue: getReactionsQueue(state)
52
+    };
53
+};
54
+
55
+export default connect(mapStateToProps)(DialogContainer);

+ 10
- 6
react/features/base/dialog/components/native/styles.js View File

@@ -33,7 +33,7 @@ export const bottomSheetStyles = {
33 33
     },
34 34
 
35 35
     scrollView: {
36
-        paddingHorizontal: MD_ITEM_MARGIN_PADDING
36
+        paddingHorizontal: 0
37 37
     },
38 38
 
39 39
     /**
@@ -117,7 +117,7 @@ const brandedDialogText = {
117 117
 };
118 118
 
119 119
 const brandedDialogLabelStyle = {
120
-    color: schemeColor('text'),
120
+    color: ColorPalette.white,
121 121
     flexShrink: 1,
122 122
     fontSize: MD_FONT_SIZE,
123 123
     opacity: 0.90
@@ -130,7 +130,7 @@ const brandedDialogItemContainerStyle = {
130 130
 };
131 131
 
132 132
 const brandedDialogIconStyle = {
133
-    color: schemeColor('icon'),
133
+    color: ColorPalette.white,
134 134
     fontSize: 24
135 135
 };
136 136
 
@@ -178,20 +178,24 @@ ColorSchemeRegistry.register('BottomSheet', {
178 178
          * Container style for a generic item rendered in the menu.
179 179
          */
180 180
         style: {
181
-            ...brandedDialogItemContainerStyle
181
+            ...brandedDialogItemContainerStyle,
182
+            backgroundColor: ColorPalette.darkBackground,
183
+            paddingHorizontal: MD_ITEM_MARGIN_PADDING
182 184
         },
183 185
 
184 186
         /**
185 187
          * Additional style that is not directly used as a style object.
186 188
          */
187
-        underlayColor: ColorPalette.overflowMenuItemUnderlay
189
+        underlayColor: ColorPalette.toggled
188 190
     },
189 191
 
190 192
     /**
191 193
      * Bottom sheet's base style.
192 194
      */
193 195
     sheet: {
194
-        backgroundColor: schemeColor('background')
196
+        backgroundColor: ColorPalette.black,
197
+        borderTopLeftRadius: 16,
198
+        borderTopRightRadius: 16
195 199
     }
196 200
 });
197 201
 

+ 61
- 0
react/features/chat/middleware.js View File

@@ -1,5 +1,8 @@
1 1
 // @flow
2 2
 
3
+import { batch } from 'react-redux';
4
+
5
+import { ENDPOINT_REACTION_NAME } from '../../../modules/API/constants';
3 6
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
4 7
 import {
5 8
     CONFERENCE_JOINED,
@@ -19,7 +22,18 @@ import {
19 22
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
20 23
 import { playSound, registerSound, unregisterSound } from '../base/sounds';
21 24
 import { openDisplayNamePrompt } from '../display-name';
25
+import { ADD_REACTIONS_MESSAGE } from '../reactions/actionTypes';
26
+import {
27
+    pushReaction
28
+} from '../reactions/actions.any';
29
+import { REACTIONS } from '../reactions/constants';
30
+import { endpointMessageReceived } from '../subtitles';
22 31
 import { showToolbox } from '../toolbox/actions';
32
+import {
33
+    hideToolbox,
34
+    setToolboxTimeout,
35
+    setToolboxVisible
36
+} from '../toolbox/actions.web';
23 37
 
24 38
 import { ADD_MESSAGE, SEND_MESSAGE, OPEN_CHAT, CLOSE_CHAT } from './actionTypes';
25 39
 import { addMessage, clearMessages } from './actions';
@@ -143,6 +157,15 @@ MiddlewareRegistry.register(store => next => action => {
143 157
         }
144 158
         break;
145 159
     }
160
+
161
+    case ADD_REACTIONS_MESSAGE: {
162
+        _handleReceivedMessage(store, {
163
+            id: localParticipant.id,
164
+            message: action.message,
165
+            privateMessage: false,
166
+            timestamp: Date.now()
167
+        });
168
+    }
146 169
     }
147 170
 
148 171
     return next(action);
@@ -189,6 +212,7 @@ StateListenerRegistry.register(
189 212
  * @returns {void}
190 213
  */
191 214
 function _addChatMsgListener(conference, store) {
215
+    const reactions = {};
192 216
 
193 217
     if (store.getState()['features/base/config'].iAmRecorder) {
194 218
         // We don't register anything on web if we are in iAmRecorder mode
@@ -219,6 +243,43 @@ function _addChatMsgListener(conference, store) {
219 243
         }
220 244
     );
221 245
 
246
+    conference.on(
247
+        JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
248
+        (...args) => {
249
+            store.dispatch(endpointMessageReceived(...args));
250
+
251
+            if (args && args.length >= 2) {
252
+                const [ { _id }, eventData ] = args;
253
+
254
+                if (eventData.name === ENDPOINT_REACTION_NAME) {
255
+                    reactions[_id] = reactions[_id] ?? {
256
+                        timeout: null,
257
+                        message: ''
258
+                    };
259
+                    batch(() => {
260
+                        store.dispatch(pushReaction(eventData.reaction));
261
+                        store.dispatch(setToolboxVisible(true));
262
+                        store.dispatch(setToolboxTimeout(
263
+                                () => store.dispatch(hideToolbox()),
264
+                                5000)
265
+                        );
266
+                    });
267
+
268
+                    clearTimeout(reactions[_id].timeout);
269
+                    reactions[_id].message = `${reactions[_id].message}${REACTIONS[eventData.reaction].message}`;
270
+                    reactions[_id].timeout = setTimeout(() => {
271
+                        _handleReceivedMessage(store, {
272
+                            id: _id,
273
+                            message: reactions[_id].message,
274
+                            privateMessage: false,
275
+                            timestamp: eventData.timestamp
276
+                        });
277
+                        delete reactions[_id];
278
+                    }, 500);
279
+                }
280
+            }
281
+        });
282
+
222 283
     conference.on(
223 284
         JitsiConferenceEvents.CONFERENCE_ERROR, (errorType, error) => {
224 285
             errorType === JitsiConferenceErrors.CHAT_ERROR && _handleChatError(store, error);

+ 11
- 1
react/features/keyboard-shortcuts/components/KeyboardShortcutsDialog.web.js View File

@@ -67,6 +67,14 @@ class KeyboardShortcutsDialog extends Component<Props> {
67 67
      * @returns {ReactElement}
68 68
      */
69 69
     _renderShortcutsListItem(keyboardKey, translationKey) {
70
+        let modifierKey = 'Alt';
71
+
72
+        if (window.navigator?.platform) {
73
+            if (window.navigator.platform.indexOf('Mac') !== -1) {
74
+                modifierKey = '⌥';
75
+            }
76
+        }
77
+
70 78
         return (
71 79
             <li
72 80
                 className = 'shortcuts-list__item'
@@ -78,7 +86,9 @@ class KeyboardShortcutsDialog extends Component<Props> {
78 86
                 </span>
79 87
                 <span className = 'item-action'>
80 88
                     <Lozenge isBold = { true }>
81
-                        { keyboardKey }
89
+                        { keyboardKey.startsWith(':')
90
+                            ? `${modifierKey} + ${keyboardKey.slice(1)}`
91
+                            : keyboardKey }
82 92
                     </Lozenge>
83 93
                 </span>
84 94
             </li>

+ 55
- 0
react/features/reactions/actionTypes.js View File

@@ -0,0 +1,55 @@
1
+/**
2
+ * The type of the (redux) action which shows/hides the reactions menu.
3
+ *
4
+ * {
5
+ *     type: TOGGLE_REACTIONS_VISIBLE,
6
+ *     visible: boolean
7
+ * }
8
+ */
9
+export const TOGGLE_REACTIONS_VISIBLE = 'TOGGLE_REACTIONS_VISIBLE';
10
+
11
+/**
12
+ * The type of the action which adds a new reaction to the reactions message and sets
13
+ * a new timeout.
14
+ *
15
+ * {
16
+ *     type: SET_REACTION_MESSAGE,
17
+ *     message: string,
18
+ *     timeoutID: number
19
+ * }
20
+ */
21
+export const SET_REACTIONS_MESSAGE = 'SET_REACTIONS_MESSAGE';
22
+
23
+/**
24
+ * The type of the action which resets the reactions message and timeout.
25
+ *
26
+ * {
27
+ *     type: CLEAR_REACTION_MESSAGE
28
+ * }
29
+ */
30
+export const CLEAR_REACTIONS_MESSAGE = 'CLEAR_REACTIONS_MESSAGE';
31
+
32
+/**
33
+ * The type of the action which sets the reactions queue.
34
+ *
35
+ * {
36
+ *     type: SET_REACTION_QUEUE,
37
+ *     value: Array
38
+ * }
39
+ */
40
+export const SET_REACTION_QUEUE = 'SET_REACTION_QUEUE';
41
+
42
+/**
43
+ * The type of the action which signals a send reaction to everyone in the conference.
44
+ */
45
+export const SEND_REACTION = 'SEND_REACTION';
46
+
47
+/**
48
+ * The type of the action to add a reaction message to the chat.
49
+ */
50
+export const ADD_REACTIONS_MESSAGE = 'ADD_REACTIONS_MESSAGE';
51
+
52
+/**
53
+ * The type of action to add a reaction to the queue.
54
+ */
55
+export const PUSH_REACTION = 'PUSH_REACTION';

+ 108
- 0
react/features/reactions/actions.any.js View File

@@ -0,0 +1,108 @@
1
+// @flow
2
+
3
+import {
4
+    ADD_REACTIONS_MESSAGE,
5
+    CLEAR_REACTIONS_MESSAGE,
6
+    PUSH_REACTION,
7
+    SEND_REACTION,
8
+    SET_REACTIONS_MESSAGE,
9
+    SET_REACTION_QUEUE
10
+} from './actionTypes';
11
+import { type ReactionEmojiProps } from './constants';
12
+
13
+/**
14
+ * Sets the reaction queue.
15
+ *
16
+ * @param {Array} value - The new queue.
17
+ * @returns {Function}
18
+ */
19
+export function setReactionQueue(value: Array<ReactionEmojiProps>) {
20
+    return {
21
+        type: SET_REACTION_QUEUE,
22
+        value
23
+    };
24
+}
25
+
26
+/**
27
+ * Appends the reactions message to the chat and resets the state.
28
+ *
29
+ * @returns {void}
30
+ */
31
+export function flushReactionsToChat() {
32
+    return {
33
+        type: CLEAR_REACTIONS_MESSAGE
34
+    };
35
+}
36
+
37
+/**
38
+ * Adds a new reaction to the reactions message.
39
+ *
40
+ * @param {boolean} value - The new reaction.
41
+ * @returns {Object}
42
+ */
43
+export function addReactionsMessage(value: string) {
44
+    return {
45
+        type: SET_REACTIONS_MESSAGE,
46
+        reaction: value
47
+    };
48
+}
49
+
50
+/**
51
+ * Adds a new reaction to the reactions message.
52
+ *
53
+ * @param {boolean} value - Reaction to be added to queue.
54
+ * @returns {Object}
55
+ */
56
+export function pushReaction(value: string) {
57
+    return {
58
+        type: PUSH_REACTION,
59
+        reaction: value
60
+    };
61
+}
62
+
63
+/**
64
+ * Removes a reaction from the queue.
65
+ *
66
+ * @param {number} uid - Id of the reaction to be removed.
67
+ * @returns {void}
68
+ */
69
+export function removeReaction(uid: number) {
70
+    return (dispatch: Function, getState: Function) => {
71
+        const queue = getState()['features/reactions'].queue;
72
+
73
+        dispatch(setReactionQueue(queue.filter(reaction => reaction.uid !== uid)));
74
+    };
75
+}
76
+
77
+
78
+/**
79
+ * Sends a reaction message to everyone in the conference.
80
+ *
81
+ * @param {string} reaction - The reaction to send out.
82
+ * @returns {{
83
+ *     type: SEND_REACTION,
84
+ *     reaction: string
85
+ * }}
86
+ */
87
+export function sendReaction(reaction: string) {
88
+    return {
89
+        type: SEND_REACTION,
90
+        reaction
91
+    };
92
+}
93
+
94
+/**
95
+ * Adds a reactions message to the chat.
96
+ *
97
+ * @param {string} message - The reactions message to add to chat.
98
+ * @returns {{
99
+ *     type: ADD_REACTIONS_MESSAGE,
100
+ *     message: string
101
+ * }}
102
+ */
103
+export function addReactionsMessageToChat(message: string) {
104
+    return {
105
+        type: ADD_REACTIONS_MESSAGE,
106
+        message
107
+    };
108
+}

+ 16
- 0
react/features/reactions/actions.web.js View File

@@ -0,0 +1,16 @@
1
+// @flow
2
+
3
+import {
4
+    TOGGLE_REACTIONS_VISIBLE
5
+} from './actionTypes';
6
+
7
+/**
8
+ * Toggles the visibility of the reactions menu.
9
+ *
10
+ * @returns {Function}
11
+ */
12
+export function toggleReactionsMenuVisibility() {
13
+    return {
14
+        type: TOGGLE_REACTIONS_VISIBLE
15
+    };
16
+}

+ 1
- 0
react/features/reactions/components/_.native.js View File

@@ -0,0 +1 @@
1
+export * from './native';

+ 1
- 0
react/features/reactions/components/_.web.js View File

@@ -0,0 +1 @@
1
+export * from './web';

+ 1
- 0
react/features/reactions/components/index.js View File

@@ -0,0 +1 @@
1
+export * from './_';

+ 165
- 0
react/features/reactions/components/native/RaiseHandButton.js View File

@@ -0,0 +1,165 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { Text, TouchableHighlight, View } from 'react-native';
5
+import { type Dispatch } from 'redux';
6
+
7
+import {
8
+    createToolbarEvent,
9
+    sendAnalytics
10
+} from '../../../analytics';
11
+import { ColorSchemeRegistry } from '../../../base/color-scheme';
12
+import { translate } from '../../../base/i18n';
13
+import {
14
+    getLocalParticipant,
15
+    raiseHand
16
+} from '../../../base/participants';
17
+import { connect } from '../../../base/redux';
18
+import { type AbstractButtonProps } from '../../../base/toolbox/components';
19
+
20
+import { type ReactionStyles } from './ReactionButton';
21
+
22
+/**
23
+ * The type of the React {@code Component} props of {@link RaiseHandButton}.
24
+ */
25
+type Props = AbstractButtonProps & {
26
+
27
+    /**
28
+     * The local participant.
29
+     */
30
+    _localParticipant: Object,
31
+
32
+    /**
33
+     * Whether the participant raused their hand or not.
34
+     */
35
+    _raisedHand: boolean,
36
+
37
+    /**
38
+     * The redux {@code dispatch} function.
39
+     */
40
+    dispatch: Dispatch<any>,
41
+
42
+    /**
43
+     * Used for translation
44
+     */
45
+    t: Function,
46
+
47
+    /**
48
+     * Used to close the overflow menu after raise hand is clicked.
49
+     */
50
+    onCancel: Function,
51
+
52
+    /**
53
+     * Styles for the button.
54
+     */
55
+    _styles: ReactionStyles
56
+};
57
+
58
+/**
59
+ * An implementation of a button to raise or lower hand.
60
+ */
61
+class RaiseHandButton extends Component<Props, *> {
62
+    accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
63
+    label = 'toolbar.raiseYourHand';
64
+    toggledLabel = 'toolbar.lowerYourHand';
65
+
66
+    /**
67
+     * Initializes a new {@code RaiseHandButton} instance.
68
+     *
69
+     * @param {Props} props - The React {@code Component} props to initialize
70
+     * the new {@code RaiseHandButton} instance with.
71
+     */
72
+    constructor(props: Props) {
73
+        super(props);
74
+
75
+        // Bind event handlers so they are only bound once per instance.
76
+        this._onClick = this._onClick.bind(this);
77
+        this._toggleRaisedHand = this._toggleRaisedHand.bind(this);
78
+        this._getLabel = this._getLabel.bind(this);
79
+    }
80
+
81
+    _onClick: () => void;
82
+
83
+    _toggleRaisedHand: () => void;
84
+
85
+    _getLabel: () => string;
86
+
87
+    /**
88
+     * Handles clicking / pressing the button.
89
+     *
90
+     * @returns {void}
91
+     */
92
+    _onClick() {
93
+        this._toggleRaisedHand();
94
+        this.props.onCancel();
95
+    }
96
+
97
+    /**
98
+     * Toggles the rased hand status of the local participant.
99
+     *
100
+     * @returns {void}
101
+     */
102
+    _toggleRaisedHand() {
103
+        const enable = !this.props._raisedHand;
104
+
105
+        sendAnalytics(createToolbarEvent('raise.hand', { enable }));
106
+
107
+        this.props.dispatch(raiseHand(enable));
108
+    }
109
+
110
+    /**
111
+     * Gets the current label, taking the toggled state into account. If no
112
+     * toggled label is provided, the regular label will also be used in the
113
+     * toggled state.
114
+     *
115
+     * @returns {string}
116
+     */
117
+    _getLabel() {
118
+        const { _raisedHand, t } = this.props;
119
+
120
+        return t(_raisedHand ? this.toggledLabel : this.label);
121
+    }
122
+
123
+    /**
124
+     * Implements React's {@link Component#render()}.
125
+     *
126
+     * @inheritdoc
127
+     * @returns {ReactElement}
128
+     */
129
+    render() {
130
+        const { _styles, t } = this.props;
131
+
132
+        return (
133
+            <TouchableHighlight
134
+                accessibilityLabel = { t(this.accessibilityLabel) }
135
+                accessibilityRole = 'button'
136
+                onPress = { this._onClick }
137
+                style = { _styles.style }
138
+                underlayColor = { _styles.underlayColor }>
139
+                <View style = { _styles.container }>
140
+                    <Text style = { _styles.emoji }>✋</Text>
141
+                    <Text style = { _styles.text }>{this._getLabel()}</Text>
142
+                </View>
143
+            </TouchableHighlight>
144
+        );
145
+    }
146
+}
147
+
148
+/**
149
+ * Maps part of the Redux state to the props of this component.
150
+ *
151
+ * @param {Object} state - The Redux state.
152
+ * @private
153
+ * @returns {Props}
154
+ */
155
+function _mapStateToProps(state): Object {
156
+    const _localParticipant = getLocalParticipant(state);
157
+
158
+    return {
159
+        _localParticipant,
160
+        _raisedHand: _localParticipant.raisedHand,
161
+        _styles: ColorSchemeRegistry.get(state, 'Toolbox').raiseHandButton
162
+    };
163
+}
164
+
165
+export default translate(connect(_mapStateToProps)(RaiseHandButton));

+ 96
- 0
react/features/reactions/components/native/ReactionButton.js View File

@@ -0,0 +1,96 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import { Text, TouchableHighlight } from 'react-native';
5
+import { useDispatch } from 'react-redux';
6
+
7
+import { translate } from '../../../base/i18n';
8
+import type { StyleType } from '../../../base/styles';
9
+import { sendReaction } from '../../actions.any';
10
+import { REACTIONS } from '../../constants';
11
+
12
+
13
+export type ReactionStyles = {
14
+
15
+    /**
16
+     * Style for the button.
17
+     */
18
+    style: StyleType,
19
+
20
+    /**
21
+     * Underlay color for the button.
22
+     */
23
+    underlayColor: StyleType,
24
+
25
+    /**
26
+     * Style for the emoji text on the button.
27
+     */
28
+    emoji: StyleType,
29
+
30
+    /**
31
+     * Style for the label text on the button.
32
+     */
33
+    text?: StyleType,
34
+
35
+    /**
36
+     * Style for text container. Used on raise hand button.
37
+     */
38
+    container?: StyleType
39
+
40
+}
41
+
42
+/**
43
+ * The type of the React {@code Component} props of {@link ReactionButton}.
44
+ */
45
+type Props = {
46
+
47
+    /**
48
+     * Collection of styles for the button.
49
+     */
50
+    styles: ReactionStyles,
51
+
52
+    /**
53
+     * The reaction to be sent
54
+     */
55
+    reaction: string,
56
+
57
+    /**
58
+     * Invoked to obtain translated strings.
59
+     */
60
+    t: Function
61
+};
62
+
63
+/**
64
+ * An implementation of a button to send a reaction.
65
+ *
66
+ * @returns {ReactElement}
67
+ */
68
+function ReactionButton({
69
+    styles,
70
+    reaction,
71
+    t
72
+}: Props) {
73
+    const dispatch = useDispatch();
74
+
75
+    /**
76
+     * Handles clicking / pressing the button.
77
+     *
78
+     * @returns {void}
79
+     */
80
+    function _onClick() {
81
+        dispatch(sendReaction(reaction));
82
+    }
83
+
84
+    return (
85
+        <TouchableHighlight
86
+            accessibilityLabel = { t(`toolbar.accessibilityLabel.${reaction}`) }
87
+            accessibilityRole = 'button'
88
+            onPress = { _onClick }
89
+            style = { styles.style }
90
+            underlayColor = { styles.underlayColor }>
91
+            <Text style = { styles.emoji }>{REACTIONS[reaction].emoji}</Text>
92
+        </TouchableHighlight>
93
+    );
94
+}
95
+
96
+export default translate(ReactionButton);

+ 96
- 0
react/features/reactions/components/native/ReactionEmoji.js View File

@@ -0,0 +1,96 @@
1
+// @flow
2
+
3
+import React, { useEffect, useMemo, useRef, useState } from 'react';
4
+import { Animated } from 'react-native';
5
+import { useDispatch, useSelector } from 'react-redux';
6
+
7
+import { ColorSchemeRegistry } from '../../../base/color-scheme';
8
+import { removeReaction } from '../../actions.any';
9
+import { REACTIONS, type ReactionEmojiProps } from '../../constants';
10
+
11
+
12
+type Props = ReactionEmojiProps & {
13
+
14
+    /**
15
+     * Index of reaction on the queue.
16
+     * Used to differentiate between first and other animations.
17
+     */
18
+    index: number
19
+};
20
+
21
+
22
+/**
23
+ * Animated reaction emoji.
24
+ *
25
+ * @returns {ReactElement}
26
+ */
27
+function ReactionEmoji({ reaction, uid, index }: Props) {
28
+    const _styles = useSelector(state => ColorSchemeRegistry.get(state, 'Toolbox'));
29
+    const _height = useSelector(state => state['features/base/responsive-ui'].clientHeight);
30
+    const dispatch = useDispatch();
31
+
32
+    const animationVal = useRef(new Animated.Value(0)).current;
33
+
34
+    const vh = useState(_height / 100)[0];
35
+
36
+    const randomInt = (min, max) => Math.floor((Math.random() * (max - min + 1)) + min);
37
+
38
+    const animationIndex = useMemo(() => index % 21, [ index ]);
39
+
40
+    const coordinates = useState({
41
+        topX: animationIndex === 0 ? 40 : randomInt(-100, 100),
42
+        topY: animationIndex === 0 ? -70 : randomInt(-65, -75),
43
+        bottomX: animationIndex === 0 ? 140 : randomInt(150, 200),
44
+        bottomY: animationIndex === 0 ? -50 : randomInt(-40, -50)
45
+    })[0];
46
+
47
+
48
+    useEffect(() => {
49
+        setTimeout(() => dispatch(removeReaction(uid)), 5000);
50
+    }, []);
51
+
52
+    useEffect(() => {
53
+        Animated.timing(
54
+            animationVal,
55
+            {
56
+                toValue: 1,
57
+                duration: 5000,
58
+                useNativeDriver: true
59
+            }
60
+        ).start();
61
+    }, [ animationVal ]);
62
+
63
+
64
+    return (
65
+        <Animated.Text
66
+            style = {{
67
+                ..._styles.emojiAnimation,
68
+                transform: [
69
+                    { translateY: animationVal.interpolate({
70
+                        inputRange: [ 0, 0.70, 0.75, 1 ],
71
+                        outputRange: [ 0, coordinates.topY * vh, coordinates.topY * vh, coordinates.bottomY * vh ]
72
+                    })
73
+                    }, {
74
+                        translateX: animationVal.interpolate({
75
+                            inputRange: [ 0, 0.70, 0.75, 1 ],
76
+                            outputRange: [ 0, coordinates.topX, coordinates.topX,
77
+                                coordinates.topX < 0 ? -coordinates.bottomX : coordinates.bottomX ]
78
+                        })
79
+                    }, {
80
+                        scale: animationVal.interpolate({
81
+                            inputRange: [ 0, 0.70, 0.75, 1 ],
82
+                            outputRange: [ 0.6, 1.5, 1.5, 1 ]
83
+                        })
84
+                    }
85
+                ],
86
+                opacity: animationVal.interpolate({
87
+                    inputRange: [ 0, 0.7, 0.75, 1 ],
88
+                    outputRange: [ 1, 1, 1, 0 ]
89
+                })
90
+            }}>
91
+            {REACTIONS[reaction].emoji}
92
+        </Animated.Text>
93
+    );
94
+}
95
+
96
+export default ReactionEmoji;

+ 59
- 0
react/features/reactions/components/native/ReactionMenu.js View File

@@ -0,0 +1,59 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import { View } from 'react-native';
5
+import { useSelector } from 'react-redux';
6
+
7
+import { ColorSchemeRegistry } from '../../../base/color-scheme';
8
+import { getParticipantCount } from '../../../base/participants';
9
+import { REACTIONS } from '../../constants';
10
+
11
+import RaiseHandButton from './RaiseHandButton';
12
+import ReactionButton from './ReactionButton';
13
+
14
+/**
15
+ * The type of the React {@code Component} props of {@link ReactionMenu}.
16
+ */
17
+type Props = {
18
+
19
+    /**
20
+     * Used to close the overflow menu after raise hand is clicked.
21
+     */
22
+    onCancel: Function,
23
+
24
+    /**
25
+     * Whether or not it's displayed in the overflow menu.
26
+     */
27
+    overflowMenu: boolean
28
+};
29
+
30
+/**
31
+ * Animated reaction emoji.
32
+ *
33
+ * @returns {ReactElement}
34
+ */
35
+function ReactionMenu({
36
+    onCancel,
37
+    overflowMenu
38
+}: Props) {
39
+    const _styles = useSelector(state => ColorSchemeRegistry.get(state, 'Toolbox'));
40
+    const _participantCount = useSelector(state => getParticipantCount(state));
41
+
42
+    return (
43
+        <View style = { overflowMenu ? _styles.overflowReactionMenu : _styles.reactionMenu }>
44
+            {_participantCount > 1
45
+                && <View style = { _styles.reactionRow }>
46
+                    {Object.keys(REACTIONS).map(key => (
47
+                        <ReactionButton
48
+                            key = { key }
49
+                            reaction = { key }
50
+                            styles = { _styles.reactionButton } />
51
+                    ))}
52
+                </View>
53
+            }
54
+            <RaiseHandButton onCancel = { onCancel } />
55
+        </View>
56
+    );
57
+}
58
+
59
+export default ReactionMenu;

+ 143
- 0
react/features/reactions/components/native/ReactionMenuDialog.js View File

@@ -0,0 +1,143 @@
1
+// @flow
2
+
3
+import React, { PureComponent } from 'react';
4
+import { SafeAreaView, TouchableWithoutFeedback, View } from 'react-native';
5
+
6
+import { ColorSchemeRegistry } from '../../../base/color-scheme';
7
+import { hideDialog, isDialogOpen } from '../../../base/dialog';
8
+import { getParticipantCount } from '../../../base/participants';
9
+import { connect } from '../../../base/redux';
10
+import type { StyleType } from '../../../base/styles';
11
+
12
+import ReactionMenu from './ReactionMenu';
13
+
14
+/**
15
+ * The type of the React {@code Component} props of {@link ReactionMenuDialog}.
16
+ */
17
+type Props = {
18
+
19
+    /**
20
+     * The color-schemed stylesheet of the feature.
21
+     */
22
+    _styles: StyleType,
23
+
24
+    /**
25
+     * True if the dialog is currently visible, false otherwise.
26
+     */
27
+    _isOpen: boolean,
28
+
29
+    /**
30
+     * The width of the screen.
31
+     */
32
+    _width: number,
33
+
34
+    /**
35
+    * The height of the screen.
36
+    */
37
+    _height: number,
38
+
39
+    /**
40
+     * Number of conference participants.
41
+     */
42
+    _participantCount: number,
43
+
44
+    /**
45
+     * Used for hiding the dialog when the selection was completed.
46
+     */
47
+    dispatch: Function
48
+};
49
+
50
+/**
51
+ * The exported React {@code Component}. We need it to execute
52
+ * {@link hideDialog}.
53
+ *
54
+ * XXX It does not break our coding style rule to not utilize globals for state,
55
+ * because it is merely another name for {@code export}'s {@code default}.
56
+ */
57
+let ReactionMenu_; // eslint-disable-line prefer-const
58
+
59
+/**
60
+ * Implements a React {@code Component} with some extra actions in addition to
61
+ * those in the toolbar.
62
+ */
63
+class ReactionMenuDialog extends PureComponent<Props> {
64
+    /**
65
+     * Initializes a new {@code ReactionMenuDialog} instance.
66
+     *
67
+     * @inheritdoc
68
+     */
69
+    constructor(props: Props) {
70
+        super(props);
71
+
72
+        // Bind event handlers so they are only bound once per instance.
73
+        this._onCancel = this._onCancel.bind(this);
74
+    }
75
+
76
+    /**
77
+     * Implements React's {@link Component#render()}.
78
+     *
79
+     * @inheritdoc
80
+     * @returns {ReactElement}
81
+     */
82
+    render() {
83
+        const { _styles, _width, _height, _participantCount } = this.props;
84
+
85
+        return (
86
+            <SafeAreaView style = { _styles }>
87
+                <TouchableWithoutFeedback
88
+                    onPress = { this._onCancel }>
89
+                    <View style = { _styles }>
90
+                        <View
91
+                            style = {{
92
+                                left: (_width - 360) / 2,
93
+                                top: _height - (_participantCount > 1 ? 144 : 80) - 80
94
+                            }}>
95
+                            <ReactionMenu
96
+                                onCancel = { this._onCancel }
97
+                                overflowMenu = { false } />
98
+                        </View>
99
+                    </View>
100
+                </TouchableWithoutFeedback>
101
+            </SafeAreaView>
102
+        );
103
+    }
104
+
105
+    _onCancel: () => boolean;
106
+
107
+    /**
108
+     * Hides this {@code ReactionMenuDialog}.
109
+     *
110
+     * @private
111
+     * @returns {boolean}
112
+     */
113
+    _onCancel() {
114
+        if (this.props._isOpen) {
115
+            this.props.dispatch(hideDialog(ReactionMenu_));
116
+
117
+            return true;
118
+        }
119
+
120
+        return false;
121
+    }
122
+}
123
+
124
+/**
125
+ * Function that maps parts of Redux state tree into component props.
126
+ *
127
+ * @param {Object} state - Redux state.
128
+ * @private
129
+ * @returns {Props}
130
+ */
131
+function _mapStateToProps(state) {
132
+    return {
133
+        _isOpen: isDialogOpen(state, ReactionMenu_),
134
+        _styles: ColorSchemeRegistry.get(state, 'Toolbox').reactionDialog,
135
+        _width: state['features/base/responsive-ui'].clientWidth,
136
+        _height: state['features/base/responsive-ui'].clientHeight,
137
+        _participantCount: getParticipantCount(state)
138
+    };
139
+}
140
+
141
+ReactionMenu_ = connect(_mapStateToProps)(ReactionMenuDialog);
142
+
143
+export default ReactionMenu_;

react/features/toolbox/components/native/RaiseHandButton.js → react/features/reactions/components/native/ReactionsMenuButton.js View File

@@ -2,34 +2,32 @@
2 2
 
3 3
 import { type Dispatch } from 'redux';
4 4
 
5
-import {
6
-    createToolbarEvent,
7
-    sendAnalytics
8
-} from '../../../analytics';
5
+import { isDialogOpen, openDialog } from '../../../base/dialog';
9 6
 import { RAISE_HAND_ENABLED, getFeatureFlag } from '../../../base/flags';
10 7
 import { translate } from '../../../base/i18n';
11 8
 import { IconRaisedHand } from '../../../base/icons';
12 9
 import {
13
-    getLocalParticipant,
14
-    raiseHand
10
+    getLocalParticipant
15 11
 } from '../../../base/participants';
16 12
 import { connect } from '../../../base/redux';
17 13
 import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
18 14
 
15
+import ReactionMenuDialog from './ReactionMenuDialog';
16
+
19 17
 /**
20
- * The type of the React {@code Component} props of {@link RaiseHandButton}.
18
+ * The type of the React {@code Component} props of {@link ReactionsMenuButton}.
21 19
  */
22 20
 type Props = AbstractButtonProps & {
23 21
 
24 22
     /**
25
-     * The local participant.
23
+     * Whether the participant raised their hand or not.
26 24
      */
27
-    _localParticipant: Object,
25
+    _raisedHand: boolean,
28 26
 
29 27
     /**
30
-     * Whether the participant raused their hand or not.
28
+     * Whether or not the reactions menu is open.
31 29
      */
32
-    _raisedHand: boolean,
30
+    _reactionsOpen: boolean,
33 31
 
34 32
     /**
35 33
      * The redux {@code dispatch} function.
@@ -40,11 +38,11 @@ type Props = AbstractButtonProps & {
40 38
 /**
41 39
  * An implementation of a button to raise or lower hand.
42 40
  */
43
-class RaiseHandButton extends AbstractButton<Props, *> {
44
-    accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
41
+class ReactionsMenuButton extends AbstractButton<Props, *> {
42
+    accessibilityLabel = 'toolbar.accessibilityLabel.reactionsMenu';
45 43
     icon = IconRaisedHand;
46
-    label = 'toolbar.raiseYourHand';
47
-    toggledLabel = 'toolbar.lowerYourHand';
44
+    label = 'toolbar.openReactionsMenu';
45
+    toggledLabel = 'toolbar.closeReactionsMenu';
48 46
 
49 47
     /**
50 48
      * Handles clicking / pressing the button.
@@ -54,7 +52,7 @@ class RaiseHandButton extends AbstractButton<Props, *> {
54 52
      * @returns {void}
55 53
      */
56 54
     _handleClick() {
57
-        this._toggleRaisedHand();
55
+        this.props.dispatch(openDialog(ReactionMenuDialog));
58 56
     }
59 57
 
60 58
     /**
@@ -65,20 +63,7 @@ class RaiseHandButton extends AbstractButton<Props, *> {
65 63
      * @returns {boolean}
66 64
      */
67 65
     _isToggled() {
68
-        return this.props._raisedHand;
69
-    }
70
-
71
-    /**
72
-     * Toggles the rased hand status of the local participant.
73
-     *
74
-     * @returns {void}
75
-     */
76
-    _toggleRaisedHand() {
77
-        const enable = !this.props._raisedHand;
78
-
79
-        sendAnalytics(createToolbarEvent('raise.hand', { enable }));
80
-
81
-        this.props.dispatch(raiseHand(enable));
66
+        return this.props._raisedHand || this.props._reactionsOpen;
82 67
     }
83 68
 }
84 69
 
@@ -96,10 +81,10 @@ function _mapStateToProps(state, ownProps): Object {
96 81
     const { visible = enabled } = ownProps;
97 82
 
98 83
     return {
99
-        _localParticipant,
100 84
         _raisedHand: _localParticipant.raisedHand,
85
+        _reactionsOpen: isDialogOpen(state, ReactionMenuDialog),
101 86
         visible
102 87
     };
103 88
 }
104 89
 
105
-export default translate(connect(_mapStateToProps)(RaiseHandButton));
90
+export default translate(connect(_mapStateToProps)(ReactionsMenuButton));

+ 3
- 0
react/features/reactions/components/native/index.js View File

@@ -0,0 +1,3 @@
1
+export { default as ReactionsMenuButton } from './ReactionsMenuButton';
2
+export { default as ReactionEmoji } from './ReactionEmoji';
3
+export { default as ReactionMenu } from './ReactionMenu';

+ 125
- 0
react/features/reactions/components/web/ReactionButton.js View File

@@ -0,0 +1,125 @@
1
+/* @flow */
2
+
3
+import React from 'react';
4
+
5
+import { Tooltip } from '../../../base/tooltip';
6
+import AbstractToolbarButton from '../../../toolbox/components/AbstractToolbarButton';
7
+import type { Props as AbstractToolbarButtonProps } from '../../../toolbox/components/AbstractToolbarButton';
8
+
9
+/**
10
+ * The type of the React {@code Component} props of {@link ReactionButton}.
11
+ */
12
+type Props = AbstractToolbarButtonProps & {
13
+
14
+    /**
15
+     * Optional text to display in the tooltip.
16
+     */
17
+    tooltip?: string,
18
+
19
+    /**
20
+     * From which direction the tooltip should appear, relative to the
21
+     * button.
22
+     */
23
+    tooltipPosition: string,
24
+
25
+    /**
26
+     * Optional label for the button
27
+     */
28
+    label?: string
29
+};
30
+
31
+/**
32
+ * Represents a button in the reactions menu.
33
+ *
34
+ * @extends AbstractToolbarButton
35
+ */
36
+class ReactionButton extends AbstractToolbarButton<Props> {
37
+    /**
38
+     * Default values for {@code ReactionButton} component's properties.
39
+     *
40
+     * @static
41
+     */
42
+    static defaultProps = {
43
+        tooltipPosition: 'top'
44
+    };
45
+
46
+    /**
47
+     * Initializes a new {@code ReactionButton} instance.
48
+     *
49
+     * @inheritdoc
50
+     */
51
+    constructor(props: Props) {
52
+        super(props);
53
+
54
+        this._onKeyDown = this._onKeyDown.bind(this);
55
+    }
56
+
57
+    _onKeyDown: (Object) => void;
58
+
59
+    /**
60
+     * Handles 'Enter' key on the button to trigger onClick for accessibility.
61
+     * We should be handling Space onKeyUp but it conflicts with PTT.
62
+     *
63
+     * @param {Object} event - The key event.
64
+     * @private
65
+     * @returns {void}
66
+     */
67
+    _onKeyDown(event) {
68
+        // If the event coming to the dialog has been subject to preventDefault
69
+        // we don't handle it here.
70
+        if (event.defaultPrevented) {
71
+            return;
72
+        }
73
+
74
+        if (event.key === 'Enter') {
75
+            event.preventDefault();
76
+            event.stopPropagation();
77
+            this.props.onClick();
78
+        }
79
+    }
80
+
81
+    /**
82
+     * Renders the button of this {@code ReactionButton}.
83
+     *
84
+     * @param {Object} children - The children, if any, to be rendered inside
85
+     * the button. Presumably, contains the emoji of this {@code ReactionButton}.
86
+     * @protected
87
+     * @returns {ReactElement} The button of this {@code ReactionButton}.
88
+     */
89
+    _renderButton(children) {
90
+        return (
91
+            <div
92
+                aria-label = { this.props.accessibilityLabel }
93
+                aria-pressed = { this.props.toggled }
94
+                className = 'toolbox-button'
95
+                onClick = { this.props.onClick }
96
+                onKeyDown = { this._onKeyDown }
97
+                role = 'button'
98
+                tabIndex = { 0 }>
99
+                { this.props.tooltip
100
+                    ? <Tooltip
101
+                        content = { this.props.tooltip }
102
+                        position = { this.props.tooltipPosition }>
103
+                        { children }
104
+                    </Tooltip>
105
+                    : children }
106
+            </div>
107
+        );
108
+    }
109
+
110
+    /**
111
+     * Renders the icon (emoji) of this {@code reactionButton}.
112
+     *
113
+     * @inheritdoc
114
+     */
115
+    _renderIcon() {
116
+        return (
117
+            <div className = { `toolbox-icon ${this.props.toggled ? 'toggled' : ''}` }>
118
+                <span className = 'emoji'>{this.props.icon}</span>
119
+                {this.props.label && <span className = 'text'>{this.props.label}</span>}
120
+            </div>
121
+        );
122
+    }
123
+}
124
+
125
+export default ReactionButton;

+ 96
- 0
react/features/reactions/components/web/ReactionEmoji.js View File

@@ -0,0 +1,96 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+
5
+import { connect } from '../../../base/redux';
6
+import { removeReaction } from '../../actions.any';
7
+import { REACTIONS } from '../../constants';
8
+
9
+type Props = {
10
+
11
+    /**
12
+     * Reaction to be displayed.
13
+     */
14
+    reaction: string,
15
+
16
+    /**
17
+     * Id of the reaction.
18
+     */
19
+    uid: Number,
20
+
21
+    /**
22
+     * Removes reaction from redux state.
23
+     */
24
+    removeReaction: Function,
25
+
26
+    /**
27
+     * Index of the reaction in the queue.
28
+     */
29
+    index: number
30
+};
31
+
32
+type State = {
33
+
34
+    /**
35
+     * Index of CSS animation. Number between 0-20.
36
+     */
37
+    index: number
38
+}
39
+
40
+
41
+/**
42
+ * Used to display animated reactions.
43
+ *
44
+ * @returns {ReactElement}
45
+ */
46
+class ReactionEmoji extends Component<Props, State> {
47
+    /**
48
+     * Initializes a new {@code ReactionEmoji} instance.
49
+     *
50
+     * @param {Props} props - The read-only React {@code Component} props with
51
+     * which the new instance is to be initialized.
52
+     */
53
+    constructor(props: Props) {
54
+        super(props);
55
+
56
+        this.state = {
57
+            index: props.index % 21
58
+        };
59
+    }
60
+
61
+    /**
62
+     * Implements React Component's componentDidMount.
63
+     *
64
+     * @inheritdoc
65
+     */
66
+    componentDidMount() {
67
+        setTimeout(() => this.props.removeReaction(this.props.uid), 5000);
68
+    }
69
+
70
+    /**
71
+     * Implements React's {@link Component#render}.
72
+     *
73
+     * @inheritdoc
74
+     */
75
+    render() {
76
+        const { reaction, uid } = this.props;
77
+        const { index } = this.state;
78
+
79
+        return (
80
+            <div
81
+                className = { `reaction-emoji reaction-${index}` }
82
+                id = { uid }>
83
+                { REACTIONS[reaction].emoji }
84
+            </div>
85
+        );
86
+    }
87
+}
88
+
89
+const mapDispatchToProps = {
90
+    removeReaction
91
+};
92
+
93
+export default connect(
94
+    null,
95
+    mapDispatchToProps,
96
+)(ReactionEmoji);

+ 233
- 0
react/features/reactions/components/web/ReactionsMenu.js View File

@@ -0,0 +1,233 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { bindActionCreators } from 'redux';
5
+
6
+import {
7
+    createToolbarEvent,
8
+    sendAnalytics
9
+} from '../../../analytics';
10
+import { translate } from '../../../base/i18n';
11
+import { getLocalParticipant, getParticipantCount, participantUpdated } from '../../../base/participants';
12
+import { connect } from '../../../base/redux';
13
+import { dockToolbox } from '../../../toolbox/actions.web';
14
+import { sendReaction } from '../../actions.any';
15
+import { toggleReactionsMenuVisibility } from '../../actions.web';
16
+import { REACTIONS } from '../../constants';
17
+
18
+import ReactionButton from './ReactionButton';
19
+
20
+type Props = {
21
+
22
+    /**
23
+     * The number of conference participants.
24
+     */
25
+    _participantCount: number,
26
+
27
+    /**
28
+     * Used for translation.
29
+     */
30
+    t: Function,
31
+
32
+    /**
33
+     * Whether or not the local participant's hand is raised.
34
+     */
35
+    _raisedHand: boolean,
36
+
37
+    /**
38
+     * The ID of the local participant.
39
+     */
40
+    _localParticipantID: String,
41
+
42
+    /**
43
+     * The Redux Dispatch function.
44
+     */
45
+    dispatch: Function,
46
+
47
+    /**
48
+     * Docks the toolbox
49
+     */
50
+    _dockToolbox: Function,
51
+
52
+    /**
53
+     * Whether or not it's displayed in the overflow menu.
54
+     */
55
+    overflowMenu: boolean
56
+};
57
+
58
+declare var APP: Object;
59
+
60
+/**
61
+ * Implements the reactions menu.
62
+ *
63
+ * @returns {ReactElement}
64
+ */
65
+class ReactionsMenu extends Component<Props> {
66
+    /**
67
+     * Initializes a new {@code ReactionsMenu} instance.
68
+     *
69
+     * @param {Props} props - The read-only React {@code Component} props with
70
+     * which the new instance is to be initialized.
71
+     */
72
+    constructor(props: Props) {
73
+        super(props);
74
+
75
+        this._onToolbarToggleRaiseHand = this._onToolbarToggleRaiseHand.bind(this);
76
+        this._getReactionButtons = this._getReactionButtons.bind(this);
77
+    }
78
+
79
+    _onToolbarToggleRaiseHand: () => void;
80
+
81
+    _getReactionButtons: () => Array<React$Element<*>>;
82
+
83
+    /**
84
+     * Implements React Component's componentDidMount.
85
+     *
86
+     * @inheritdoc
87
+     */
88
+    componentDidMount() {
89
+        this.props._dockToolbox(true);
90
+    }
91
+
92
+    /**
93
+     * Implements React Component's componentWillUnmount.
94
+     *
95
+     * @inheritdoc
96
+     */
97
+    componentWillUnmount() {
98
+        this.props._dockToolbox(false);
99
+    }
100
+
101
+    /**
102
+     * Creates an analytics toolbar event and dispatches an action for toggling
103
+     * raise hand.
104
+     *
105
+     * @returns {void}
106
+     */
107
+    _onToolbarToggleRaiseHand() {
108
+        sendAnalytics(createToolbarEvent(
109
+            'raise.hand',
110
+            { enable: !this.props._raisedHand }));
111
+        this._doToggleRaiseHand();
112
+        this.props.dispatch(toggleReactionsMenuVisibility());
113
+    }
114
+
115
+    /**
116
+     * Dispatches an action to toggle the local participant's raised hand state.
117
+     *
118
+     * @private
119
+     * @returns {void}
120
+     */
121
+    _doToggleRaiseHand() {
122
+        const { _localParticipantID, _raisedHand } = this.props;
123
+        const newRaisedStatus = !_raisedHand;
124
+
125
+        this.props.dispatch(participantUpdated({
126
+            // XXX Only the local participant is allowed to update without
127
+            // stating the JitsiConference instance (i.e. participant property
128
+            // `conference` for a remote participant) because the local
129
+            // participant is uniquely identified by the very fact that there is
130
+            // only one local participant.
131
+
132
+            id: _localParticipantID,
133
+            local: true,
134
+            raisedHand: newRaisedStatus
135
+        }));
136
+
137
+        APP.API.notifyRaiseHandUpdated(_localParticipantID, newRaisedStatus);
138
+    }
139
+
140
+    /**
141
+     * Returns the emoji reaction buttons.
142
+     *
143
+     * @returns {Array}
144
+     */
145
+    _getReactionButtons() {
146
+        const { t, dispatch } = this.props;
147
+
148
+        return Object.keys(REACTIONS).map(key => {
149
+            /**
150
+             * Sends reaction message.
151
+             *
152
+             * @returns {void}
153
+             */
154
+            function sendMessage() {
155
+                dispatch(sendReaction(key));
156
+            }
157
+
158
+            return (<ReactionButton
159
+                accessibilityLabel = { t(`toolbar.accessibilityLabel.${key}`) }
160
+                icon = { REACTIONS[key].emoji }
161
+                key = { key }
162
+                onClick = { sendMessage }
163
+                toggled = { false }
164
+                tooltip = { t(`toolbar.${key}`) } />);
165
+        });
166
+    }
167
+
168
+    /**
169
+     * Implements React's {@link Component#render}.
170
+     *
171
+     * @inheritdoc
172
+     */
173
+    render() {
174
+        const { _participantCount, _raisedHand, t, overflowMenu } = this.props;
175
+
176
+        return (
177
+            <div className = { `reactions-menu ${overflowMenu ? 'overflow' : ''}` }>
178
+                { _participantCount > 1 && <div className = 'reactions-row'>
179
+                    { this._getReactionButtons() }
180
+                </div> }
181
+                <div className = 'raise-hand-row'>
182
+                    <ReactionButton
183
+                        accessibilityLabel = { t('toolbar.accessibilityLabel.raiseHand') }
184
+                        icon = '✋'
185
+                        key = 'raisehand'
186
+                        label = {
187
+                            `${t(`toolbar.${_raisedHand ? 'lowerYourHand' : 'raiseYourHand'}`)}
188
+                            ${overflowMenu ? '' : ' (R)'}`
189
+                        }
190
+                        onClick = { this._onToolbarToggleRaiseHand }
191
+                        toggled = { true } />
192
+                </div>
193
+            </div>
194
+        );
195
+    }
196
+}
197
+
198
+/**
199
+ * Function that maps parts of Redux state tree into component props.
200
+ *
201
+ * @param {Object} state - Redux state.
202
+ * @returns {Object}
203
+ */
204
+function mapStateToProps(state) {
205
+    const localParticipant = getLocalParticipant(state);
206
+
207
+    return {
208
+        _localParticipantID: localParticipant.id,
209
+        _raisedHand: localParticipant.raisedHand,
210
+        _participantCount: getParticipantCount(state)
211
+    };
212
+}
213
+
214
+/**
215
+ * Function that maps parts of Redux actions into component props.
216
+ *
217
+ * @param {Object} dispatch - Redux dispatch.
218
+ * @returns {Object}
219
+ */
220
+function mapDispatchToProps(dispatch) {
221
+    return {
222
+        dispatch,
223
+        ...bindActionCreators(
224
+        {
225
+            _dockToolbox: dockToolbox
226
+        }, dispatch)
227
+    };
228
+}
229
+
230
+export default translate(connect(
231
+    mapStateToProps,
232
+    mapDispatchToProps,
233
+)(ReactionsMenu));

+ 139
- 0
react/features/reactions/components/web/ReactionsMenuButton.js View File

@@ -0,0 +1,139 @@
1
+// @flow
2
+
3
+import React, { useEffect } from 'react';
4
+
5
+import { translate } from '../../../base/i18n';
6
+import { IconRaisedHand } from '../../../base/icons';
7
+import { getLocalParticipant } from '../../../base/participants';
8
+import { connect } from '../../../base/redux';
9
+import ToolbarButton from '../../../toolbox/components/web/ToolbarButton';
10
+import { sendReaction } from '../../actions.any';
11
+import { toggleReactionsMenuVisibility } from '../../actions.web';
12
+import { REACTIONS, type ReactionEmojiProps } from '../../constants';
13
+import { getReactionsQueue } from '../../functions.any';
14
+import { getReactionsMenuVisibility } from '../../functions.web';
15
+
16
+import ReactionEmoji from './ReactionEmoji';
17
+import ReactionsMenuPopup from './ReactionsMenuPopup';
18
+
19
+type Props = {
20
+
21
+    /**
22
+     * Used for translation.
23
+     */
24
+    t: Function,
25
+
26
+    /**
27
+     * Whether or not the local participant's hand is raised.
28
+     */
29
+    raisedHand: boolean,
30
+
31
+    /**
32
+     * Click handler for the reaction button. Toggles the reactions menu.
33
+     */
34
+    onReactionsClick: Function,
35
+
36
+    /**
37
+     * Whether or not the reactions menu is open.
38
+     */
39
+    isOpen: boolean,
40
+
41
+    /**
42
+     * The array of reactions to be displayed.
43
+     */
44
+    reactionsQueue: Array<ReactionEmojiProps>,
45
+
46
+    /**
47
+     * Redux dispatch function.
48
+     */
49
+    dispatch: Function
50
+};
51
+
52
+
53
+declare var APP: Object;
54
+
55
+/**
56
+ * Button used for the reactions menu.
57
+ *
58
+ * @returns {ReactElement}
59
+ */
60
+function ReactionsMenuButton({
61
+    t,
62
+    raisedHand,
63
+    isOpen,
64
+    reactionsQueue,
65
+    dispatch
66
+}: Props) {
67
+
68
+    useEffect(() => {
69
+        const KEYBOARD_SHORTCUTS = Object.keys(REACTIONS).map(key => {
70
+            return {
71
+                character: REACTIONS[key].shortcutChar,
72
+                exec: () => dispatch(sendReaction(key)),
73
+                helpDescription: t(`toolbar.reaction${key.charAt(0).toUpperCase()}${key.slice(1)}`),
74
+                altKey: true
75
+            };
76
+        });
77
+
78
+        KEYBOARD_SHORTCUTS.forEach(shortcut => {
79
+            APP.keyboardshortcut.registerShortcut(
80
+                shortcut.character,
81
+                null,
82
+                shortcut.exec,
83
+                shortcut.helpDescription,
84
+                shortcut.altKey);
85
+        });
86
+
87
+        return () => {
88
+            Object.keys(REACTIONS).map(key => REACTIONS[key].shortcutChar)
89
+                .forEach(letter =>
90
+                    APP.keyboardshortcut.unregisterShortcut(letter, true));
91
+        };
92
+    }, []);
93
+
94
+    /**
95
+     * Toggles the reactions menu visibility.
96
+     *
97
+     * @returns {void}
98
+     */
99
+    function toggleReactionsMenu() {
100
+        dispatch(toggleReactionsMenuVisibility());
101
+    }
102
+
103
+    return (
104
+        <div className = 'reactions-menu-popup-container'>
105
+            <ReactionsMenuPopup>
106
+                <ToolbarButton
107
+                    accessibilityLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
108
+                    icon = { IconRaisedHand }
109
+                    key = 'reactions'
110
+                    onClick = { toggleReactionsMenu }
111
+                    toggled = { raisedHand }
112
+                    tooltip = { t(`toolbar.${isOpen ? 'closeReactionsMenu' : 'openReactionsMenu'}`) } />
113
+            </ReactionsMenuPopup>
114
+            {reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
115
+                index = { index }
116
+                key = { uid }
117
+                reaction = { reaction }
118
+                uid = { uid } />))}
119
+        </div>
120
+    );
121
+}
122
+
123
+/**
124
+ * Function that maps parts of Redux state tree into component props.
125
+ *
126
+ * @param {Object} state - Redux state.
127
+ * @returns {Object}
128
+ */
129
+function mapStateToProps(state) {
130
+    const localParticipant = getLocalParticipant(state);
131
+
132
+    return {
133
+        isOpen: getReactionsMenuVisibility(state),
134
+        reactionsQueue: getReactionsQueue(state),
135
+        raisedHand: localParticipant?.raisedHand
136
+    };
137
+}
138
+
139
+export default translate(connect(mapStateToProps)(ReactionsMenuButton));

+ 58
- 0
react/features/reactions/components/web/ReactionsMenuPopup.js View File

@@ -0,0 +1,58 @@
1
+// @flow
2
+
3
+import InlineDialog from '@atlaskit/inline-dialog';
4
+import React from 'react';
5
+import { useDispatch, useSelector } from 'react-redux';
6
+
7
+import { toggleReactionsMenuVisibility } from '../../actions.web';
8
+import { getReactionsMenuVisibility } from '../../functions.web';
9
+
10
+import ReactionsMenu from './ReactionsMenu';
11
+
12
+
13
+type Props = {
14
+
15
+    /**
16
+    * Component's children (the reactions menu button).
17
+    */
18
+    children: React$Node
19
+}
20
+
21
+/**
22
+ * Popup with reactions menu.
23
+ *
24
+ * @returns {ReactElement}
25
+ */
26
+function ReactionsMenuPopup({
27
+    children
28
+}: Props) {
29
+    /**
30
+    * Flag controlling the visibility of the popup.
31
+    */
32
+    const isOpen = useSelector(state => getReactionsMenuVisibility(state));
33
+
34
+    const dispatch = useDispatch();
35
+
36
+    /**
37
+     * Toggles reactions menu visibility.
38
+     *
39
+     * @returns {void}
40
+     */
41
+    function onClose() {
42
+        dispatch(toggleReactionsMenuVisibility());
43
+    }
44
+
45
+    return (
46
+        <div className = 'reactions-menu-popup'>
47
+            <InlineDialog
48
+                content = { <ReactionsMenu /> }
49
+                isOpen = { isOpen }
50
+                onClose = { onClose }
51
+                placement = 'top'>
52
+                {children}
53
+            </InlineDialog>
54
+        </div>
55
+    );
56
+}
57
+
58
+export default ReactionsMenuPopup;

+ 7
- 0
react/features/reactions/components/web/index.js View File

@@ -0,0 +1,7 @@
1
+// @flow
2
+
3
+export { default as ReactionButton } from './ReactionButton';
4
+export { default as ReactionEmoji } from './ReactionEmoji';
5
+export { default as ReactionsMenu } from './ReactionsMenu';
6
+export { default as ReactionsMenuButton } from './ReactionsMenuButton';
7
+export { default as ReactionsMenuPopup } from './ReactionsMenuPopup';

+ 47
- 0
react/features/reactions/constants.js View File

@@ -0,0 +1,47 @@
1
+// @flow
2
+
3
+export const REACTIONS = {
4
+    clap: {
5
+        message: ':clap:',
6
+        emoji: '👏',
7
+        shortcutChar: 'C'
8
+    },
9
+    like: {
10
+        message: ':thumbs_up:',
11
+        emoji: '👍',
12
+        shortcutChar: 'T'
13
+    },
14
+    smile: {
15
+        message: ':smile:',
16
+        emoji: '😀',
17
+        shortcutChar: 'S'
18
+    },
19
+    joy: {
20
+        message: ':joy:',
21
+        emoji: '😂',
22
+        shortcutChar: 'L'
23
+    },
24
+    surprised: {
25
+        message: ':face_with_open_mouth:',
26
+        emoji: '😮',
27
+        shortcutChar: 'O'
28
+    },
29
+    party: {
30
+        message: ':party_popper:',
31
+        emoji: '🎉',
32
+        shortcutChar: 'P'
33
+    }
34
+};
35
+
36
+export type ReactionEmojiProps = {
37
+
38
+    /**
39
+     * Reaction to be displayed.
40
+     */
41
+    reaction: string,
42
+
43
+    /**
44
+     * Id of the reaction.
45
+     */
46
+    uid: number
47
+}

+ 11
- 0
react/features/reactions/functions.any.js View File

@@ -0,0 +1,11 @@
1
+// @flow
2
+
3
+/**
4
+ * Returns the queue of reactions.
5
+ *
6
+ * @param {Object} state - The state of the application.
7
+ * @returns {boolean}
8
+ */
9
+export function getReactionsQueue(state: Object) {
10
+    return state['features/reactions'].queue;
11
+}

+ 11
- 0
react/features/reactions/functions.web.js View File

@@ -0,0 +1,11 @@
1
+// @flow
2
+
3
+/**
4
+ * Returns the visibility state of the reactions menu.
5
+ *
6
+ * @param {Object} state - The state of the application.
7
+ * @returns {boolean}
8
+ */
9
+export function getReactionsMenuVisibility(state: Object) {
10
+    return state['features/reactions'].visible;
11
+}

+ 84
- 0
react/features/reactions/middleware.js View File

@@ -0,0 +1,84 @@
1
+// @flow
2
+
3
+import { ENDPOINT_REACTION_NAME } from '../../../modules/API/constants';
4
+import { MiddlewareRegistry } from '../base/redux';
5
+
6
+import {
7
+    SET_REACTIONS_MESSAGE,
8
+    CLEAR_REACTIONS_MESSAGE,
9
+    SEND_REACTION,
10
+    PUSH_REACTION
11
+} from './actionTypes';
12
+import {
13
+    addReactionsMessage,
14
+    addReactionsMessageToChat,
15
+    flushReactionsToChat,
16
+    pushReaction,
17
+    setReactionQueue
18
+} from './actions.any';
19
+import { REACTIONS } from './constants';
20
+
21
+
22
+declare var APP: Object;
23
+
24
+/**
25
+ * Middleware which intercepts Reactions actions to handle changes to the
26
+ * visibility timeout of the Reactions.
27
+ *
28
+ * @param {Store} store - The redux store.
29
+ * @returns {Function}
30
+ */
31
+MiddlewareRegistry.register(store => next => action => {
32
+    const { dispatch, getState } = store;
33
+
34
+    switch (action.type) {
35
+    case SET_REACTIONS_MESSAGE: {
36
+        const { timeoutID, message } = getState()['features/reactions'];
37
+        const { reaction } = action;
38
+
39
+        clearTimeout(timeoutID);
40
+        action.message = `${message}${reaction}`;
41
+        action.timeoutID = setTimeout(() => {
42
+            dispatch(flushReactionsToChat());
43
+        }, 500);
44
+
45
+        break;
46
+    }
47
+
48
+    case CLEAR_REACTIONS_MESSAGE: {
49
+        const { message } = getState()['features/reactions'];
50
+
51
+        dispatch(addReactionsMessageToChat(message));
52
+
53
+        break;
54
+    }
55
+
56
+    case SEND_REACTION: {
57
+        const state = store.getState();
58
+        const { conference } = state['features/base/conference'];
59
+
60
+        if (conference) {
61
+            conference.sendEndpointMessage('', {
62
+                name: ENDPOINT_REACTION_NAME,
63
+                reaction: action.reaction,
64
+                timestamp: Date.now()
65
+            });
66
+            dispatch(addReactionsMessage(REACTIONS[action.reaction].message));
67
+            dispatch(pushReaction(action.reaction));
68
+        }
69
+        break;
70
+    }
71
+
72
+    case PUSH_REACTION: {
73
+        const queue = store.getState()['features/reactions'].queue;
74
+        const reaction = action.reaction;
75
+
76
+        dispatch(setReactionQueue([ ...queue, {
77
+            reaction,
78
+            uid: window.Date.now()
79
+        } ]));
80
+    }
81
+    }
82
+
83
+    return next(action);
84
+});

+ 90
- 0
react/features/reactions/reducer.js View File

@@ -0,0 +1,90 @@
1
+// @flow
2
+
3
+import { ReducerRegistry } from '../base/redux';
4
+
5
+import {
6
+    TOGGLE_REACTIONS_VISIBLE,
7
+    SET_REACTIONS_MESSAGE,
8
+    CLEAR_REACTIONS_MESSAGE,
9
+    SET_REACTION_QUEUE
10
+} from './actionTypes';
11
+
12
+/**
13
+ * Returns initial state for reactions' part of Redux store.
14
+ *
15
+ * @private
16
+ * @returns {{
17
+ *     visible: boolean,
18
+ *     message: string,
19
+ *     timeoutID: number,
20
+ *     queue: Array
21
+ * }}
22
+ */
23
+function _getInitialState() {
24
+    return {
25
+        /**
26
+         * The indicator that determines whether the reactions menu is visible.
27
+         *
28
+         * @type {boolean}
29
+         */
30
+        visible: false,
31
+
32
+        /**
33
+         * A string that contains the message to be added to the chat.
34
+         *
35
+         * @type {string}
36
+         */
37
+        message: '',
38
+
39
+        /**
40
+         * A number, non-zero value which identifies the timer created by a call
41
+         * to setTimeout().
42
+         *
43
+         * @type {number|null}
44
+         */
45
+        timeoutID: null,
46
+
47
+        /**
48
+         * The array of reactions to animate
49
+         *
50
+         * @type {Array}
51
+         */
52
+        queue: []
53
+    };
54
+}
55
+
56
+ReducerRegistry.register(
57
+    'features/reactions',
58
+    (state: Object = _getInitialState(), action: Object) => {
59
+        switch (action.type) {
60
+
61
+        case TOGGLE_REACTIONS_VISIBLE:
62
+            return {
63
+                ...state,
64
+                visible: !state.visible
65
+            };
66
+
67
+        case SET_REACTIONS_MESSAGE:
68
+            return {
69
+                ...state,
70
+                message: action.message,
71
+                timeoutID: action.timeoutID
72
+            };
73
+
74
+        case CLEAR_REACTIONS_MESSAGE:
75
+            return {
76
+                ...state,
77
+                message: '',
78
+                timeoutID: null
79
+            };
80
+
81
+        case SET_REACTION_QUEUE: {
82
+            return {
83
+                ...state,
84
+                queue: action.value
85
+            };
86
+        }
87
+        }
88
+
89
+        return state;
90
+    });

+ 0
- 20
react/features/toolbox/components/native/MoreOptionsButton.js View File

@@ -1,20 +0,0 @@
1
-// @flow
2
-
3
-import { translate } from '../../../base/i18n';
4
-import { IconMenu } from '../../../base/icons';
5
-import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
6
-
7
-
8
-type Props = AbstractButtonProps;
9
-
10
-/**
11
- * An implementation of a button to show more menu options.
12
- */
13
-class MoreOptionsButton extends AbstractButton<Props, any> {
14
-    accessibilityLabel = 'toolbar.accessibilityLabel.moreOptions';
15
-    icon = IconMenu;
16
-    label = 'toolbar.moreOptions';
17
-}
18
-
19
-
20
-export default translate(MoreOptionsButton);

+ 37
- 97
react/features/toolbox/components/native/OverflowMenu.js View File

@@ -1,17 +1,15 @@
1 1
 // @flow
2 2
 
3 3
 import React, { PureComponent } from 'react';
4
-import { TouchableOpacity, View } from 'react-native';
5
-import Collapsible from 'react-native-collapsible';
6 4
 
7 5
 import { ColorSchemeRegistry } from '../../../base/color-scheme';
8 6
 import { BottomSheet, hideDialog, isDialogOpen } from '../../../base/dialog';
9
-import { IconDragHandle } from '../../../base/icons';
10 7
 import { connect } from '../../../base/redux';
11 8
 import { StyleType } from '../../../base/styles';
12 9
 import { SharedDocumentButton } from '../../../etherpad';
13 10
 import { InviteButton } from '../../../invite';
14 11
 import { AudioRouteButton } from '../../../mobile/audio-mode';
12
+import { ReactionMenu } from '../../../reactions/components';
15 13
 import { LiveStreamButton, RecordButton } from '../../../recording';
16 14
 import SecurityDialogButton from '../../../security/components/security-dialog/SecurityDialogButton';
17 15
 import { SharedVideoButton } from '../../../shared-video/components';
@@ -23,11 +21,8 @@ import MuteEveryoneButton from '../MuteEveryoneButton';
23 21
 import MuteEveryonesVideoButton from '../MuteEveryonesVideoButton';
24 22
 
25 23
 import AudioOnlyButton from './AudioOnlyButton';
26
-import MoreOptionsButton from './MoreOptionsButton';
27
-import RaiseHandButton from './RaiseHandButton';
28 24
 import ScreenSharingButton from './ScreenSharingButton.js';
29 25
 import ToggleCameraButton from './ToggleCameraButton';
30
-import styles from './styles';
31 26
 
32 27
 /**
33 28
  * The type of the React {@code Component} props of {@link OverflowMenu}.
@@ -65,12 +60,7 @@ type State = {
65 60
     /**
66 61
      * True if the bottom scheet is scrolled to the top.
67 62
      */
68
-    scrolledToTop: boolean,
69
-
70
-    /**
71
-     * True if the 'more' button set needas to be rendered.
72
-     */
73
-    showMore: boolean
63
+    scrolledToTop: boolean
74 64
 }
75 65
 
76 66
 /**
@@ -96,15 +86,12 @@ class OverflowMenu extends PureComponent<Props, State> {
96 86
         super(props);
97 87
 
98 88
         this.state = {
99
-            scrolledToTop: true,
100
-            showMore: false
89
+            scrolledToTop: true
101 90
         };
102 91
 
103 92
         // Bind event handlers so they are only bound once per instance.
104 93
         this._onCancel = this._onCancel.bind(this);
105
-        this._onSwipe = this._onSwipe.bind(this);
106
-        this._onToggleMenu = this._onToggleMenu.bind(this);
107
-        this._renderMenuExpandToggle = this._renderMenuExpandToggle.bind(this);
94
+        this._renderReactionMenu = this._renderReactionMenu.bind(this);
108 95
     }
109 96
 
110 97
     /**
@@ -115,7 +102,6 @@ class OverflowMenu extends PureComponent<Props, State> {
115 102
      */
116 103
     render() {
117 104
         const { _bottomSheetStyles, _width } = this.props;
118
-        const { showMore } = this.state;
119 105
         const toolbarButtons = getMovableButtons(_width);
120 106
 
121 107
         const buttonProps = {
@@ -124,63 +110,45 @@ class OverflowMenu extends PureComponent<Props, State> {
124 110
             styles: _bottomSheetStyles.buttons
125 111
         };
126 112
 
127
-        const moreOptionsButtonProps = {
128
-            ...buttonProps,
129
-            afterClick: this._onToggleMenu,
130
-            visible: !showMore
113
+        const topButtonProps = {
114
+            afterClick: this._onCancel,
115
+            showLabel: true,
116
+            styles: {
117
+                ..._bottomSheetStyles.buttons,
118
+                style: {
119
+                    ..._bottomSheetStyles.buttons.style,
120
+                    borderTopLeftRadius: 16,
121
+                    borderTopRightRadius: 16,
122
+                    paddingTop: 16
123
+                }
124
+            }
131 125
         };
132 126
 
133 127
         return (
134 128
             <BottomSheet
135 129
                 onCancel = { this._onCancel }
136
-                onSwipe = { this._onSwipe }
137
-                renderHeader = { this._renderMenuExpandToggle }>
138
-                <AudioRouteButton { ...buttonProps } />
130
+                renderFooter = { toolbarButtons.has('raisehand')
131
+                    ? null
132
+                    : this._renderReactionMenu }>
133
+                <AudioRouteButton { ...topButtonProps } />
139 134
                 {!toolbarButtons.has('invite') && <InviteButton { ...buttonProps } />}
140 135
                 <AudioOnlyButton { ...buttonProps } />
141
-                {!toolbarButtons.has('raisehand') && <RaiseHandButton { ...buttonProps } />}
142 136
                 <SecurityDialogButton { ...buttonProps } />
143 137
                 <ScreenSharingButton { ...buttonProps } />
144
-                <MoreOptionsButton { ...moreOptionsButtonProps } />
145
-                <Collapsible collapsed = { !showMore }>
146
-                    {!toolbarButtons.has('togglecamera') && <ToggleCameraButton { ...buttonProps } />}
147
-                    {!toolbarButtons.has('tileview') && <TileViewButton { ...buttonProps } />}
148
-                    <RecordButton { ...buttonProps } />
149
-                    <LiveStreamButton { ...buttonProps } />
150
-                    <SharedVideoButton { ...buttonProps } />
151
-                    <ClosedCaptionButton { ...buttonProps } />
152
-                    <SharedDocumentButton { ...buttonProps } />
153
-                    <MuteEveryoneButton { ...buttonProps } />
154
-                    <MuteEveryonesVideoButton { ...buttonProps } />
155
-                    <HelpButton { ...buttonProps } />
156
-                </Collapsible>
138
+                {!toolbarButtons.has('togglecamera') && <ToggleCameraButton { ...buttonProps } />}
139
+                {!toolbarButtons.has('tileview') && <TileViewButton { ...buttonProps } />}
140
+                <RecordButton { ...buttonProps } />
141
+                <LiveStreamButton { ...buttonProps } />
142
+                <SharedVideoButton { ...buttonProps } />
143
+                <ClosedCaptionButton { ...buttonProps } />
144
+                <SharedDocumentButton { ...buttonProps } />
145
+                <MuteEveryoneButton { ...buttonProps } />
146
+                <MuteEveryonesVideoButton { ...buttonProps } />
147
+                <HelpButton { ...buttonProps } />
157 148
             </BottomSheet>
158 149
         );
159 150
     }
160 151
 
161
-    _renderMenuExpandToggle: () => React$Element<any>;
162
-
163
-    /**
164
-     * Function to render the menu toggle in the bottom sheet header area.
165
-     *
166
-     * @returns {React$Element}
167
-     */
168
-    _renderMenuExpandToggle() {
169
-        return (
170
-            <View
171
-                style = { [
172
-                    this.props._bottomSheetStyles.sheet,
173
-                    styles.expandMenuContainer
174
-                ] }>
175
-                <TouchableOpacity onPress = { this._onToggleMenu }>
176
-                    { /* $FlowFixMe */ }
177
-                    <IconDragHandle
178
-                        fill = { this.props._bottomSheetStyles.buttons.iconStyle.color } />
179
-                </TouchableOpacity>
180
-            </View>
181
-        );
182
-    }
183
-
184 152
     _onCancel: () => boolean;
185 153
 
186 154
     /**
@@ -199,45 +167,17 @@ class OverflowMenu extends PureComponent<Props, State> {
199 167
         return false;
200 168
     }
201 169
 
202
-    _onSwipe: string => void;
170
+    _renderReactionMenu: () => React$Element<any>;
203 171
 
204 172
     /**
205
-     * Callback to be invoked when swipe gesture is detected on the menu. Returns true
206
-     * if the swipe gesture is handled by the menu, false otherwise.
173
+     * Functoin to render the reaction menu as the footer of the bottom sheet.
207 174
      *
208
-     * @param {string} direction - Direction of 'up' or 'down'.
209
-     * @returns {boolean}
210
-     */
211
-    _onSwipe(direction) {
212
-        const { showMore } = this.state;
213
-
214
-        switch (direction) {
215
-        case 'up':
216
-            !showMore && this.setState({
217
-                showMore: true
218
-            });
219
-
220
-            return !showMore;
221
-        case 'down':
222
-            showMore && this.setState({
223
-                showMore: false
224
-            });
225
-
226
-            return showMore;
227
-        }
228
-    }
229
-
230
-    _onToggleMenu: () => void;
231
-
232
-    /**
233
-     * Callback to be invoked when the expand menu button is pressed.
234
-     *
235
-     * @returns {void}
175
+     * @returns {React$Element}
236 176
      */
237
-    _onToggleMenu() {
238
-        this.setState({
239
-            showMore: !this.state.showMore
240
-        });
177
+    _renderReactionMenu() {
178
+        return (<ReactionMenu
179
+            onCancel = { this._onCancel }
180
+            overflowMenu = { true } />);
241 181
     }
242 182
 }
243 183
 

+ 4
- 4
react/features/toolbox/components/native/Toolbox.js View File

@@ -8,6 +8,7 @@ import { connect } from '../../../base/redux';
8 8
 import { StyleType } from '../../../base/styles';
9 9
 import { ChatButton } from '../../../chat';
10 10
 import { InviteButton } from '../../../invite';
11
+import { ReactionsMenuButton } from '../../../reactions/components';
11 12
 import { TileViewButton } from '../../../video-layout';
12 13
 import { isToolboxVisible, getMovableButtons } from '../../functions.native';
13 14
 import AudioMuteButton from '../AudioMuteButton';
@@ -15,7 +16,6 @@ import HangupButton from '../HangupButton';
15 16
 import VideoMuteButton from '../VideoMuteButton';
16 17
 
17 18
 import OverflowMenuButton from './OverflowMenuButton';
18
-import RaiseHandButton from './RaiseHandButton';
19 19
 import ToggleCameraButton from './ToggleCameraButton';
20 20
 import styles from './styles';
21 21
 
@@ -87,9 +87,9 @@ function Toolbox(props: Props) {
87 87
                           toggledStyles = { backgroundToggledStyle } />}
88 88
 
89 89
                 { additionalButtons.has('raisehand')
90
-                      && <RaiseHandButton
91
-                          styles = { buttonStylesBorderless }
92
-                          toggledStyles = { backgroundToggledStyle } />}
90
+                    && <ReactionsMenuButton
91
+                        styles = { buttonStylesBorderless }
92
+                        toggledStyles = { backgroundToggledStyle } />}
93 93
                 {additionalButtons.has('tileview') && <TileViewButton styles = { buttonStylesBorderless } />}
94 94
                 {additionalButtons.has('invite') && <InviteButton styles = { buttonStylesBorderless } />}
95 95
                 {additionalButtons.has('togglecamera')

+ 88
- 7
react/features/toolbox/components/native/styles.js View File

@@ -40,18 +40,38 @@ const whiteToolbarButtonIcon = {
40 40
     color: ColorPalette.white
41 41
 };
42 42
 
43
+/**
44
+ * The style of reaction buttons.
45
+ */
46
+const reactionButton = {
47
+    ...toolbarButton,
48
+    backgroundColor: 'transparent',
49
+    alignItems: 'center',
50
+    marginTop: 0,
51
+    marginHorizontal: 0
52
+};
53
+
54
+/**
55
+ * The style of the emoji on the reaction buttons.
56
+ */
57
+const reactionEmoji = {
58
+    fontSize: 20,
59
+    color: ColorPalette.white
60
+};
61
+
62
+const reactionMenu = {
63
+    flexDirection: 'column',
64
+    justifyContent: 'center',
65
+    alignItems: 'center',
66
+    backgroundColor: ColorPalette.black,
67
+    padding: 16
68
+};
69
+
43 70
 /**
44 71
  * The Toolbox and toolbar related styles.
45 72
  */
46 73
 const styles = {
47 74
 
48
-    expandMenuContainer: {
49
-        alignItems: 'center',
50
-        borderTopLeftRadius: 16,
51
-        borderTopRightRadius: 16,
52
-        flexDirection: 'column'
53
-    },
54
-
55 75
     sheetGestureRecognizer: {
56 76
         alignItems: 'stretch',
57 77
         flexDirection: 'column'
@@ -120,6 +140,67 @@ ColorSchemeRegistry.register('Toolbox', {
120 140
         underlayColor: ColorPalette.buttonUnderlay
121 141
     },
122 142
 
143
+    reactionDialog: {
144
+        position: 'absolute',
145
+        width: '100%',
146
+        height: '100%',
147
+        backgroundColor: 'transparent'
148
+    },
149
+
150
+    overflowReactionMenu: reactionMenu,
151
+
152
+    reactionMenu: {
153
+        ...reactionMenu,
154
+        borderRadius: 3,
155
+        width: 360
156
+    },
157
+
158
+    reactionRow: {
159
+        flexDirection: 'row',
160
+        justifyContent: 'space-between',
161
+        alignItems: 'center',
162
+        width: '100%',
163
+        marginBottom: 16
164
+    },
165
+
166
+    reactionButton: {
167
+        style: reactionButton,
168
+        underlayColor: ColorPalette.toggled,
169
+        emoji: reactionEmoji
170
+    },
171
+
172
+    raiseHandButton: {
173
+        style: {
174
+            ...reactionButton,
175
+            backgroundColor: ColorPalette.toggled,
176
+            width: '100%',
177
+            borderRadius: 6
178
+        },
179
+        underlayColor: ColorPalette.toggled,
180
+        emoji: reactionEmoji,
181
+        text: {
182
+            color: ColorPalette.white,
183
+            fontWeight: '600',
184
+            marginLeft: 8,
185
+            lineHeight: 24
186
+        },
187
+        container: {
188
+            flexDirection: 'row',
189
+            alignItems: 'center',
190
+            justifyContent: 'center'
191
+        }
192
+    },
193
+
194
+    emojiAnimation: {
195
+        color: ColorPalette.white,
196
+        position: 'absolute',
197
+        zIndex: 1001,
198
+        elevation: 2,
199
+        fontSize: 20,
200
+        left: '50%',
201
+        top: '100%'
202
+    },
203
+
123 204
     /**
124 205
      * Styles for toggled buttons in the toolbar.
125 206
      */

+ 7
- 60
react/features/toolbox/components/web/Drawer.js View File

@@ -1,36 +1,24 @@
1 1
 // @flow
2 2
 
3
-import React, { useEffect, useRef, useState } from 'react';
3
+import React, { useEffect, useRef } from 'react';
4 4
 
5
-import { translate } from '../../../base/i18n';
6
-import { Icon, IconArrowUpWide, IconArrowDownWide } from '../../../base/icons';
7 5
 
8 6
 type Props = {
9 7
 
10
-    /**
11
-     * Whether the drawer should have a button that expands its size or not.
12
-     */
13
-    canExpand: ?boolean,
14
-
15 8
     /**
16 9
      * The component(s) to be displayed within the drawer menu.
17 10
      */
18 11
     children: React$Node,
19 12
 
20 13
     /**
21
-     Whether the drawer should be shown or not.
14
+     * Whether the drawer should be shown or not.
22 15
      */
23 16
     isOpen: boolean,
24 17
 
25 18
     /**
26
-     Function that hides the drawer.
27
-     */
28
-    onClose: Function,
29
-
30
-    /**
31
-     * Invoked to obtain translated strings.
19
+     * Function that hides the drawer.
32 20
      */
33
-    t: Function
21
+    onClose: Function
34 22
 };
35 23
 
36 24
 /**
@@ -39,12 +27,9 @@ type Props = {
39 27
  * @returns {ReactElement}
40 28
  */
41 29
 function Drawer({
42
-    canExpand,
43 30
     children,
44 31
     isOpen,
45
-    onClose,
46
-    t }: Props) {
47
-    const [ expanded, setExpanded ] = useState(false);
32
+    onClose }: Props) {
48 33
     const drawerRef: Object = useRef(null);
49 34
 
50 35
     /**
@@ -69,53 +54,15 @@ function Drawer({
69 54
         };
70 55
     }, [ drawerRef ]);
71 56
 
72
-    /**
73
-     * Toggles the menu state between expanded/collapsed.
74
-     *
75
-     * @returns {void}
76
-     */
77
-    function toggleExpanded() {
78
-        setExpanded(!expanded);
79
-    }
80
-
81
-    /**
82
-     * KeyPress handler for accessibility.
83
-     *
84
-     * @param {React.KeyboardEventHandler<HTMLDivElement>} e - The key event to handle.
85
-     *
86
-     * @returns {void}
87
-     */
88
-    function onKeyPress(e) {
89
-        if (e.key === ' ' || e.key === 'Enter') {
90
-            e.preventDefault();
91
-            toggleExpanded();
92
-        }
93
-    }
94
-
95 57
     return (
96 58
         isOpen ? (
97 59
             <div
98
-                className = { `drawer-menu${expanded ? ' expanded' : ''}` }
60
+                className = 'drawer-menu'
99 61
                 ref = { drawerRef }>
100
-                {canExpand && (
101
-                    <div
102
-                        aria-expanded = { expanded }
103
-                        aria-label = { expanded ? t('toolbar.accessibilityLabel.collapse')
104
-                            : t('toolbar.accessibilityLabel.expand') }
105
-                        className = 'drawer-toggle'
106
-                        onClick = { toggleExpanded }
107
-                        onKeyPress = { onKeyPress }
108
-                        role = 'button'
109
-                        tabIndex = { 0 }>
110
-                        <Icon
111
-                            size = { 24 }
112
-                            src = { expanded ? IconArrowDownWide : IconArrowUpWide } />
113
-                    </div>
114
-                )}
115 62
                 {children}
116 63
             </div>
117 64
         ) : null
118 65
     );
119 66
 }
120 67
 
121
-export default translate(Drawer);
68
+export default Drawer;

+ 25
- 4
react/features/toolbox/components/web/OverflowMenuButton.js View File

@@ -7,6 +7,9 @@ import { createToolbarEvent, sendAnalytics } from '../../../analytics';
7 7
 import { translate } from '../../../base/i18n';
8 8
 import { IconHorizontalPoints } from '../../../base/icons';
9 9
 import { connect } from '../../../base/redux';
10
+import { ReactionEmoji, ReactionsMenu } from '../../../reactions/components';
11
+import { type ReactionEmojiProps } from '../../../reactions/constants';
12
+import { getReactionsQueue } from '../../../reactions/functions.any';
10 13
 
11 14
 import Drawer from './Drawer';
12 15
 import DrawerPortal from './DrawerPortal';
@@ -45,7 +48,17 @@ type Props = {
45 48
     /**
46 49
      * Invoked to obtain translated strings.
47 50
      */
48
-    t: Function
51
+    t: Function,
52
+
53
+    /**
54
+     * The array of reactions to be displayed.
55
+     */
56
+    reactionsQueue: Array<ReactionEmojiProps>,
57
+
58
+    /**
59
+     * Whether or not to display the reactions in the mobile menu.
60
+     */
61
+    showMobileReactions: boolean
49 62
 };
50 63
 
51 64
 /**
@@ -93,7 +106,7 @@ class OverflowMenuButton extends Component<Props> {
93 106
      * @returns {ReactElement}
94 107
      */
95 108
     render() {
96
-        const { children, isOpen, overflowDrawer } = this.props;
109
+        const { children, isOpen, overflowDrawer, reactionsQueue, showMobileReactions } = this.props;
97 110
 
98 111
         return (
99 112
             <div className = 'toolbox-button-wth-dialog'>
@@ -103,11 +116,18 @@ class OverflowMenuButton extends Component<Props> {
103 116
                             {this._renderToolbarButton()}
104 117
                             <DrawerPortal>
105 118
                                 <Drawer
106
-                                    canExpand = { true }
107 119
                                     isOpen = { isOpen }
108 120
                                     onClose = { this._onCloseDialog }>
109 121
                                     {children}
122
+                                    {showMobileReactions && <ReactionsMenu overflowMenu = { true } />}
110 123
                                 </Drawer>
124
+                                {showMobileReactions && <div className = 'reactions-animations-container'>
125
+                                    {reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
126
+                                        index = { index }
127
+                                        key = { uid }
128
+                                        reaction = { reaction }
129
+                                        uid = { uid } />))}
130
+                                </div>}
111 131
                             </DrawerPortal>
112 132
                         </>
113 133
                     ) : (
@@ -188,7 +208,8 @@ function mapStateToProps(state) {
188 208
     const { overflowDrawer } = state['features/toolbox'];
189 209
 
190 210
     return {
191
-        overflowDrawer
211
+        overflowDrawer,
212
+        reactionsQueue: getReactionsQueue(state)
192 213
     };
193 214
 }
194 215
 

+ 0
- 83
react/features/toolbox/components/web/RaiseHandButton.js View File

@@ -1,83 +0,0 @@
1
-// @flow
2
-
3
-import { translate } from '../../../base/i18n';
4
-import { IconRaisedHand } from '../../../base/icons';
5
-import { getLocalParticipant } from '../../../base/participants';
6
-import { connect } from '../../../base/redux';
7
-import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
8
-
9
-type Props = AbstractButtonProps & {
10
-
11
-    /**
12
-     * Whether or not the local participant's hand is raised.
13
-     */
14
-     _raisedHand: boolean,
15
-
16
-     /**
17
-      * External handler for click action.
18
-      */
19
-     handleClick: Function
20
-};
21
-
22
-/**
23
- * Implementation of a button for toggling raise hand functionality.
24
- */
25
-class RaiseHandButton extends AbstractButton<Props, *> {
26
-    accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
27
-    icon = IconRaisedHand
28
-    label = 'toolbar.raiseYourHand';
29
-    toggledLabel = 'toolbar.lowerYourHand'
30
-
31
-    /**
32
-     * Retrieves tooltip dynamically.
33
-     */
34
-    get tooltip() {
35
-        return this.props._raisedHand ? 'toolbar.lowerYourHand' : 'toolbar.raiseYourHand';
36
-    }
37
-
38
-    /**
39
-     * Required by linter due to AbstractButton overwritten prop being writable.
40
-     *
41
-     * @param {string} value - The value.
42
-     */
43
-    set tooltip(value) {
44
-        return value;
45
-    }
46
-
47
-    /**
48
-     * Handles clicking / pressing the button, and opens the appropriate dialog.
49
-     *
50
-     * @protected
51
-     * @returns {void}
52
-     */
53
-    _handleClick() {
54
-        this.props.handleClick();
55
-    }
56
-
57
-    /**
58
-     * Indicates whether this button is in toggled state or not.
59
-     *
60
-     * @override
61
-     * @protected
62
-     * @returns {boolean}
63
-     */
64
-    _isToggled() {
65
-        return this.props._raisedHand;
66
-    }
67
-}
68
-
69
-/**
70
- * Function that maps parts of Redux state tree into component props.
71
- *
72
- * @param {Object} state - Redux state.
73
- * @returns {Object}
74
- */
75
-const mapStateToProps = state => {
76
-    const localParticipant = getLocalParticipant(state);
77
-
78
-    return {
79
-        _raisedHand: localParticipant.raisedHand
80
-    };
81
-};
82
-
83
-export default translate(connect(mapStateToProps)(RaiseHandButton));

+ 9
- 25
react/features/toolbox/components/web/Toolbox.js View File

@@ -36,6 +36,7 @@ import {
36 36
 } from '../../../participants-pane/actions';
37 37
 import ParticipantsPaneButton from '../../../participants-pane/components/ParticipantsPaneButton';
38 38
 import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
39
+import { ReactionsMenuButton } from '../../../reactions/components';
39 40
 import {
40 41
     LiveStreamButton,
41 42
     RecordButton
@@ -81,7 +82,6 @@ import AudioSettingsButton from './AudioSettingsButton';
81 82
 import FullscreenButton from './FullscreenButton';
82 83
 import OverflowMenuButton from './OverflowMenuButton';
83 84
 import ProfileButton from './ProfileButton';
84
-import RaiseHandButton from './RaiseHandButton';
85 85
 import Separator from './Separator';
86 86
 import ShareDesktopButton from './ShareDesktopButton';
87 87
 import VideoSettingsButton from './VideoSettingsButton';
@@ -256,7 +256,6 @@ class Toolbox extends Component<Props> {
256 256
         this._onToolbarOpenVideoQuality = this._onToolbarOpenVideoQuality.bind(this);
257 257
         this._onToolbarToggleChat = this._onToolbarToggleChat.bind(this);
258 258
         this._onToolbarToggleFullScreen = this._onToolbarToggleFullScreen.bind(this);
259
-        this._onToolbarToggleRaiseHand = this._onToolbarToggleRaiseHand.bind(this);
260 259
         this._onToolbarToggleScreenshare = this._onToolbarToggleScreenshare.bind(this);
261 260
         this._onShortcutToggleTileView = this._onShortcutToggleTileView.bind(this);
262 261
         this._onEscKey = this._onEscKey.bind(this);
@@ -547,8 +546,7 @@ class Toolbox extends Component<Props> {
547 546
 
548 547
         const raisehand = {
549 548
             key: 'raisehand',
550
-            Content: RaiseHandButton,
551
-            handleClick: this._onToolbarToggleRaiseHand,
549
+            Content: ReactionsMenuButton,
552 550
             group: 2
553 551
         };
554 552
 
@@ -1024,23 +1022,6 @@ class Toolbox extends Component<Props> {
1024 1022
         this._doToggleFullScreen();
1025 1023
     }
1026 1024
 
1027
-    _onToolbarToggleRaiseHand: () => void;
1028
-
1029
-    /**
1030
-     * Creates an analytics toolbar event and dispatches an action for toggling
1031
-     * raise hand.
1032
-     *
1033
-     * @private
1034
-     * @returns {void}
1035
-     */
1036
-    _onToolbarToggleRaiseHand() {
1037
-        sendAnalytics(createToolbarEvent(
1038
-            'raise.hand',
1039
-            { enable: !this.props._raisedHand }));
1040
-
1041
-        this._doToggleRaiseHand();
1042
-    }
1043
-
1044 1025
     _onToolbarToggleScreenshare: () => void;
1045 1026
 
1046 1027
     /**
@@ -1144,7 +1125,10 @@ class Toolbox extends Component<Props> {
1144 1125
                                 ariaControls = 'overflow-menu'
1145 1126
                                 isOpen = { _overflowMenuVisible }
1146 1127
                                 key = 'overflow-menu'
1147
-                                onVisibilityChange = { this._onSetOverflowVisible }>
1128
+                                onVisibilityChange = { this._onSetOverflowVisible }
1129
+                                showMobileReactions = {
1130
+                                    overflowMenuButtons.find(({ key }) => key === 'raisehand')
1131
+                                }>
1148 1132
                                 <ul
1149 1133
                                     aria-label = { t(toolbarAccLabel) }
1150 1134
                                     className = 'overflow-menu'
@@ -1154,15 +1138,15 @@ class Toolbox extends Component<Props> {
1154 1138
                                     {overflowMenuButtons.map(({ group, key, Content, ...rest }, index, arr) => {
1155 1139
                                         const showSeparator = index > 0 && arr[index - 1].group !== group;
1156 1140
 
1157
-                                        return (
1158
-                                            <>
1141
+                                        return key !== 'raisehand'
1142
+                                            && <>
1159 1143
                                                 {showSeparator && <Separator key = { `hr${group}` } />}
1160 1144
                                                 <Content
1161 1145
                                                     { ...rest }
1162 1146
                                                     key = { key }
1163 1147
                                                     showLabel = { true } />
1164 1148
                                             </>
1165
-                                        );
1149
+                                        ;
1166 1150
                                     })}
1167 1151
                                 </ul>
1168 1152
                             </OverflowMenuButton>

+ 2
- 0
react/features/toolbox/middleware.js View File

@@ -8,6 +8,7 @@ import {
8 8
     SET_FULL_SCREEN
9 9
 } from './actionTypes';
10 10
 
11
+
11 12
 declare var APP: Object;
12 13
 
13 14
 /**
@@ -18,6 +19,7 @@ declare var APP: Object;
18 19
  * @returns {Function}
19 20
  */
20 21
 MiddlewareRegistry.register(store => next => action => {
22
+
21 23
     switch (action.type) {
22 24
     case CLEAR_TOOLBOX_TIMEOUT: {
23 25
         const { timeoutID } = store.getState()['features/toolbox'];

Loading…
Cancel
Save