Explorar el Código

feat(gif) Added GIF support (GIPHY integration) (#11021)

Show GIF menu in reactions menu
Search GIFs using the GIPHY API
Show GIFs as images in chat
Show GIFs on the thumbnail of the participant that sent it
Move GIF focus using up/ down arrows and send with Enter
Added analytics
master
Robert Pintilii hace 3 años
padre
commit
190041fc5a
No account linked to committer's email address

+ 15
- 0
config.js Ver fichero

@@ -1300,6 +1300,21 @@ var config = {
1300 1300
     // Specifies whether the chat emoticons are disabled or not
1301 1301
     // disableChatSmileys: false,
1302 1302
 
1303
+    // Settings for the GIPHY integration.
1304
+    // giphy: {
1305
+    //     // Whether the feature is enabled or not.
1306
+    //     enabled: false,
1307
+    //     // SDK API Key from Giphy.
1308
+    //     sdkKey: '',
1309
+    //     // Display mode can be one of:
1310
+    //     // - tile: show the GIF on the tile of the participant that sent it.
1311
+    //     // - chat: show the GIF as a message in chat
1312
+    //     // - all: all of the above. This is the default option
1313
+    //     displayMode: 'all',
1314
+    //     // How long the GIF should be displayed on the tile (in miliseconds).
1315
+    //     tileTime: 5000
1316
+    // },
1317
+
1303 1318
     // Allow all above example options to include a trailing comma and
1304 1319
     // prevent fear when commenting out the last value.
1305 1320
     makeJsonParserHappy: 'even if last key had a trailing comma'

+ 18
- 0
css/_reactions-menu.scss Ver fichero

@@ -7,7 +7,20 @@
7 7
 	border-radius: 3px;
8 8
 	padding: 16px;
9 9
 
10
+	&.with-gif {
11
+		width: 328px;
12
+
13
+		.reactions-row .toolbox-button:last-of-type {
14
+			top: 3px;
15
+
16
+			& .toolbox-icon.toggled {
17
+				background-color: #000000;
18
+			}
19
+		}
20
+	}
21
+
10 22
 	&.overflow {
23
+		width: 100%;
11 24
 
12 25
 		.toolbox-icon {
13 26
 			width: 48px;
@@ -27,6 +40,10 @@
27 40
 			.toolbox-button {
28 41
 				margin-right: 0;
29 42
 			}
43
+
44
+			.toolbox-button:last-of-type {
45
+				top: 0;
46
+			}
30 47
 		}
31 48
 	}
32 49
 
@@ -56,6 +73,7 @@
56 73
 		.toolbox-button {
57 74
 			margin-right: 8px;
58 75
 			touch-action: manipulation;
76
+			position: relative;
59 77
 		}
60 78
 
61 79
 		.toolbox-button:last-of-type {

BIN
images/GIPHY_icon.png Ver fichero


BIN
images/GIPHY_logo.png Ver fichero


+ 7
- 0
lang/main.json Ver fichero

@@ -421,6 +421,10 @@
421 421
         "veryBad": "Very Bad",
422 422
         "veryGood": "Very Good"
423 423
     },
424
+    "giphy": {
425
+        "noResults": "No results found :(",
426
+        "search": "Search GIPHY"
427
+    },
424 428
     "helpView": {
425 429
         "header": "Help center"
426 430
     },
@@ -487,6 +491,7 @@
487 491
         "focusLocal": "Focus on your video",
488 492
         "focusRemote": "Focus on another person's video",
489 493
         "fullScreen": "View or exit full screen",
494
+        "giphyMenu": "Toggle GIPHY menu",
490 495
         "keyboardShortcuts": "Keyboard shortcuts",
491 496
         "localRecording": "Show or hide local recording controls",
492 497
         "mute": "Mute or unmute your microphone",
@@ -1007,6 +1012,7 @@
1007 1012
             "expand": "Expand",
1008 1013
             "feedback": "Leave feedback",
1009 1014
             "fullScreen": "Toggle full screen",
1015
+            "giphy": "Toggle GIPHY menu",
1010 1016
             "grantModerator": "Grant Moderator Rights",
1011 1017
             "hangup": "Leave the meeting",
1012 1018
             "help": "Help",
@@ -1076,6 +1082,7 @@
1076 1082
         "exitFullScreen": "Exit full screen",
1077 1083
         "exitTileView": "Exit tile view",
1078 1084
         "feedback": "Leave feedback",
1085
+        "giphy": "Toggle GIPHY menu",
1079 1086
         "hangup": "Leave the meeting",
1080 1087
         "help": "Help",
1081 1088
         "invite": "Invite people",

+ 666
- 3
package-lock.json
La diferencia del archivo ha sido suprimido porque es demasiado grande
Ver fichero


+ 2
- 0
package.json Ver fichero

@@ -33,6 +33,8 @@
33 33
     "@atlaskit/theme": "11.0.2",
34 34
     "@atlaskit/toggle": "12.0.3",
35 35
     "@atlaskit/tooltip": "17.1.2",
36
+    "@giphy/js-fetch-api": "4.1.2",
37
+    "@giphy/react-components": "5.6.0",
36 38
     "@hapi/bourne": "2.0.0",
37 39
     "@jitsi/js-utils": "2.0.0",
38 40
     "@jitsi/logger": "2.0.0",

+ 12
- 0
react/features/analytics/AnalyticsEvents.js Ver fichero

@@ -899,3 +899,15 @@ export function createBreakoutRoomsEvent(actionSubject) {
899 899
         source: 'breakout.rooms'
900 900
     };
901 901
 }
902
+
903
+/**
904
+ * Creates and event which indicates a GIF was sent.
905
+ *
906
+ * @returns {Object} The event in a format suitable for sending via
907
+ * sendAnalytics.
908
+ */
909
+export function createGifSentEvent() {
910
+    return {
911
+        action: 'gif.sent'
912
+    };
913
+}

+ 1
- 0
react/features/app/middlewares.any.js Ver fichero

@@ -30,6 +30,7 @@ import '../display-name/middleware';
30 30
 import '../etherpad/middleware';
31 31
 import '../filmstrip/middleware';
32 32
 import '../follow-me/middleware';
33
+import '../gifs/middleware';
33 34
 import '../invite/middleware';
34 35
 import '../jaas/middleware';
35 36
 import '../large-video/middleware';

+ 1
- 0
react/features/app/reducers.any.js Ver fichero

@@ -33,6 +33,7 @@ import '../dynamic-branding/reducer';
33 33
 import '../etherpad/reducer';
34 34
 import '../filmstrip/reducer';
35 35
 import '../follow-me/reducer';
36
+import '../gifs/reducer';
36 37
 import '../google-api/reducer';
37 38
 import '../invite/reducer';
38 39
 import '../jaas/reducer';

+ 1
- 0
react/features/base/config/configWhitelist.js Ver fichero

@@ -161,6 +161,7 @@ export default [
161 161
     'forceJVB121Ratio',
162 162
     'forceTurnRelay',
163 163
     'gatherStats',
164
+    'giphy',
164 165
     'googleApiApplicationClientID',
165 166
     'hiddenPremeetingButtons',
166 167
     'hideConferenceSubject',

+ 4
- 0
react/features/base/premeeting/components/web/InputField.js Ver fichero

@@ -2,6 +2,7 @@
2 2
 
3 3
 import React, { PureComponent } from 'react';
4 4
 
5
+import { isMobileBrowser } from '../../../environment/utils';
5 6
 import { getFieldValue } from '../../../react';
6 7
 
7 8
 type Props = {
@@ -132,6 +133,9 @@ export default class InputField extends PureComponent<Props, State> {
132 133
                 onKeyDown = { this._onKeyDown }
133 134
                 placeholder = { this.props.placeHolder }
134 135
                 readOnly = { this.props.readOnly }
136
+                // eslint-disable-next-line react/jsx-no-bind
137
+                ref = { inputElement => this.props.autoFocus && isMobileBrowser()
138
+                    && inputElement && inputElement.focus() }
135 139
                 type = { this.props.type }
136 140
                 value = { this.state.value } />
137 141
         );

+ 21
- 8
react/features/base/react/components/web/Message.js Ver fichero

@@ -3,6 +3,9 @@
3 3
 import React, { Component } from 'react';
4 4
 import { toArray } from 'react-emoji-render';
5 5
 
6
+import GifMessage from '../../../../chat/components/web/GifMessage';
7
+import { isGifMessage } from '../../../../gifs/functions';
8
+
6 9
 import Linkify from './Linkify';
7 10
 
8 11
 type Props = {
@@ -44,16 +47,26 @@ class Message extends Component<Props> {
44 47
 
45 48
         const content = [];
46 49
 
47
-        for (const token of tokens) {
48
-            if (token.includes('://')) {
50
+        // check if the message is a GIF
51
+        if (isGifMessage(text)) {
52
+            const url = text.substring(4, text.length - 1);
49 53
 
50
-                // Bypass the emojification when urls are involved
51
-                content.push(token);
52
-            } else {
53
-                content.push(...toArray(token, { className: 'smiley' }));
54
-            }
54
+            content.push(<GifMessage
55
+                key = { url }
56
+                url = { url } />);
57
+        } else {
58
+            for (const token of tokens) {
55 59
 
56
-            content.push(' ');
60
+                if (token.includes('://')) {
61
+
62
+                    // Bypass the emojification when urls are involved
63
+                    content.push(token);
64
+                } else {
65
+                    content.push(...toArray(token, { className: 'smiley' }));
66
+                }
67
+
68
+                content.push(' ');
69
+            }
57 70
         }
58 71
 
59 72
         content.forEach(token => {

+ 42
- 0
react/features/chat/components/web/GifMessage.js Ver fichero

@@ -0,0 +1,42 @@
1
+// @flow
2
+
3
+import { makeStyles } from '@material-ui/styles';
4
+import React from 'react';
5
+
6
+type Props = {
7
+
8
+    /**
9
+     * URL of the GIF.
10
+     */
11
+    url: string
12
+}
13
+
14
+const useStyles = makeStyles(() => {
15
+    return {
16
+        container: {
17
+            display: 'flex',
18
+            justifyContent: 'center',
19
+            overflow: 'hidden',
20
+            maxHeight: '150px',
21
+
22
+            '& img': {
23
+                maxWidth: '100%',
24
+                maxHeight: '100%',
25
+                objectFit: 'contain',
26
+                flexGrow: '1'
27
+            }
28
+        }
29
+    };
30
+});
31
+
32
+const GifMessage = ({ url }: Props) => {
33
+    const styles = useStyles();
34
+
35
+    return (<div className = { styles.container }>
36
+        <img
37
+            alt = { url }
38
+            src = { url } />
39
+    </div>);
40
+};
41
+
42
+export default GifMessage;

+ 48
- 10
react/features/chat/middleware.js Ver fichero

@@ -18,6 +18,9 @@ import {
18 18
 } from '../base/participants';
19 19
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
20 20
 import { playSound, registerSound, unregisterSound } from '../base/sounds';
21
+import { addGif } from '../gifs/actions';
22
+import { GIF_PREFIX } from '../gifs/constants';
23
+import { getGifDisplayMode, isGifMessage } from '../gifs/functions';
21 24
 import { NOTIFICATION_TIMEOUT_TYPE, showMessageNotification } from '../notifications';
22 25
 import { resetNbUnreadPollsMessages } from '../polls/actions';
23 26
 import { ADD_REACTION_MESSAGE } from '../reactions/actionTypes';
@@ -226,25 +229,21 @@ function _addChatMsgListener(conference, store) {
226 229
     conference.on(
227 230
         JitsiConferenceEvents.MESSAGE_RECEIVED,
228 231
         (id, message, timestamp) => {
229
-            _handleReceivedMessage(store, {
230
-                id,
232
+            _onConferenceMessageReceived(store, { id,
231 233
                 message,
232
-                privateMessage: false,
233
-                lobbyChat: false,
234
-                timestamp
235
-            });
234
+                timestamp,
235
+                privateMessage: false });
236 236
         }
237 237
     );
238 238
 
239 239
     conference.on(
240 240
         JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
241 241
         (id, message, timestamp) => {
242
-            _handleReceivedMessage(store, {
242
+            _onConferenceMessageReceived(store, {
243 243
                 id,
244 244
                 message,
245
-                privateMessage: true,
246
-                lobbyChat: false,
247
-                timestamp
245
+                timestamp,
246
+                privateMessage: true
248 247
             });
249 248
         }
250 249
     );
@@ -283,6 +282,45 @@ function _addChatMsgListener(conference, store) {
283 282
         });
284 283
 }
285 284
 
285
+/**
286
+ * Handles a received message.
287
+ *
288
+ * @param {Object} store - Redux store.
289
+ * @param {Object} message - The message object.
290
+ * @returns {void}
291
+ */
292
+function _onConferenceMessageReceived(store, { id, message, timestamp, privateMessage }) {
293
+    const isGif = isGifMessage(message);
294
+
295
+    if (isGif) {
296
+        _handleGifMessageReceived(store, id, message);
297
+        if (getGifDisplayMode(store.getState()) === 'tile') {
298
+            return;
299
+        }
300
+    }
301
+    _handleReceivedMessage(store, {
302
+        id,
303
+        message,
304
+        privateMessage,
305
+        lobbyChat: false,
306
+        timestamp
307
+    }, true, isGif);
308
+}
309
+
310
+/**
311
+ * Handles a received gif message.
312
+ *
313
+ * @param {Object} store - Redux store.
314
+ * @param {string} id - Id of the participant that sent the message.
315
+ * @param {string} message - The message sent.
316
+ * @returns {void}
317
+ */
318
+function _handleGifMessageReceived(store, id, message) {
319
+    const url = message.substring(GIF_PREFIX.length, message.length - 1);
320
+
321
+    store.dispatch(addGif(id, url));
322
+}
323
+
286 324
 /**
287 325
  * Handles a chat error received from the xmpp server.
288 326
  *

+ 102
- 12
react/features/filmstrip/components/web/Thumbnail.js Ver fichero

@@ -24,6 +24,8 @@ import {
24 24
     updateLastTrackVideoMediaEvent
25 25
 } from '../../../base/tracks';
26 26
 import { getVideoObjectPosition } from '../../../face-centering/functions';
27
+import { hideGif, showGif } from '../../../gifs/actions';
28
+import { getGifDisplayMode, getGifForParticipant } from '../../../gifs/functions';
27 29
 import { PresenceLabel } from '../../../presence-status';
28 30
 import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
29 31
 import {
@@ -96,6 +98,11 @@ export type Props = {|
96 98
      */
97 99
     _disableTileEnlargement: boolean,
98 100
 
101
+    /**
102
+     * URL of GIF sent by this participant, null if there's none.
103
+     */
104
+    _gifSrc ?: string,
105
+
99 106
     /**
100 107
      * The height of the Thumbnail.
101 108
      */
@@ -182,14 +189,14 @@ export type Props = {|
182 189
     _width: number,
183 190
 
184 191
     /**
185
-     * The redux dispatch function.
192
+     * An object containing CSS classes.
186 193
      */
187
-    dispatch: Function,
194
+    classes: Object,
188 195
 
189 196
     /**
190
-     * An object containing the CSS classes.
197
+     * The redux dispatch function.
191 198
      */
192
-    classes: Object,
199
+    dispatch: Function,
193 200
 
194 201
     /**
195 202
      * The horizontal offset in px for the thumbnail. Used to center the thumbnails from the last row in tile view.
@@ -267,10 +274,14 @@ const defaultStyles = theme => {
267 274
             position: 'absolute',
268 275
             width: '100%',
269 276
             height: '100%',
270
-            zIndex: '9',
277
+            zIndex: 9,
271 278
             borderRadius: '4px'
272 279
         },
273 280
 
281
+        borderIndicatorOnTop: {
282
+            zIndex: 11
283
+        },
284
+
274 285
         activeSpeaker: {
275 286
             '& .active-speaker-indicator': {
276 287
                 boxShadow: `inset 0px 0px 0px 4px ${theme.palette.link01Active} !important`
@@ -281,6 +292,25 @@ const defaultStyles = theme => {
281 292
             '& .raised-hand-border': {
282 293
                 boxShadow: `inset 0px 0px 0px 2px ${theme.palette.warning02} !important`
283 294
             }
295
+        },
296
+
297
+        gif: {
298
+            position: 'absolute',
299
+            width: '100%',
300
+            height: '100%',
301
+            zIndex: 11,
302
+            display: 'flex',
303
+            justifyContent: 'center',
304
+            alignItems: 'center',
305
+            overflow: 'hidden',
306
+            backgroundColor: theme.palette.ui02,
307
+
308
+            '& img': {
309
+                maxWidth: '100%',
310
+                maxHeight: '100%',
311
+                objectFit: 'contain',
312
+                flexGrow: '1'
313
+            }
284 314
         }
285 315
     };
286 316
 };
@@ -339,6 +369,8 @@ class Thumbnail extends Component<Props, State> {
339 369
         this._onTouchMove = this._onTouchMove.bind(this);
340 370
         this._showPopover = this._showPopover.bind(this);
341 371
         this._hidePopover = this._hidePopover.bind(this);
372
+        this._onGifMouseEnter = this._onGifMouseEnter.bind(this);
373
+        this._onGifMouseLeave = this._onGifMouseLeave.bind(this);
342 374
     }
343 375
 
344 376
     /**
@@ -741,6 +773,52 @@ class Thumbnail extends Component<Props, State> {
741 773
         return className;
742 774
     }
743 775
 
776
+    _onGifMouseEnter: () => void;
777
+
778
+    /**
779
+     * Keep showing the GIF for the current participant.
780
+     *
781
+     * @returns {void}
782
+     */
783
+    _onGifMouseEnter() {
784
+        const { dispatch, _participant: { id } } = this.props;
785
+
786
+        dispatch(showGif(id));
787
+    }
788
+
789
+    _onGifMouseLeave: () => void;
790
+
791
+    /**
792
+     * Keep showing the GIF for the current participant.
793
+     *
794
+     * @returns {void}
795
+     */
796
+    _onGifMouseLeave() {
797
+        const { dispatch, _participant: { id } } = this.props;
798
+
799
+        dispatch(hideGif(id));
800
+    }
801
+
802
+    /**
803
+     * Renders GIF.
804
+     *
805
+     * @returns {Component}
806
+     */
807
+    _renderGif() {
808
+        const { _gifSrc, classes } = this.props;
809
+
810
+        return _gifSrc && (
811
+            <div
812
+                className = { classes.gif }
813
+                onMouseEnter = { this._onGifMouseEnter }
814
+                onMouseLeave = { this._onGifMouseLeave }>
815
+                <img
816
+                    alt = 'GIF'
817
+                    src = { _gifSrc } />
818
+            </div>
819
+        );
820
+    }
821
+
744 822
     _onCanPlay: Object => void;
745 823
 
746 824
     /**
@@ -798,7 +876,8 @@ class Thumbnail extends Component<Props, State> {
798 876
             _localFlipX,
799 877
             _participant,
800 878
             _videoTrack,
801
-            classes
879
+            classes,
880
+            _gifSrc
802 881
         } = this.props;
803 882
         const { id } = _participant || {};
804 883
         const { isHovered, popoverVisible } = this.state;
@@ -850,9 +929,9 @@ class Thumbnail extends Component<Props, State> {
850 929
                     }
851 930
                 ) }
852 931
                 style = { styles.thumbnail }>
853
-                {local
932
+                {!_gifSrc && (local
854 933
                     ? <span id = 'localVideoWrapper'>{video}</span>
855
-                    : video}
934
+                    : video)}
856 935
                 <div className = { classes.containerBackground } />
857 936
                 <div
858 937
                     className = { clsx(classes.indicatorsContainer,
@@ -880,7 +959,7 @@ class Thumbnail extends Component<Props, State> {
880 959
                         local = { local }
881 960
                         participantId = { id } />
882 961
                 </div>
883
-                { this._renderAvatar(styles.avatar) }
962
+                {!_gifSrc && this._renderAvatar(styles.avatar) }
884 963
                 { !local && (
885 964
                     <div className = 'presence-label-container'>
886 965
                         <PresenceLabel
@@ -889,8 +968,15 @@ class Thumbnail extends Component<Props, State> {
889 968
                     </div>
890 969
                 )}
891 970
                 <ThumbnailAudioIndicator _audioTrack = { _audioTrack } />
892
-                <div className = { clsx(classes.borderIndicator, 'raised-hand-border') } />
893
-                <div className = { clsx(classes.borderIndicator, 'active-speaker-indicator') } />
971
+                {this._renderGif()}
972
+                <div
973
+                    className = { clsx(classes.borderIndicator,
974
+                    _gifSrc && classes.borderIndicatorOnTop,
975
+                    'raised-hand-border') } />
976
+                <div
977
+                    className = { clsx(classes.borderIndicator,
978
+                    _gifSrc && classes.borderIndicatorOnTop,
979
+                    'active-speaker-indicator') } />
894 980
             </span>
895 981
         );
896 982
     }
@@ -1003,6 +1089,9 @@ function _mapStateToProps(state, ownProps): Object {
1003 1089
     }
1004 1090
     }
1005 1091
 
1092
+    const { gifUrl: gifSrc } = getGifForParticipant(state, id);
1093
+    const mode = getGifDisplayMode(state);
1094
+
1006 1095
     return {
1007 1096
         _audioTrack,
1008 1097
         _currentLayout,
@@ -1023,7 +1112,8 @@ function _mapStateToProps(state, ownProps): Object {
1023 1112
         _raisedHand: hasRaisedHand(participant),
1024 1113
         _videoObjectPosition: getVideoObjectPosition(state, participant?.id),
1025 1114
         _videoTrack,
1026
-        ...size
1115
+        ...size,
1116
+        _gifSrc: mode === 'chat' ? null : gifSrc
1027 1117
     };
1028 1118
 }
1029 1119
 

+ 55
- 0
react/features/gifs/actionTypes.js Ver fichero

@@ -0,0 +1,55 @@
1
+/**
2
+ * Adds a gif for a given participant.
3
+ * {{
4
+ *      type: ADD_GIF_FOR_PARTICIPANT,
5
+ *      participantId: string,
6
+ *      gifUrl: string,
7
+ *      timeoutID: number
8
+ * }}
9
+ */
10
+export const ADD_GIF_FOR_PARTICIPANT = 'ADD_GIF_FOR_PARTICIPANT';
11
+
12
+/**
13
+ * Set timeout to hide a gif for a given participant.
14
+ * {{
15
+ *      type: HIDE_GIF_FOR_PARTICIPANT,
16
+ *      participantId: string
17
+ * }}
18
+ */
19
+export const HIDE_GIF_FOR_PARTICIPANT = 'HIDE_GIF_FOR_PARTICIPANT';
20
+
21
+/**
22
+ * Removes a gif for a given participant.
23
+ * {{
24
+ *      type: REMOVE_GIF_FOR_PARTICIPANT,
25
+ *      participantId: string
26
+ * }}
27
+ */
28
+export const REMOVE_GIF_FOR_PARTICIPANT = 'REMOVE_GIF_FOR_PARTICIPANT';
29
+
30
+/**
31
+ * Set gif menu drawer visibility.
32
+ * {{
33
+ *      type: SET_GIF_DRAWER_VISIBILITY,
34
+ *      visible: boolean
35
+ * }}
36
+ */
37
+export const SET_GIF_DRAWER_VISIBILITY = 'SET_GIF_DRAWER_VISIBILITY';
38
+
39
+/**
40
+ * Set gif menu visibility.
41
+ * {{
42
+ *      type: SET_GIF_MENU_VISIBILITY,
43
+ *      visible: boolean
44
+ * }}
45
+ */
46
+export const SET_GIF_MENU_VISIBILITY = 'SET_GIF_MENU_VISIBILITY';
47
+
48
+/**
49
+ * Keep showing a gif for a given participant.
50
+ * {{
51
+ *      type: SHOW_GIF_FOR_PARTICIPANT,
52
+ *      participantId: string
53
+ * }}
54
+ */
55
+export const SHOW_GIF_FOR_PARTICIPANT = 'SHOW_GIF_FOR_PARTICIPANT';

+ 88
- 0
react/features/gifs/actions.js Ver fichero

@@ -0,0 +1,88 @@
1
+import {
2
+    ADD_GIF_FOR_PARTICIPANT,
3
+    HIDE_GIF_FOR_PARTICIPANT,
4
+    REMOVE_GIF_FOR_PARTICIPANT,
5
+    SET_GIF_DRAWER_VISIBILITY,
6
+    SET_GIF_MENU_VISIBILITY,
7
+    SHOW_GIF_FOR_PARTICIPANT
8
+} from './actionTypes';
9
+
10
+/**
11
+ * Adds a GIF for a given participant.
12
+ *
13
+ * @param {string} participantId - The id of the participant that sent the GIF.
14
+ * @param {string} gifUrl - The URL of the GIF.
15
+ * @returns {Object}
16
+ */
17
+export function addGif(participantId, gifUrl) {
18
+    return {
19
+        type: ADD_GIF_FOR_PARTICIPANT,
20
+        participantId,
21
+        gifUrl
22
+    };
23
+}
24
+
25
+/**
26
+ * Removes the GIF of the given participant.
27
+ *
28
+ * @param {string} participantId - The Id of the participant for whom to remove the GIF.
29
+ * @returns {Object}
30
+ */
31
+export function removeGif(participantId) {
32
+    return {
33
+        type: REMOVE_GIF_FOR_PARTICIPANT,
34
+        participantId
35
+    };
36
+}
37
+
38
+/**
39
+ * Keep showing the GIF of the given participant.
40
+ *
41
+ * @param {string} participantId - The Id of the participant for whom to show the GIF.
42
+ * @returns {Object}
43
+ */
44
+export function showGif(participantId) {
45
+    return {
46
+        type: SHOW_GIF_FOR_PARTICIPANT,
47
+        participantId
48
+    };
49
+}
50
+
51
+/**
52
+ * Set timeout to hide the GIF of the given participant.
53
+ *
54
+ * @param {string} participantId - The Id of the participant for whom to show the GIF.
55
+ * @returns {Object}
56
+ */
57
+export function hideGif(participantId) {
58
+    return {
59
+        type: HIDE_GIF_FOR_PARTICIPANT,
60
+        participantId
61
+    };
62
+}
63
+
64
+/**
65
+ * Set visibility of the GIF drawer.
66
+ *
67
+ * @param {boolean} visible - Whether or not it should be visible.
68
+ * @returns {Object}
69
+ */
70
+export function setGifDrawerVisibility(visible) {
71
+    return {
72
+        type: SET_GIF_DRAWER_VISIBILITY,
73
+        visible
74
+    };
75
+}
76
+
77
+/**
78
+ * Set visibility of the GIF menu.
79
+ *
80
+ * @param {boolean} visible - Whether or not it should be visible.
81
+ * @returns {Object}
82
+ */
83
+export function setGifMenuVisibility(visible) {
84
+    return {
85
+        type: SET_GIF_MENU_VISIBILITY,
86
+        visible
87
+    };
88
+}

+ 1
- 0
react/features/gifs/components/_.web.js Ver fichero

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

+ 1
- 0
react/features/gifs/components/index.js Ver fichero

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

+ 223
- 0
react/features/gifs/components/web/GifsMenu.js Ver fichero

@@ -0,0 +1,223 @@
1
+// @flow
2
+
3
+import { GiphyFetch } from '@giphy/js-fetch-api';
4
+import { Grid } from '@giphy/react-components';
5
+import { makeStyles } from '@material-ui/core';
6
+import clsx from 'clsx';
7
+import React, { useCallback, useEffect, useState } from 'react';
8
+import { useTranslation } from 'react-i18next';
9
+import { batch, useDispatch, useSelector } from 'react-redux';
10
+
11
+import { createGifSentEvent, sendAnalytics } from '../../../analytics';
12
+import InputField from '../../../base/premeeting/components/web/InputField';
13
+import BaseTheme from '../../../base/ui/components/BaseTheme';
14
+import { sendMessage } from '../../../chat/actions.any';
15
+import { SCROLL_SIZE } from '../../../filmstrip';
16
+import { toggleReactionsMenuVisibility } from '../../../reactions/actions.web';
17
+import { setOverflowMenuVisible } from '../../../toolbox/actions.web';
18
+import { Drawer, JitsiPortal } from '../../../toolbox/components/web';
19
+import { showOverflowDrawer } from '../../../toolbox/functions.web';
20
+import { setGifDrawerVisibility } from '../../actions';
21
+import { formatGifUrlMessage, getGifAPIKey, getGifUrl } from '../../functions';
22
+
23
+const OVERFLOW_DRAWER_PADDING = BaseTheme.spacing(3);
24
+
25
+const useStyles = makeStyles(theme => {
26
+    return {
27
+        gifsMenu: {
28
+            width: '100%',
29
+            marginBottom: `${theme.spacing(2)}px`,
30
+            display: 'flex',
31
+            flexDirection: 'column',
32
+
33
+            '& div:focus': {
34
+                border: '1px solid red !important',
35
+                boxSizing: 'border-box'
36
+            }
37
+        },
38
+
39
+        searchField: {
40
+            backgroundColor: theme.palette.field01,
41
+            borderRadius: `${theme.shape.borderRadius}px`,
42
+            border: 'none',
43
+            outline: 0,
44
+            ...theme.typography.bodyShortRegular,
45
+            lineHeight: `${theme.typography.bodyShortRegular.lineHeight}px`,
46
+            color: theme.palette.text01,
47
+            padding: `${theme.spacing(2)}px ${theme.spacing(3)}px`,
48
+            width: '100%',
49
+            marginBottom: `${theme.spacing(3)}px`
50
+        },
51
+
52
+        gifContainer: {
53
+            height: '245px',
54
+            overflowY: 'auto'
55
+        },
56
+
57
+        logoContainer: {
58
+            width: `calc(100% - ${SCROLL_SIZE}px)`,
59
+            backgroundColor: '#121119',
60
+            display: 'flex',
61
+            alignItems: 'center',
62
+            justifyContent: 'center',
63
+            color: '#fff',
64
+            marginTop: `${theme.spacing(1)}px`
65
+        },
66
+
67
+        overflowMenu: {
68
+            padding: `${theme.spacing(3)}px`,
69
+            width: '100%',
70
+            boxSizing: 'border-box'
71
+        },
72
+
73
+        gifContainerOverflow: {
74
+            flexGrow: 1
75
+        },
76
+
77
+        drawer: {
78
+            display: 'flex',
79
+            height: '100%'
80
+        }
81
+    };
82
+});
83
+
84
+/**
85
+ * Gifs menu.
86
+ *
87
+ * @returns {ReactElement}
88
+ */
89
+function GifsMenu() {
90
+    const API_KEY = useSelector(getGifAPIKey);
91
+    const giphyFetch = new GiphyFetch(API_KEY);
92
+    const [ searchKey, setSearchKey ] = useState();
93
+    const styles = useStyles();
94
+    const dispatch = useDispatch();
95
+    const { t } = useTranslation();
96
+    const overflowDrawer = useSelector(showOverflowDrawer);
97
+    const { clientWidth } = useSelector(state => state['features/base/responsive-ui']);
98
+
99
+    const fetchGifs = useCallback(async (offset = 0) => {
100
+        const options = {
101
+            rating: 'pg-13',
102
+            limit: 20,
103
+            offset
104
+        };
105
+
106
+        if (!searchKey) {
107
+            return await giphyFetch.trending(options);
108
+        }
109
+
110
+        return await giphyFetch.search(searchKey, options);
111
+    }, [ searchKey ]);
112
+
113
+    const onDrawerClose = useCallback(() => {
114
+        dispatch(setGifDrawerVisibility(false));
115
+        dispatch(setOverflowMenuVisible(false));
116
+    });
117
+
118
+    const handleGifClick = useCallback((gif, e) => {
119
+        e?.stopPropagation();
120
+        const url = getGifUrl(gif);
121
+
122
+        sendAnalytics(createGifSentEvent());
123
+        batch(() => {
124
+            dispatch(sendMessage(formatGifUrlMessage(url), true));
125
+            dispatch(toggleReactionsMenuVisibility());
126
+            overflowDrawer && onDrawerClose();
127
+        });
128
+    }, [ dispatch, overflowDrawer ]);
129
+
130
+    const handleGifKeyPress = useCallback((gif, e) => {
131
+        if (e.nativeEvent.keyCode === 13) {
132
+            handleGifClick(gif, null);
133
+        }
134
+    }, [ handleGifClick ]);
135
+
136
+    const handleSearchKeyChange = useCallback(value => {
137
+        setSearchKey(value);
138
+    });
139
+
140
+    const handleKeyDown = useCallback(e => {
141
+        if (e.keyCode === 38) { // up arrow
142
+            e.preventDefault();
143
+
144
+            // if the first gif is focused move focus to the input
145
+            if (document.activeElement.previousElementSibling === null) {
146
+                document.querySelector('.gif-input').focus();
147
+            } else {
148
+                document.activeElement.previousElementSibling.focus();
149
+            }
150
+        } else if (e.keyCode === 40) { // down arrow
151
+            e.preventDefault();
152
+
153
+            // if the input is focused move focus to the first gif
154
+            if (document.activeElement.classList.contains('gif-input')) {
155
+                document.querySelector('.giphy-gif').focus();
156
+            } else {
157
+                document.activeElement.nextElementSibling.focus();
158
+            }
159
+        }
160
+    }, []);
161
+
162
+    useEffect(() => {
163
+        document.addEventListener('keydown', handleKeyDown);
164
+
165
+        return () => document.removeEventListener('keydown', handleKeyDown);
166
+    }, []);
167
+
168
+    // For some reason, the Grid component does not do an initial call on mobile.
169
+    // This fixes that.
170
+    useEffect(() => setSearchKey(''), []);
171
+
172
+    const gifMenu = (
173
+        <div
174
+            className = { clsx(styles.gifsMenu,
175
+                overflowDrawer && styles.overflowMenu
176
+            ) }>
177
+            <InputField
178
+                autoFocus = { true }
179
+                className = { clsx(styles.searchField, 'gif-input') }
180
+                onChange = { handleSearchKeyChange }
181
+                placeHolder = { t('giphy.search') }
182
+                testId = 'gifSearch.key'
183
+                type = 'text' />
184
+            <div
185
+                className = { clsx(styles.gifContainer,
186
+                overflowDrawer && styles.gifContainerOverflow) }>
187
+                <Grid
188
+                    columns = { 2 }
189
+                    fetchGifs = { fetchGifs }
190
+                    gutter = { 6 }
191
+                    hideAttribution = { true }
192
+                    key = { searchKey }
193
+                    noLink = { true }
194
+                    noResultsMessage = { t('giphy.noResults') }
195
+                    onGifClick = { handleGifClick }
196
+                    onGifKeyPress = { handleGifKeyPress }
197
+                    width = { overflowDrawer
198
+                        ? clientWidth - (2 * OVERFLOW_DRAWER_PADDING) - SCROLL_SIZE
199
+                        : 320
200
+                    } />
201
+            </div>
202
+            <div className = { styles.logoContainer }>
203
+                <span>Powered by</span>
204
+                <img
205
+                    alt = 'GIPHY Logo'
206
+                    src = 'images/GIPHY_logo.png' />
207
+            </div>
208
+        </div>
209
+    );
210
+
211
+    return overflowDrawer ? (
212
+        <JitsiPortal>
213
+            <Drawer
214
+                className = { styles.drawer }
215
+                isOpen = { true }
216
+                onClose = { onDrawerClose }>
217
+                {gifMenu}
218
+            </Drawer>
219
+        </JitsiPortal>
220
+    ) : gifMenu;
221
+}
222
+
223
+export default GifsMenu;

+ 42
- 0
react/features/gifs/components/web/GifsMenuButton.js Ver fichero

@@ -0,0 +1,42 @@
1
+import React, { useCallback } from 'react';
2
+import { useTranslation } from 'react-i18next';
3
+import { useDispatch, useSelector } from 'react-redux';
4
+
5
+import ReactionButton from '../../../reactions/components/web/ReactionButton';
6
+import { showOverflowDrawer } from '../../../toolbox/functions.web';
7
+import { setGifDrawerVisibility, setGifMenuVisibility } from '../../actions';
8
+import { isGifsMenuOpen } from '../../functions';
9
+
10
+const GifsMenuButton = () => {
11
+    const menuOpen = useSelector(isGifsMenuOpen);
12
+    const overflowDrawer = useSelector(showOverflowDrawer);
13
+    const { t } = useTranslation();
14
+    const dispatch = useDispatch();
15
+
16
+    const icon = (
17
+        <img
18
+            alt = 'GIPHY Logo'
19
+            height = { 24 }
20
+            src = 'images/GIPHY_icon.png' />
21
+    );
22
+
23
+    const handleClick = useCallback(() =>
24
+        dispatch(
25
+            overflowDrawer
26
+                ? setGifDrawerVisibility(!menuOpen)
27
+                : setGifMenuVisibility(!menuOpen)
28
+        )
29
+    , [ menuOpen, overflowDrawer ]);
30
+
31
+    return (
32
+        <ReactionButton
33
+            accessibilityLabel = { t('toolbar.accessibilityLabel.giphy') }
34
+            icon = { icon }
35
+            key = 'gif'
36
+            onClick = { handleClick }
37
+            toggled = { true }
38
+            tooltip = { t('toolbar.accessibilityLabel.giphy') } />
39
+    );
40
+};
41
+
42
+export default GifsMenuButton;

+ 4
- 0
react/features/gifs/components/web/index.js Ver fichero

@@ -0,0 +1,4 @@
1
+// @flow
2
+
3
+export { default as GifsMenuButton } from './GifsMenuButton';
4
+export { default as GifsMenu } from './GifsMenu';

+ 9
- 0
react/features/gifs/constants.js Ver fichero

@@ -0,0 +1,9 @@
1
+/**
2
+ * The default time that GIFs will be displayed on the tile.
3
+ */
4
+export const GIF_DEFAULT_TIMEOUT = 5000;
5
+
6
+/**
7
+ * The prefix for formatted GIF messages.
8
+ */
9
+export const GIF_PREFIX = 'gif[';

+ 96
- 0
react/features/gifs/functions.js Ver fichero

@@ -0,0 +1,96 @@
1
+import { showOverflowDrawer } from '../toolbox/functions.web';
2
+
3
+import { GIF_PREFIX } from './constants';
4
+
5
+/**
6
+ * Gets the URL of the GIF for the given participant or null if there's none.
7
+ *
8
+ * @param {Object} state - Redux state.
9
+ * @param {string} participantId - Id of the participant for which to remove the GIF.
10
+ * @returns {Object}
11
+ */
12
+export function getGifForParticipant(state, participantId) {
13
+    return state['features/gifs'].gifList.get(participantId) || {};
14
+}
15
+
16
+/**
17
+ * Whether or not the message is a GIF message.
18
+ *
19
+ * @param {string} message - Message to check.
20
+ * @returns {boolean}
21
+ */
22
+export function isGifMessage(message) {
23
+    return message.trim().startsWith(GIF_PREFIX);
24
+}
25
+
26
+/**
27
+ * Returns the visibility state of the gifs menu.
28
+ *
29
+ * @param {Object} state - The state of the application.
30
+ * @returns {boolean}
31
+ */
32
+export function isGifsMenuOpen(state) {
33
+    const overflowDrawer = showOverflowDrawer(state);
34
+    const { drawerVisible, menuOpen } = state['features/gifs'];
35
+
36
+    return overflowDrawer ? drawerVisible : menuOpen;
37
+}
38
+
39
+/**
40
+ * Returns the url of the gif selected in the gifs menu.
41
+ *
42
+ * @param {Object} gif - The gif data.
43
+ * @returns {boolean}
44
+ */
45
+export function getGifUrl(gif) {
46
+    const embedUrl = gif?.embed_url || '';
47
+    const idx = embedUrl.lastIndexOf('/');
48
+    const id = embedUrl.substr(idx + 1);
49
+
50
+    return `https://i.giphy.com/media/${id}/giphy.webp`;
51
+}
52
+
53
+/**
54
+ * Formats the gif message.
55
+ *
56
+ * @param {string} url - GIF url.
57
+ * @returns {string}
58
+ */
59
+export function formatGifUrlMessage(url) {
60
+    return `${GIF_PREFIX}${url}]`;
61
+}
62
+
63
+/**
64
+ * Get the Giphy API Key from config.
65
+ *
66
+ * @param {Object} state - Redux state.
67
+ * @returns {string}
68
+ */
69
+export function getGifAPIKey(state) {
70
+    return state['features/base/config']?.giphy?.sdkKey;
71
+}
72
+
73
+/**
74
+ * Returns whether or not the feature is enabled.
75
+ *
76
+ * @param {Object} state - Redux state.
77
+ * @returns {boolean}
78
+ */
79
+export function isGifEnabled(state) {
80
+    const { disableThirdPartyRequests } = state['features/base/config'];
81
+    const { giphy } = state['features/base/config'];
82
+
83
+    return !disableThirdPartyRequests && giphy?.enabled && Boolean(giphy?.sdkKey);
84
+}
85
+
86
+/**
87
+ * Get the GIF display mode.
88
+ *
89
+ * @param {Object} state - Redux state.
90
+ * @returns {string}
91
+ */
92
+export function getGifDisplayMode(state) {
93
+    const { giphy } = state['features/base/config'];
94
+
95
+    return giphy?.displayMode || 'all';
96
+}

+ 60
- 0
react/features/gifs/middleware.js Ver fichero

@@ -0,0 +1,60 @@
1
+import { MiddlewareRegistry } from '../base/redux';
2
+
3
+import { ADD_GIF_FOR_PARTICIPANT, HIDE_GIF_FOR_PARTICIPANT, SHOW_GIF_FOR_PARTICIPANT } from './actionTypes';
4
+import { removeGif } from './actions';
5
+import { GIF_DEFAULT_TIMEOUT } from './constants';
6
+import { getGifForParticipant } from './functions';
7
+
8
+/**
9
+ * Middleware which intercepts Gifs actions to handle changes to the
10
+ * visibility timeout of the Gifs.
11
+ *
12
+ * @param {Store} store - The redux store.
13
+ * @returns {Function}
14
+ */
15
+MiddlewareRegistry.register(store => next => action => {
16
+    const { dispatch, getState } = store;
17
+    const state = getState();
18
+
19
+    switch (action.type) {
20
+    case ADD_GIF_FOR_PARTICIPANT: {
21
+        const id = action.participantId;
22
+        const { giphy } = state['features/base/config'];
23
+
24
+        _clearGifTimeout(state, id);
25
+        const timeoutID = setTimeout(() => dispatch(removeGif(id)), giphy?.tileTime || GIF_DEFAULT_TIMEOUT);
26
+
27
+        action.timeoutID = timeoutID;
28
+        break;
29
+    }
30
+    case SHOW_GIF_FOR_PARTICIPANT: {
31
+        const id = action.participantId;
32
+
33
+        _clearGifTimeout(state, id);
34
+        break;
35
+    }
36
+    case HIDE_GIF_FOR_PARTICIPANT: {
37
+        const { giphy } = state['features/base/config'];
38
+        const id = action.participantId;
39
+        const timeoutID = setTimeout(() => dispatch(removeGif(id)), giphy?.tileTime || GIF_DEFAULT_TIMEOUT);
40
+
41
+        action.timeoutID = timeoutID;
42
+        break;
43
+    }
44
+    }
45
+
46
+    return next(action);
47
+});
48
+
49
+/**
50
+ * Clears GIF timeout.
51
+ *
52
+ * @param {Object} state - Redux state.
53
+ * @param {string} id - Id of the participant for whom to clear the timeout.
54
+ * @returns {void}
55
+ */
56
+function _clearGifTimeout(state, id) {
57
+    const gif = getGifForParticipant(state, id);
58
+
59
+    clearTimeout(gif?.timeoutID);
60
+}

+ 73
- 0
react/features/gifs/reducer.js Ver fichero

@@ -0,0 +1,73 @@
1
+
2
+import { ReducerRegistry } from '../base/redux';
3
+
4
+import {
5
+    ADD_GIF_FOR_PARTICIPANT,
6
+    HIDE_GIF_FOR_PARTICIPANT,
7
+    REMOVE_GIF_FOR_PARTICIPANT,
8
+    SET_GIF_DRAWER_VISIBILITY,
9
+    SET_GIF_MENU_VISIBILITY
10
+} from './actionTypes';
11
+
12
+const initialState = {
13
+    drawerVisible: false,
14
+    gifList: new Map(),
15
+    menuOpen: false
16
+};
17
+
18
+ReducerRegistry.register(
19
+    'features/gifs',
20
+    (state = initialState, action) => {
21
+        switch (action.type) {
22
+        case ADD_GIF_FOR_PARTICIPANT: {
23
+            const newList = state.gifList;
24
+
25
+            newList.set(action.participantId, {
26
+                gifUrl: action.gifUrl,
27
+                timeoutID: action.timeoutID
28
+            });
29
+
30
+            return {
31
+                ...state,
32
+                gifList: newList
33
+            };
34
+        }
35
+        case REMOVE_GIF_FOR_PARTICIPANT: {
36
+            const newList = state.gifList;
37
+
38
+            newList.delete(action.participantId);
39
+
40
+            return {
41
+                ...state,
42
+                gifList: newList
43
+            };
44
+        }
45
+        case HIDE_GIF_FOR_PARTICIPANT: {
46
+            const newList = state.gifList;
47
+            const gif = state.gifList.get(action.participantId);
48
+
49
+            newList.set(action.participantId, {
50
+                gifUrl: gif.gifUrl,
51
+                timeoutID: action.timeoutID
52
+            });
53
+
54
+            return {
55
+                ...state,
56
+                gifList: newList
57
+            };
58
+        }
59
+        case SET_GIF_DRAWER_VISIBILITY:
60
+            return {
61
+                ...state,
62
+                drawerVisible: action.visible
63
+            };
64
+        case SET_GIF_MENU_VISIBILITY:
65
+            return {
66
+                ...state,
67
+                menuOpen: action.visible
68
+            };
69
+        }
70
+
71
+        return state;
72
+    });
73
+

+ 21
- 2
react/features/reactions/components/web/ReactionsMenu.js Ver fichero

@@ -3,6 +3,7 @@
3 3
 /* eslint-disable react/jsx-no-bind */
4 4
 
5 5
 import { withStyles } from '@material-ui/styles';
6
+import clsx from 'clsx';
6 7
 import React, { Component } from 'react';
7 8
 import { bindActionCreators } from 'redux';
8 9
 
@@ -15,6 +16,8 @@ import { isMobileBrowser } from '../../../base/environment/utils';
15 16
 import { translate } from '../../../base/i18n';
16 17
 import { getLocalParticipant, hasRaisedHand, raiseHand } from '../../../base/participants';
17 18
 import { connect } from '../../../base/redux';
19
+import { GifsMenu, GifsMenuButton } from '../../../gifs/components';
20
+import { isGifEnabled, isGifsMenuOpen } from '../../../gifs/functions';
18 21
 import { dockToolbox } from '../../../toolbox/actions.web';
19 22
 import { addReactionToBuffer } from '../../actions.any';
20 23
 import { toggleReactionsMenuVisibility } from '../../actions.web';
@@ -29,6 +32,16 @@ type Props = {
29 32
      */
30 33
     _dockToolbox: Function,
31 34
 
35
+    /**
36
+     * Whether or not the GIF feature is enabled.
37
+     */
38
+    _isGifEnabled: boolean,
39
+
40
+    /**
41
+     * Whether or not the GIF menu is visible.
42
+     */
43
+    _isGifMenuVisible: boolean,
44
+
32 45
     /**
33 46
      * Whether or not it's a mobile browser.
34 47
      */
@@ -193,12 +206,16 @@ class ReactionsMenu extends Component<Props> {
193 206
      * @inheritdoc
194 207
      */
195 208
     render() {
196
-        const { _raisedHand, t, overflowMenu, _isMobile, classes } = this.props;
209
+        const { _raisedHand, t, overflowMenu, _isMobile, classes, _isGifMenuVisible, _isGifEnabled } = this.props;
197 210
 
198 211
         return (
199
-            <div className = { `reactions-menu ${overflowMenu ? `overflow ${classes.overflow}` : ''}` }>
212
+            <div
213
+                className = { clsx('reactions-menu', _isGifEnabled && 'with-gif',
214
+                    overflowMenu && `overflow ${classes.overflow}`) }>
215
+                {_isGifEnabled && _isGifMenuVisible && <GifsMenu />}
200 216
                 <div className = 'reactions-row'>
201 217
                     { this._getReactionButtons() }
218
+                    {_isGifEnabled && <GifsMenuButton />}
202 219
                 </div>
203 220
                 {_isMobile && (
204 221
                     <div className = 'raise-hand-row'>
@@ -231,6 +248,8 @@ function mapStateToProps(state) {
231 248
     return {
232 249
         _localParticipantID: localParticipant.id,
233 250
         _isMobile: isMobileBrowser(),
251
+        _isGifEnabled: isGifEnabled(state),
252
+        _isGifMenuVisible: isGifsMenuOpen(state),
234 253
         _raisedHand: hasRaisedHand(localParticipant)
235 254
     };
236 255
 }

+ 7
- 1
react/features/toolbox/components/web/Drawer.js Ver fichero

@@ -8,6 +8,11 @@ import { DRAWER_MAX_HEIGHT } from '../../constants';
8 8
 
9 9
 type Props = {
10 10
 
11
+    /**
12
+     * Class name for custom styles.
13
+     */
14
+    className: string,
15
+
11 16
     /**
12 17
      * The component(s) to be displayed within the drawer menu.
13 18
      */
@@ -40,6 +45,7 @@ const useStyles = makeStyles(theme => {
40 45
  */
41 46
 function Drawer({
42 47
     children,
48
+    className = '',
43 49
     isOpen,
44 50
     onClose
45 51
 }: Props) {
@@ -72,7 +78,7 @@ function Drawer({
72 78
                 className = 'drawer-menu-container'
73 79
                 onClick = { handleOutsideClick }>
74 80
                 <div
75
-                    className = { `drawer-menu ${styles.drawer}` }
81
+                    className = { `drawer-menu ${styles.drawer} ${className}` }
76 82
                     onClick = { handleInsideClick }>
77 83
                     {children}
78 84
                 </div>

+ 27
- 1
react/features/toolbox/components/web/Toolbox.js Ver fichero

@@ -3,6 +3,7 @@
3 3
 import { withStyles } from '@material-ui/core/styles';
4 4
 import clsx from 'clsx';
5 5
 import React, { Component, Fragment } from 'react';
6
+import { batch } from 'react-redux';
6 7
 
7 8
 import keyboardShortcut from '../../../../../modules/keyboardshortcut/keyboardshortcut';
8 9
 import {
@@ -30,6 +31,8 @@ import { ChatButton } from '../../../chat/components';
30 31
 import { EmbedMeetingButton } from '../../../embed-meeting';
31 32
 import { SharedDocumentButton } from '../../../etherpad';
32 33
 import { FeedbackButton } from '../../../feedback';
34
+import { setGifMenuVisibility } from '../../../gifs/actions';
35
+import { isGifEnabled } from '../../../gifs/functions';
33 36
 import { InviteButton } from '../../../invite/components/add-people-dialog';
34 37
 import { isVpaasMeeting } from '../../../jaas/functions';
35 38
 import { KeyboardShortcutsButton } from '../../../keyboard-shortcuts';
@@ -41,6 +44,7 @@ import {
41 44
 import { ParticipantsPaneButton } from '../../../participants-pane/components/web';
42 45
 import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
43 46
 import { addReactionToBuffer } from '../../../reactions/actions.any';
47
+import { toggleReactionsMenuVisibility } from '../../../reactions/actions.web';
44 48
 import { ReactionsMenuButton } from '../../../reactions/components';
45 49
 import { REACTIONS, REACTIONS_MENU_HEIGHT } from '../../../reactions/constants';
46 50
 import { isReactionsEnabled } from '../../../reactions/functions.any';
@@ -159,6 +163,11 @@ type Props = {
159 163
      */
160 164
     _fullScreen: boolean,
161 165
 
166
+    /**
167
+     * Whether or not the GIFs feature is enabled.
168
+     */
169
+    _gifsEnabled: boolean,
170
+
162 171
     /**
163 172
      * Whether the app has Salesforce integration.
164 173
      */
@@ -334,7 +343,7 @@ class Toolbox extends Component<Props> {
334 343
      * @returns {void}
335 344
      */
336 345
     componentDidMount() {
337
-        const { _toolbarButtons, t, dispatch, _reactionsEnabled } = this.props;
346
+        const { _toolbarButtons, t, dispatch, _reactionsEnabled, _gifsEnabled } = this.props;
338 347
         const KEYBOARD_SHORTCUTS = [
339 348
             isToolbarButtonEnabled('videoquality', _toolbarButtons) && {
340 349
                 character: 'A',
@@ -408,6 +417,22 @@ class Toolbox extends Component<Props> {
408 417
                     shortcut.helpDescription,
409 418
                     shortcut.altKey);
410 419
             });
420
+
421
+            if (_gifsEnabled) {
422
+                const onGifShortcut = () => {
423
+                    batch(() => {
424
+                        dispatch(toggleReactionsMenuVisibility());
425
+                        dispatch(setGifMenuVisibility(true));
426
+                    });
427
+                };
428
+
429
+                APP.keyboardshortcut.registerShortcut(
430
+                    'G',
431
+                    null,
432
+                    onGifShortcut,
433
+                    t('keyboardShortcuts.giphyMenu')
434
+                );
435
+            }
411 436
         }
412 437
     }
413 438
 
@@ -1410,6 +1435,7 @@ function _mapStateToProps(state, ownProps) {
1410 1435
         _disabled: Boolean(iAmRecorder || iAmSipGateway),
1411 1436
         _feedbackConfigured: Boolean(callStatsID),
1412 1437
         _fullScreen: fullScreen,
1438
+        _gifsEnabled: isGifEnabled(state),
1413 1439
         _isProfileDisabled: Boolean(disableProfile),
1414 1440
         _isIosMobile: isIosMobileBrowser(),
1415 1441
         _isMobile: isMobileBrowser(),

Loading…
Cancelar
Guardar