瀏覽代碼

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 3 年之前
父節點
當前提交
190041fc5a
沒有連結到貢獻者的電子郵件帳戶。

+ 15
- 0
config.js 查看文件

1300
     // Specifies whether the chat emoticons are disabled or not
1300
     // Specifies whether the chat emoticons are disabled or not
1301
     // disableChatSmileys: false,
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
     // Allow all above example options to include a trailing comma and
1318
     // Allow all above example options to include a trailing comma and
1304
     // prevent fear when commenting out the last value.
1319
     // prevent fear when commenting out the last value.
1305
     makeJsonParserHappy: 'even if last key had a trailing comma'
1320
     makeJsonParserHappy: 'even if last key had a trailing comma'

+ 18
- 0
css/_reactions-menu.scss 查看文件

7
 	border-radius: 3px;
7
 	border-radius: 3px;
8
 	padding: 16px;
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
 	&.overflow {
22
 	&.overflow {
23
+		width: 100%;
11
 
24
 
12
 		.toolbox-icon {
25
 		.toolbox-icon {
13
 			width: 48px;
26
 			width: 48px;
27
 			.toolbox-button {
40
 			.toolbox-button {
28
 				margin-right: 0;
41
 				margin-right: 0;
29
 			}
42
 			}
43
+
44
+			.toolbox-button:last-of-type {
45
+				top: 0;
46
+			}
30
 		}
47
 		}
31
 	}
48
 	}
32
 
49
 
56
 		.toolbox-button {
73
 		.toolbox-button {
57
 			margin-right: 8px;
74
 			margin-right: 8px;
58
 			touch-action: manipulation;
75
 			touch-action: manipulation;
76
+			position: relative;
59
 		}
77
 		}
60
 
78
 
61
 		.toolbox-button:last-of-type {
79
 		.toolbox-button:last-of-type {

二進制
images/GIPHY_icon.png 查看文件


二進制
images/GIPHY_logo.png 查看文件


+ 7
- 0
lang/main.json 查看文件

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

+ 666
- 3
package-lock.json
文件差異過大導致無法顯示
查看文件


+ 2
- 0
package.json 查看文件

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

+ 12
- 0
react/features/analytics/AnalyticsEvents.js 查看文件

899
         source: 'breakout.rooms'
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 查看文件

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

+ 1
- 0
react/features/app/reducers.any.js 查看文件

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

+ 1
- 0
react/features/base/config/configWhitelist.js 查看文件

161
     'forceJVB121Ratio',
161
     'forceJVB121Ratio',
162
     'forceTurnRelay',
162
     'forceTurnRelay',
163
     'gatherStats',
163
     'gatherStats',
164
+    'giphy',
164
     'googleApiApplicationClientID',
165
     'googleApiApplicationClientID',
165
     'hiddenPremeetingButtons',
166
     'hiddenPremeetingButtons',
166
     'hideConferenceSubject',
167
     'hideConferenceSubject',

+ 4
- 0
react/features/base/premeeting/components/web/InputField.js 查看文件

2
 
2
 
3
 import React, { PureComponent } from 'react';
3
 import React, { PureComponent } from 'react';
4
 
4
 
5
+import { isMobileBrowser } from '../../../environment/utils';
5
 import { getFieldValue } from '../../../react';
6
 import { getFieldValue } from '../../../react';
6
 
7
 
7
 type Props = {
8
 type Props = {
132
                 onKeyDown = { this._onKeyDown }
133
                 onKeyDown = { this._onKeyDown }
133
                 placeholder = { this.props.placeHolder }
134
                 placeholder = { this.props.placeHolder }
134
                 readOnly = { this.props.readOnly }
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
                 type = { this.props.type }
139
                 type = { this.props.type }
136
                 value = { this.state.value } />
140
                 value = { this.state.value } />
137
         );
141
         );

+ 21
- 8
react/features/base/react/components/web/Message.js 查看文件

3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 import { toArray } from 'react-emoji-render';
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
 import Linkify from './Linkify';
9
 import Linkify from './Linkify';
7
 
10
 
8
 type Props = {
11
 type Props = {
44
 
47
 
45
         const content = [];
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
         content.forEach(token => {
72
         content.forEach(token => {

+ 42
- 0
react/features/chat/components/web/GifMessage.js 查看文件

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 查看文件

18
 } from '../base/participants';
18
 } from '../base/participants';
19
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
19
 import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
20
 import { playSound, registerSound, unregisterSound } from '../base/sounds';
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
 import { NOTIFICATION_TIMEOUT_TYPE, showMessageNotification } from '../notifications';
24
 import { NOTIFICATION_TIMEOUT_TYPE, showMessageNotification } from '../notifications';
22
 import { resetNbUnreadPollsMessages } from '../polls/actions';
25
 import { resetNbUnreadPollsMessages } from '../polls/actions';
23
 import { ADD_REACTION_MESSAGE } from '../reactions/actionTypes';
26
 import { ADD_REACTION_MESSAGE } from '../reactions/actionTypes';
226
     conference.on(
229
     conference.on(
227
         JitsiConferenceEvents.MESSAGE_RECEIVED,
230
         JitsiConferenceEvents.MESSAGE_RECEIVED,
228
         (id, message, timestamp) => {
231
         (id, message, timestamp) => {
229
-            _handleReceivedMessage(store, {
230
-                id,
232
+            _onConferenceMessageReceived(store, { id,
231
                 message,
233
                 message,
232
-                privateMessage: false,
233
-                lobbyChat: false,
234
-                timestamp
235
-            });
234
+                timestamp,
235
+                privateMessage: false });
236
         }
236
         }
237
     );
237
     );
238
 
238
 
239
     conference.on(
239
     conference.on(
240
         JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
240
         JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
241
         (id, message, timestamp) => {
241
         (id, message, timestamp) => {
242
-            _handleReceivedMessage(store, {
242
+            _onConferenceMessageReceived(store, {
243
                 id,
243
                 id,
244
                 message,
244
                 message,
245
-                privateMessage: true,
246
-                lobbyChat: false,
247
-                timestamp
245
+                timestamp,
246
+                privateMessage: true
248
             });
247
             });
249
         }
248
         }
250
     );
249
     );
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
  * Handles a chat error received from the xmpp server.
325
  * Handles a chat error received from the xmpp server.
288
  *
326
  *

+ 102
- 12
react/features/filmstrip/components/web/Thumbnail.js 查看文件

24
     updateLastTrackVideoMediaEvent
24
     updateLastTrackVideoMediaEvent
25
 } from '../../../base/tracks';
25
 } from '../../../base/tracks';
26
 import { getVideoObjectPosition } from '../../../face-centering/functions';
26
 import { getVideoObjectPosition } from '../../../face-centering/functions';
27
+import { hideGif, showGif } from '../../../gifs/actions';
28
+import { getGifDisplayMode, getGifForParticipant } from '../../../gifs/functions';
27
 import { PresenceLabel } from '../../../presence-status';
29
 import { PresenceLabel } from '../../../presence-status';
28
 import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
30
 import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
29
 import {
31
 import {
96
      */
98
      */
97
     _disableTileEnlargement: boolean,
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
      * The height of the Thumbnail.
107
      * The height of the Thumbnail.
101
      */
108
      */
182
     _width: number,
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
      * The horizontal offset in px for the thumbnail. Used to center the thumbnails from the last row in tile view.
202
      * The horizontal offset in px for the thumbnail. Used to center the thumbnails from the last row in tile view.
267
             position: 'absolute',
274
             position: 'absolute',
268
             width: '100%',
275
             width: '100%',
269
             height: '100%',
276
             height: '100%',
270
-            zIndex: '9',
277
+            zIndex: 9,
271
             borderRadius: '4px'
278
             borderRadius: '4px'
272
         },
279
         },
273
 
280
 
281
+        borderIndicatorOnTop: {
282
+            zIndex: 11
283
+        },
284
+
274
         activeSpeaker: {
285
         activeSpeaker: {
275
             '& .active-speaker-indicator': {
286
             '& .active-speaker-indicator': {
276
                 boxShadow: `inset 0px 0px 0px 4px ${theme.palette.link01Active} !important`
287
                 boxShadow: `inset 0px 0px 0px 4px ${theme.palette.link01Active} !important`
281
             '& .raised-hand-border': {
292
             '& .raised-hand-border': {
282
                 boxShadow: `inset 0px 0px 0px 2px ${theme.palette.warning02} !important`
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
         this._onTouchMove = this._onTouchMove.bind(this);
369
         this._onTouchMove = this._onTouchMove.bind(this);
340
         this._showPopover = this._showPopover.bind(this);
370
         this._showPopover = this._showPopover.bind(this);
341
         this._hidePopover = this._hidePopover.bind(this);
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
         return className;
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
     _onCanPlay: Object => void;
822
     _onCanPlay: Object => void;
745
 
823
 
746
     /**
824
     /**
798
             _localFlipX,
876
             _localFlipX,
799
             _participant,
877
             _participant,
800
             _videoTrack,
878
             _videoTrack,
801
-            classes
879
+            classes,
880
+            _gifSrc
802
         } = this.props;
881
         } = this.props;
803
         const { id } = _participant || {};
882
         const { id } = _participant || {};
804
         const { isHovered, popoverVisible } = this.state;
883
         const { isHovered, popoverVisible } = this.state;
850
                     }
929
                     }
851
                 ) }
930
                 ) }
852
                 style = { styles.thumbnail }>
931
                 style = { styles.thumbnail }>
853
-                {local
932
+                {!_gifSrc && (local
854
                     ? <span id = 'localVideoWrapper'>{video}</span>
933
                     ? <span id = 'localVideoWrapper'>{video}</span>
855
-                    : video}
934
+                    : video)}
856
                 <div className = { classes.containerBackground } />
935
                 <div className = { classes.containerBackground } />
857
                 <div
936
                 <div
858
                     className = { clsx(classes.indicatorsContainer,
937
                     className = { clsx(classes.indicatorsContainer,
880
                         local = { local }
959
                         local = { local }
881
                         participantId = { id } />
960
                         participantId = { id } />
882
                 </div>
961
                 </div>
883
-                { this._renderAvatar(styles.avatar) }
962
+                {!_gifSrc && this._renderAvatar(styles.avatar) }
884
                 { !local && (
963
                 { !local && (
885
                     <div className = 'presence-label-container'>
964
                     <div className = 'presence-label-container'>
886
                         <PresenceLabel
965
                         <PresenceLabel
889
                     </div>
968
                     </div>
890
                 )}
969
                 )}
891
                 <ThumbnailAudioIndicator _audioTrack = { _audioTrack } />
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
             </span>
980
             </span>
895
         );
981
         );
896
     }
982
     }
1003
     }
1089
     }
1004
     }
1090
     }
1005
 
1091
 
1092
+    const { gifUrl: gifSrc } = getGifForParticipant(state, id);
1093
+    const mode = getGifDisplayMode(state);
1094
+
1006
     return {
1095
     return {
1007
         _audioTrack,
1096
         _audioTrack,
1008
         _currentLayout,
1097
         _currentLayout,
1023
         _raisedHand: hasRaisedHand(participant),
1112
         _raisedHand: hasRaisedHand(participant),
1024
         _videoObjectPosition: getVideoObjectPosition(state, participant?.id),
1113
         _videoObjectPosition: getVideoObjectPosition(state, participant?.id),
1025
         _videoTrack,
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 查看文件

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 查看文件

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 查看文件

1
+export * from './web';

+ 1
- 0
react/features/gifs/components/index.js 查看文件

1
+export * from './_';

+ 223
- 0
react/features/gifs/components/web/GifsMenu.js 查看文件

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 查看文件

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 查看文件

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

+ 9
- 0
react/features/gifs/constants.js 查看文件

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 查看文件

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 查看文件

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 查看文件

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 查看文件

3
 /* eslint-disable react/jsx-no-bind */
3
 /* eslint-disable react/jsx-no-bind */
4
 
4
 
5
 import { withStyles } from '@material-ui/styles';
5
 import { withStyles } from '@material-ui/styles';
6
+import clsx from 'clsx';
6
 import React, { Component } from 'react';
7
 import React, { Component } from 'react';
7
 import { bindActionCreators } from 'redux';
8
 import { bindActionCreators } from 'redux';
8
 
9
 
15
 import { translate } from '../../../base/i18n';
16
 import { translate } from '../../../base/i18n';
16
 import { getLocalParticipant, hasRaisedHand, raiseHand } from '../../../base/participants';
17
 import { getLocalParticipant, hasRaisedHand, raiseHand } from '../../../base/participants';
17
 import { connect } from '../../../base/redux';
18
 import { connect } from '../../../base/redux';
19
+import { GifsMenu, GifsMenuButton } from '../../../gifs/components';
20
+import { isGifEnabled, isGifsMenuOpen } from '../../../gifs/functions';
18
 import { dockToolbox } from '../../../toolbox/actions.web';
21
 import { dockToolbox } from '../../../toolbox/actions.web';
19
 import { addReactionToBuffer } from '../../actions.any';
22
 import { addReactionToBuffer } from '../../actions.any';
20
 import { toggleReactionsMenuVisibility } from '../../actions.web';
23
 import { toggleReactionsMenuVisibility } from '../../actions.web';
29
      */
32
      */
30
     _dockToolbox: Function,
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
      * Whether or not it's a mobile browser.
46
      * Whether or not it's a mobile browser.
34
      */
47
      */
193
      * @inheritdoc
206
      * @inheritdoc
194
      */
207
      */
195
     render() {
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
         return (
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
                 <div className = 'reactions-row'>
216
                 <div className = 'reactions-row'>
201
                     { this._getReactionButtons() }
217
                     { this._getReactionButtons() }
218
+                    {_isGifEnabled && <GifsMenuButton />}
202
                 </div>
219
                 </div>
203
                 {_isMobile && (
220
                 {_isMobile && (
204
                     <div className = 'raise-hand-row'>
221
                     <div className = 'raise-hand-row'>
231
     return {
248
     return {
232
         _localParticipantID: localParticipant.id,
249
         _localParticipantID: localParticipant.id,
233
         _isMobile: isMobileBrowser(),
250
         _isMobile: isMobileBrowser(),
251
+        _isGifEnabled: isGifEnabled(state),
252
+        _isGifMenuVisible: isGifsMenuOpen(state),
234
         _raisedHand: hasRaisedHand(localParticipant)
253
         _raisedHand: hasRaisedHand(localParticipant)
235
     };
254
     };
236
 }
255
 }

+ 7
- 1
react/features/toolbox/components/web/Drawer.js 查看文件

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

+ 27
- 1
react/features/toolbox/components/web/Toolbox.js 查看文件

3
 import { withStyles } from '@material-ui/core/styles';
3
 import { withStyles } from '@material-ui/core/styles';
4
 import clsx from 'clsx';
4
 import clsx from 'clsx';
5
 import React, { Component, Fragment } from 'react';
5
 import React, { Component, Fragment } from 'react';
6
+import { batch } from 'react-redux';
6
 
7
 
7
 import keyboardShortcut from '../../../../../modules/keyboardshortcut/keyboardshortcut';
8
 import keyboardShortcut from '../../../../../modules/keyboardshortcut/keyboardshortcut';
8
 import {
9
 import {
30
 import { EmbedMeetingButton } from '../../../embed-meeting';
31
 import { EmbedMeetingButton } from '../../../embed-meeting';
31
 import { SharedDocumentButton } from '../../../etherpad';
32
 import { SharedDocumentButton } from '../../../etherpad';
32
 import { FeedbackButton } from '../../../feedback';
33
 import { FeedbackButton } from '../../../feedback';
34
+import { setGifMenuVisibility } from '../../../gifs/actions';
35
+import { isGifEnabled } from '../../../gifs/functions';
33
 import { InviteButton } from '../../../invite/components/add-people-dialog';
36
 import { InviteButton } from '../../../invite/components/add-people-dialog';
34
 import { isVpaasMeeting } from '../../../jaas/functions';
37
 import { isVpaasMeeting } from '../../../jaas/functions';
35
 import { KeyboardShortcutsButton } from '../../../keyboard-shortcuts';
38
 import { KeyboardShortcutsButton } from '../../../keyboard-shortcuts';
41
 import { ParticipantsPaneButton } from '../../../participants-pane/components/web';
44
 import { ParticipantsPaneButton } from '../../../participants-pane/components/web';
42
 import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
45
 import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
43
 import { addReactionToBuffer } from '../../../reactions/actions.any';
46
 import { addReactionToBuffer } from '../../../reactions/actions.any';
47
+import { toggleReactionsMenuVisibility } from '../../../reactions/actions.web';
44
 import { ReactionsMenuButton } from '../../../reactions/components';
48
 import { ReactionsMenuButton } from '../../../reactions/components';
45
 import { REACTIONS, REACTIONS_MENU_HEIGHT } from '../../../reactions/constants';
49
 import { REACTIONS, REACTIONS_MENU_HEIGHT } from '../../../reactions/constants';
46
 import { isReactionsEnabled } from '../../../reactions/functions.any';
50
 import { isReactionsEnabled } from '../../../reactions/functions.any';
159
      */
163
      */
160
     _fullScreen: boolean,
164
     _fullScreen: boolean,
161
 
165
 
166
+    /**
167
+     * Whether or not the GIFs feature is enabled.
168
+     */
169
+    _gifsEnabled: boolean,
170
+
162
     /**
171
     /**
163
      * Whether the app has Salesforce integration.
172
      * Whether the app has Salesforce integration.
164
      */
173
      */
334
      * @returns {void}
343
      * @returns {void}
335
      */
344
      */
336
     componentDidMount() {
345
     componentDidMount() {
337
-        const { _toolbarButtons, t, dispatch, _reactionsEnabled } = this.props;
346
+        const { _toolbarButtons, t, dispatch, _reactionsEnabled, _gifsEnabled } = this.props;
338
         const KEYBOARD_SHORTCUTS = [
347
         const KEYBOARD_SHORTCUTS = [
339
             isToolbarButtonEnabled('videoquality', _toolbarButtons) && {
348
             isToolbarButtonEnabled('videoquality', _toolbarButtons) && {
340
                 character: 'A',
349
                 character: 'A',
408
                     shortcut.helpDescription,
417
                     shortcut.helpDescription,
409
                     shortcut.altKey);
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
         _disabled: Boolean(iAmRecorder || iAmSipGateway),
1435
         _disabled: Boolean(iAmRecorder || iAmSipGateway),
1411
         _feedbackConfigured: Boolean(callStatsID),
1436
         _feedbackConfigured: Boolean(callStatsID),
1412
         _fullScreen: fullScreen,
1437
         _fullScreen: fullScreen,
1438
+        _gifsEnabled: isGifEnabled(state),
1413
         _isProfileDisabled: Boolean(disableProfile),
1439
         _isProfileDisabled: Boolean(disableProfile),
1414
         _isIosMobile: isIosMobileBrowser(),
1440
         _isIosMobile: isIosMobileBrowser(),
1415
         _isMobile: isMobileBrowser(),
1441
         _isMobile: isMobileBrowser(),

Loading…
取消
儲存