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
This commit is contained in:
parent
b6d55571ba
commit
190041fc5a
15
config.js
15
config.js
|
@ -1300,6 +1300,21 @@ var config = {
|
||||||
// Specifies whether the chat emoticons are disabled or not
|
// Specifies whether the chat emoticons are disabled or not
|
||||||
// disableChatSmileys: false,
|
// disableChatSmileys: false,
|
||||||
|
|
||||||
|
// Settings for the GIPHY integration.
|
||||||
|
// giphy: {
|
||||||
|
// // Whether the feature is enabled or not.
|
||||||
|
// enabled: false,
|
||||||
|
// // SDK API Key from Giphy.
|
||||||
|
// sdkKey: '',
|
||||||
|
// // Display mode can be one of:
|
||||||
|
// // - tile: show the GIF on the tile of the participant that sent it.
|
||||||
|
// // - chat: show the GIF as a message in chat
|
||||||
|
// // - all: all of the above. This is the default option
|
||||||
|
// displayMode: 'all',
|
||||||
|
// // How long the GIF should be displayed on the tile (in miliseconds).
|
||||||
|
// tileTime: 5000
|
||||||
|
// },
|
||||||
|
|
||||||
// Allow all above example options to include a trailing comma and
|
// Allow all above example options to include a trailing comma and
|
||||||
// prevent fear when commenting out the last value.
|
// prevent fear when commenting out the last value.
|
||||||
makeJsonParserHappy: 'even if last key had a trailing comma'
|
makeJsonParserHappy: 'even if last key had a trailing comma'
|
||||||
|
|
|
@ -7,7 +7,20 @@
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
||||||
|
&.with-gif {
|
||||||
|
width: 328px;
|
||||||
|
|
||||||
|
.reactions-row .toolbox-button:last-of-type {
|
||||||
|
top: 3px;
|
||||||
|
|
||||||
|
& .toolbox-icon.toggled {
|
||||||
|
background-color: #000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.overflow {
|
&.overflow {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
.toolbox-icon {
|
.toolbox-icon {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
|
@ -27,6 +40,10 @@
|
||||||
.toolbox-button {
|
.toolbox-button {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbox-button:last-of-type {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +73,7 @@
|
||||||
.toolbox-button {
|
.toolbox-button {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbox-button:last-of-type {
|
.toolbox-button:last-of-type {
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 284 B |
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
|
@ -421,6 +421,10 @@
|
||||||
"veryBad": "Very Bad",
|
"veryBad": "Very Bad",
|
||||||
"veryGood": "Very Good"
|
"veryGood": "Very Good"
|
||||||
},
|
},
|
||||||
|
"giphy": {
|
||||||
|
"noResults": "No results found :(",
|
||||||
|
"search": "Search GIPHY"
|
||||||
|
},
|
||||||
"helpView": {
|
"helpView": {
|
||||||
"header": "Help center"
|
"header": "Help center"
|
||||||
},
|
},
|
||||||
|
@ -487,6 +491,7 @@
|
||||||
"focusLocal": "Focus on your video",
|
"focusLocal": "Focus on your video",
|
||||||
"focusRemote": "Focus on another person's video",
|
"focusRemote": "Focus on another person's video",
|
||||||
"fullScreen": "View or exit full screen",
|
"fullScreen": "View or exit full screen",
|
||||||
|
"giphyMenu": "Toggle GIPHY menu",
|
||||||
"keyboardShortcuts": "Keyboard shortcuts",
|
"keyboardShortcuts": "Keyboard shortcuts",
|
||||||
"localRecording": "Show or hide local recording controls",
|
"localRecording": "Show or hide local recording controls",
|
||||||
"mute": "Mute or unmute your microphone",
|
"mute": "Mute or unmute your microphone",
|
||||||
|
@ -1007,6 +1012,7 @@
|
||||||
"expand": "Expand",
|
"expand": "Expand",
|
||||||
"feedback": "Leave feedback",
|
"feedback": "Leave feedback",
|
||||||
"fullScreen": "Toggle full screen",
|
"fullScreen": "Toggle full screen",
|
||||||
|
"giphy": "Toggle GIPHY menu",
|
||||||
"grantModerator": "Grant Moderator Rights",
|
"grantModerator": "Grant Moderator Rights",
|
||||||
"hangup": "Leave the meeting",
|
"hangup": "Leave the meeting",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
|
@ -1076,6 +1082,7 @@
|
||||||
"exitFullScreen": "Exit full screen",
|
"exitFullScreen": "Exit full screen",
|
||||||
"exitTileView": "Exit tile view",
|
"exitTileView": "Exit tile view",
|
||||||
"feedback": "Leave feedback",
|
"feedback": "Leave feedback",
|
||||||
|
"giphy": "Toggle GIPHY menu",
|
||||||
"hangup": "Leave the meeting",
|
"hangup": "Leave the meeting",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"invite": "Invite people",
|
"invite": "Invite people",
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -33,6 +33,8 @@
|
||||||
"@atlaskit/theme": "11.0.2",
|
"@atlaskit/theme": "11.0.2",
|
||||||
"@atlaskit/toggle": "12.0.3",
|
"@atlaskit/toggle": "12.0.3",
|
||||||
"@atlaskit/tooltip": "17.1.2",
|
"@atlaskit/tooltip": "17.1.2",
|
||||||
|
"@giphy/js-fetch-api": "4.1.2",
|
||||||
|
"@giphy/react-components": "5.6.0",
|
||||||
"@hapi/bourne": "2.0.0",
|
"@hapi/bourne": "2.0.0",
|
||||||
"@jitsi/js-utils": "2.0.0",
|
"@jitsi/js-utils": "2.0.0",
|
||||||
"@jitsi/logger": "2.0.0",
|
"@jitsi/logger": "2.0.0",
|
||||||
|
|
|
@ -899,3 +899,15 @@ export function createBreakoutRoomsEvent(actionSubject) {
|
||||||
source: 'breakout.rooms'
|
source: 'breakout.rooms'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and event which indicates a GIF was sent.
|
||||||
|
*
|
||||||
|
* @returns {Object} The event in a format suitable for sending via
|
||||||
|
* sendAnalytics.
|
||||||
|
*/
|
||||||
|
export function createGifSentEvent() {
|
||||||
|
return {
|
||||||
|
action: 'gif.sent'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import '../display-name/middleware';
|
||||||
import '../etherpad/middleware';
|
import '../etherpad/middleware';
|
||||||
import '../filmstrip/middleware';
|
import '../filmstrip/middleware';
|
||||||
import '../follow-me/middleware';
|
import '../follow-me/middleware';
|
||||||
|
import '../gifs/middleware';
|
||||||
import '../invite/middleware';
|
import '../invite/middleware';
|
||||||
import '../jaas/middleware';
|
import '../jaas/middleware';
|
||||||
import '../large-video/middleware';
|
import '../large-video/middleware';
|
||||||
|
|
|
@ -33,6 +33,7 @@ import '../dynamic-branding/reducer';
|
||||||
import '../etherpad/reducer';
|
import '../etherpad/reducer';
|
||||||
import '../filmstrip/reducer';
|
import '../filmstrip/reducer';
|
||||||
import '../follow-me/reducer';
|
import '../follow-me/reducer';
|
||||||
|
import '../gifs/reducer';
|
||||||
import '../google-api/reducer';
|
import '../google-api/reducer';
|
||||||
import '../invite/reducer';
|
import '../invite/reducer';
|
||||||
import '../jaas/reducer';
|
import '../jaas/reducer';
|
||||||
|
|
|
@ -161,6 +161,7 @@ export default [
|
||||||
'forceJVB121Ratio',
|
'forceJVB121Ratio',
|
||||||
'forceTurnRelay',
|
'forceTurnRelay',
|
||||||
'gatherStats',
|
'gatherStats',
|
||||||
|
'giphy',
|
||||||
'googleApiApplicationClientID',
|
'googleApiApplicationClientID',
|
||||||
'hiddenPremeetingButtons',
|
'hiddenPremeetingButtons',
|
||||||
'hideConferenceSubject',
|
'hideConferenceSubject',
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import { isMobileBrowser } from '../../../environment/utils';
|
||||||
import { getFieldValue } from '../../../react';
|
import { getFieldValue } from '../../../react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -132,6 +133,9 @@ export default class InputField extends PureComponent<Props, State> {
|
||||||
onKeyDown = { this._onKeyDown }
|
onKeyDown = { this._onKeyDown }
|
||||||
placeholder = { this.props.placeHolder }
|
placeholder = { this.props.placeHolder }
|
||||||
readOnly = { this.props.readOnly }
|
readOnly = { this.props.readOnly }
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
ref = { inputElement => this.props.autoFocus && isMobileBrowser()
|
||||||
|
&& inputElement && inputElement.focus() }
|
||||||
type = { this.props.type }
|
type = { this.props.type }
|
||||||
value = { this.state.value } />
|
value = { this.state.value } />
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { toArray } from 'react-emoji-render';
|
import { toArray } from 'react-emoji-render';
|
||||||
|
|
||||||
|
import GifMessage from '../../../../chat/components/web/GifMessage';
|
||||||
|
import { isGifMessage } from '../../../../gifs/functions';
|
||||||
|
|
||||||
import Linkify from './Linkify';
|
import Linkify from './Linkify';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -44,7 +47,16 @@ class Message extends Component<Props> {
|
||||||
|
|
||||||
const content = [];
|
const content = [];
|
||||||
|
|
||||||
|
// check if the message is a GIF
|
||||||
|
if (isGifMessage(text)) {
|
||||||
|
const url = text.substring(4, text.length - 1);
|
||||||
|
|
||||||
|
content.push(<GifMessage
|
||||||
|
key = { url }
|
||||||
|
url = { url } />);
|
||||||
|
} else {
|
||||||
for (const token of tokens) {
|
for (const token of tokens) {
|
||||||
|
|
||||||
if (token.includes('://')) {
|
if (token.includes('://')) {
|
||||||
|
|
||||||
// Bypass the emojification when urls are involved
|
// Bypass the emojification when urls are involved
|
||||||
|
@ -55,6 +67,7 @@ class Message extends Component<Props> {
|
||||||
|
|
||||||
content.push(' ');
|
content.push(' ');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
content.forEach(token => {
|
content.forEach(token => {
|
||||||
if (typeof token === 'string' && token !== ' ') {
|
if (typeof token === 'string' && token !== ' ') {
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL of the GIF.
|
||||||
|
*/
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() => {
|
||||||
|
return {
|
||||||
|
container: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
maxHeight: '150px',
|
||||||
|
|
||||||
|
'& img': {
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '100%',
|
||||||
|
objectFit: 'contain',
|
||||||
|
flexGrow: '1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const GifMessage = ({ url }: Props) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return (<div className = { styles.container }>
|
||||||
|
<img
|
||||||
|
alt = { url }
|
||||||
|
src = { url } />
|
||||||
|
</div>);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GifMessage;
|
|
@ -18,6 +18,9 @@ import {
|
||||||
} from '../base/participants';
|
} from '../base/participants';
|
||||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||||
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
||||||
|
import { addGif } from '../gifs/actions';
|
||||||
|
import { GIF_PREFIX } from '../gifs/constants';
|
||||||
|
import { getGifDisplayMode, isGifMessage } from '../gifs/functions';
|
||||||
import { NOTIFICATION_TIMEOUT_TYPE, showMessageNotification } from '../notifications';
|
import { NOTIFICATION_TIMEOUT_TYPE, showMessageNotification } from '../notifications';
|
||||||
import { resetNbUnreadPollsMessages } from '../polls/actions';
|
import { resetNbUnreadPollsMessages } from '../polls/actions';
|
||||||
import { ADD_REACTION_MESSAGE } from '../reactions/actionTypes';
|
import { ADD_REACTION_MESSAGE } from '../reactions/actionTypes';
|
||||||
|
@ -226,25 +229,21 @@ function _addChatMsgListener(conference, store) {
|
||||||
conference.on(
|
conference.on(
|
||||||
JitsiConferenceEvents.MESSAGE_RECEIVED,
|
JitsiConferenceEvents.MESSAGE_RECEIVED,
|
||||||
(id, message, timestamp) => {
|
(id, message, timestamp) => {
|
||||||
_handleReceivedMessage(store, {
|
_onConferenceMessageReceived(store, { id,
|
||||||
id,
|
|
||||||
message,
|
message,
|
||||||
privateMessage: false,
|
timestamp,
|
||||||
lobbyChat: false,
|
privateMessage: false });
|
||||||
timestamp
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
conference.on(
|
conference.on(
|
||||||
JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
|
JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
|
||||||
(id, message, timestamp) => {
|
(id, message, timestamp) => {
|
||||||
_handleReceivedMessage(store, {
|
_onConferenceMessageReceived(store, {
|
||||||
id,
|
id,
|
||||||
message,
|
message,
|
||||||
privateMessage: true,
|
timestamp,
|
||||||
lobbyChat: false,
|
privateMessage: true
|
||||||
timestamp
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -283,6 +282,45 @@ function _addChatMsgListener(conference, store) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a received message.
|
||||||
|
*
|
||||||
|
* @param {Object} store - Redux store.
|
||||||
|
* @param {Object} message - The message object.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function _onConferenceMessageReceived(store, { id, message, timestamp, privateMessage }) {
|
||||||
|
const isGif = isGifMessage(message);
|
||||||
|
|
||||||
|
if (isGif) {
|
||||||
|
_handleGifMessageReceived(store, id, message);
|
||||||
|
if (getGifDisplayMode(store.getState()) === 'tile') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_handleReceivedMessage(store, {
|
||||||
|
id,
|
||||||
|
message,
|
||||||
|
privateMessage,
|
||||||
|
lobbyChat: false,
|
||||||
|
timestamp
|
||||||
|
}, true, isGif);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a received gif message.
|
||||||
|
*
|
||||||
|
* @param {Object} store - Redux store.
|
||||||
|
* @param {string} id - Id of the participant that sent the message.
|
||||||
|
* @param {string} message - The message sent.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function _handleGifMessageReceived(store, id, message) {
|
||||||
|
const url = message.substring(GIF_PREFIX.length, message.length - 1);
|
||||||
|
|
||||||
|
store.dispatch(addGif(id, url));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles a chat error received from the xmpp server.
|
* Handles a chat error received from the xmpp server.
|
||||||
*
|
*
|
||||||
|
|
|
@ -24,6 +24,8 @@ import {
|
||||||
updateLastTrackVideoMediaEvent
|
updateLastTrackVideoMediaEvent
|
||||||
} from '../../../base/tracks';
|
} from '../../../base/tracks';
|
||||||
import { getVideoObjectPosition } from '../../../face-centering/functions';
|
import { getVideoObjectPosition } from '../../../face-centering/functions';
|
||||||
|
import { hideGif, showGif } from '../../../gifs/actions';
|
||||||
|
import { getGifDisplayMode, getGifForParticipant } from '../../../gifs/functions';
|
||||||
import { PresenceLabel } from '../../../presence-status';
|
import { PresenceLabel } from '../../../presence-status';
|
||||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||||
import {
|
import {
|
||||||
|
@ -96,6 +98,11 @@ export type Props = {|
|
||||||
*/
|
*/
|
||||||
_disableTileEnlargement: boolean,
|
_disableTileEnlargement: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL of GIF sent by this participant, null if there's none.
|
||||||
|
*/
|
||||||
|
_gifSrc ?: string,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The height of the Thumbnail.
|
* The height of the Thumbnail.
|
||||||
*/
|
*/
|
||||||
|
@ -181,16 +188,16 @@ export type Props = {|
|
||||||
*/
|
*/
|
||||||
_width: number,
|
_width: number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object containing CSS classes.
|
||||||
|
*/
|
||||||
|
classes: Object,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The redux dispatch function.
|
* The redux dispatch function.
|
||||||
*/
|
*/
|
||||||
dispatch: Function,
|
dispatch: Function,
|
||||||
|
|
||||||
/**
|
|
||||||
* An object containing the CSS classes.
|
|
||||||
*/
|
|
||||||
classes: Object,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The horizontal offset in px for the thumbnail. Used to center the thumbnails from the last row in tile view.
|
* 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 => {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
zIndex: '9',
|
zIndex: 9,
|
||||||
borderRadius: '4px'
|
borderRadius: '4px'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
borderIndicatorOnTop: {
|
||||||
|
zIndex: 11
|
||||||
|
},
|
||||||
|
|
||||||
activeSpeaker: {
|
activeSpeaker: {
|
||||||
'& .active-speaker-indicator': {
|
'& .active-speaker-indicator': {
|
||||||
boxShadow: `inset 0px 0px 0px 4px ${theme.palette.link01Active} !important`
|
boxShadow: `inset 0px 0px 0px 4px ${theme.palette.link01Active} !important`
|
||||||
|
@ -281,6 +292,25 @@ const defaultStyles = theme => {
|
||||||
'& .raised-hand-border': {
|
'& .raised-hand-border': {
|
||||||
boxShadow: `inset 0px 0px 0px 2px ${theme.palette.warning02} !important`
|
boxShadow: `inset 0px 0px 0px 2px ${theme.palette.warning02} !important`
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
gif: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
zIndex: 11,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundColor: theme.palette.ui02,
|
||||||
|
|
||||||
|
'& img': {
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '100%',
|
||||||
|
objectFit: 'contain',
|
||||||
|
flexGrow: '1'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -339,6 +369,8 @@ class Thumbnail extends Component<Props, State> {
|
||||||
this._onTouchMove = this._onTouchMove.bind(this);
|
this._onTouchMove = this._onTouchMove.bind(this);
|
||||||
this._showPopover = this._showPopover.bind(this);
|
this._showPopover = this._showPopover.bind(this);
|
||||||
this._hidePopover = this._hidePopover.bind(this);
|
this._hidePopover = this._hidePopover.bind(this);
|
||||||
|
this._onGifMouseEnter = this._onGifMouseEnter.bind(this);
|
||||||
|
this._onGifMouseLeave = this._onGifMouseLeave.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -741,6 +773,52 @@ class Thumbnail extends Component<Props, State> {
|
||||||
return className;
|
return className;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onGifMouseEnter: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep showing the GIF for the current participant.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onGifMouseEnter() {
|
||||||
|
const { dispatch, _participant: { id } } = this.props;
|
||||||
|
|
||||||
|
dispatch(showGif(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
_onGifMouseLeave: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep showing the GIF for the current participant.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onGifMouseLeave() {
|
||||||
|
const { dispatch, _participant: { id } } = this.props;
|
||||||
|
|
||||||
|
dispatch(hideGif(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders GIF.
|
||||||
|
*
|
||||||
|
* @returns {Component}
|
||||||
|
*/
|
||||||
|
_renderGif() {
|
||||||
|
const { _gifSrc, classes } = this.props;
|
||||||
|
|
||||||
|
return _gifSrc && (
|
||||||
|
<div
|
||||||
|
className = { classes.gif }
|
||||||
|
onMouseEnter = { this._onGifMouseEnter }
|
||||||
|
onMouseLeave = { this._onGifMouseLeave }>
|
||||||
|
<img
|
||||||
|
alt = 'GIF'
|
||||||
|
src = { _gifSrc } />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_onCanPlay: Object => void;
|
_onCanPlay: Object => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -798,7 +876,8 @@ class Thumbnail extends Component<Props, State> {
|
||||||
_localFlipX,
|
_localFlipX,
|
||||||
_participant,
|
_participant,
|
||||||
_videoTrack,
|
_videoTrack,
|
||||||
classes
|
classes,
|
||||||
|
_gifSrc
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { id } = _participant || {};
|
const { id } = _participant || {};
|
||||||
const { isHovered, popoverVisible } = this.state;
|
const { isHovered, popoverVisible } = this.state;
|
||||||
|
@ -850,9 +929,9 @@ class Thumbnail extends Component<Props, State> {
|
||||||
}
|
}
|
||||||
) }
|
) }
|
||||||
style = { styles.thumbnail }>
|
style = { styles.thumbnail }>
|
||||||
{local
|
{!_gifSrc && (local
|
||||||
? <span id = 'localVideoWrapper'>{video}</span>
|
? <span id = 'localVideoWrapper'>{video}</span>
|
||||||
: video}
|
: video)}
|
||||||
<div className = { classes.containerBackground } />
|
<div className = { classes.containerBackground } />
|
||||||
<div
|
<div
|
||||||
className = { clsx(classes.indicatorsContainer,
|
className = { clsx(classes.indicatorsContainer,
|
||||||
|
@ -880,7 +959,7 @@ class Thumbnail extends Component<Props, State> {
|
||||||
local = { local }
|
local = { local }
|
||||||
participantId = { id } />
|
participantId = { id } />
|
||||||
</div>
|
</div>
|
||||||
{ this._renderAvatar(styles.avatar) }
|
{!_gifSrc && this._renderAvatar(styles.avatar) }
|
||||||
{ !local && (
|
{ !local && (
|
||||||
<div className = 'presence-label-container'>
|
<div className = 'presence-label-container'>
|
||||||
<PresenceLabel
|
<PresenceLabel
|
||||||
|
@ -889,8 +968,15 @@ class Thumbnail extends Component<Props, State> {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ThumbnailAudioIndicator _audioTrack = { _audioTrack } />
|
<ThumbnailAudioIndicator _audioTrack = { _audioTrack } />
|
||||||
<div className = { clsx(classes.borderIndicator, 'raised-hand-border') } />
|
{this._renderGif()}
|
||||||
<div className = { clsx(classes.borderIndicator, 'active-speaker-indicator') } />
|
<div
|
||||||
|
className = { clsx(classes.borderIndicator,
|
||||||
|
_gifSrc && classes.borderIndicatorOnTop,
|
||||||
|
'raised-hand-border') } />
|
||||||
|
<div
|
||||||
|
className = { clsx(classes.borderIndicator,
|
||||||
|
_gifSrc && classes.borderIndicatorOnTop,
|
||||||
|
'active-speaker-indicator') } />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1003,6 +1089,9 @@ function _mapStateToProps(state, ownProps): Object {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { gifUrl: gifSrc } = getGifForParticipant(state, id);
|
||||||
|
const mode = getGifDisplayMode(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_audioTrack,
|
_audioTrack,
|
||||||
_currentLayout,
|
_currentLayout,
|
||||||
|
@ -1023,7 +1112,8 @@ function _mapStateToProps(state, ownProps): Object {
|
||||||
_raisedHand: hasRaisedHand(participant),
|
_raisedHand: hasRaisedHand(participant),
|
||||||
_videoObjectPosition: getVideoObjectPosition(state, participant?.id),
|
_videoObjectPosition: getVideoObjectPosition(state, participant?.id),
|
||||||
_videoTrack,
|
_videoTrack,
|
||||||
...size
|
...size,
|
||||||
|
_gifSrc: mode === 'chat' ? null : gifSrc
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
/**
|
||||||
|
* Adds a gif for a given participant.
|
||||||
|
* {{
|
||||||
|
* type: ADD_GIF_FOR_PARTICIPANT,
|
||||||
|
* participantId: string,
|
||||||
|
* gifUrl: string,
|
||||||
|
* timeoutID: number
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export const ADD_GIF_FOR_PARTICIPANT = 'ADD_GIF_FOR_PARTICIPANT';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set timeout to hide a gif for a given participant.
|
||||||
|
* {{
|
||||||
|
* type: HIDE_GIF_FOR_PARTICIPANT,
|
||||||
|
* participantId: string
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export const HIDE_GIF_FOR_PARTICIPANT = 'HIDE_GIF_FOR_PARTICIPANT';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a gif for a given participant.
|
||||||
|
* {{
|
||||||
|
* type: REMOVE_GIF_FOR_PARTICIPANT,
|
||||||
|
* participantId: string
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export const REMOVE_GIF_FOR_PARTICIPANT = 'REMOVE_GIF_FOR_PARTICIPANT';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set gif menu drawer visibility.
|
||||||
|
* {{
|
||||||
|
* type: SET_GIF_DRAWER_VISIBILITY,
|
||||||
|
* visible: boolean
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export const SET_GIF_DRAWER_VISIBILITY = 'SET_GIF_DRAWER_VISIBILITY';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set gif menu visibility.
|
||||||
|
* {{
|
||||||
|
* type: SET_GIF_MENU_VISIBILITY,
|
||||||
|
* visible: boolean
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export const SET_GIF_MENU_VISIBILITY = 'SET_GIF_MENU_VISIBILITY';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep showing a gif for a given participant.
|
||||||
|
* {{
|
||||||
|
* type: SHOW_GIF_FOR_PARTICIPANT,
|
||||||
|
* participantId: string
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export const SHOW_GIF_FOR_PARTICIPANT = 'SHOW_GIF_FOR_PARTICIPANT';
|
|
@ -0,0 +1,88 @@
|
||||||
|
import {
|
||||||
|
ADD_GIF_FOR_PARTICIPANT,
|
||||||
|
HIDE_GIF_FOR_PARTICIPANT,
|
||||||
|
REMOVE_GIF_FOR_PARTICIPANT,
|
||||||
|
SET_GIF_DRAWER_VISIBILITY,
|
||||||
|
SET_GIF_MENU_VISIBILITY,
|
||||||
|
SHOW_GIF_FOR_PARTICIPANT
|
||||||
|
} from './actionTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a GIF for a given participant.
|
||||||
|
*
|
||||||
|
* @param {string} participantId - The id of the participant that sent the GIF.
|
||||||
|
* @param {string} gifUrl - The URL of the GIF.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
export function addGif(participantId, gifUrl) {
|
||||||
|
return {
|
||||||
|
type: ADD_GIF_FOR_PARTICIPANT,
|
||||||
|
participantId,
|
||||||
|
gifUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the GIF of the given participant.
|
||||||
|
*
|
||||||
|
* @param {string} participantId - The Id of the participant for whom to remove the GIF.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
export function removeGif(participantId) {
|
||||||
|
return {
|
||||||
|
type: REMOVE_GIF_FOR_PARTICIPANT,
|
||||||
|
participantId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep showing the GIF of the given participant.
|
||||||
|
*
|
||||||
|
* @param {string} participantId - The Id of the participant for whom to show the GIF.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
export function showGif(participantId) {
|
||||||
|
return {
|
||||||
|
type: SHOW_GIF_FOR_PARTICIPANT,
|
||||||
|
participantId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set timeout to hide the GIF of the given participant.
|
||||||
|
*
|
||||||
|
* @param {string} participantId - The Id of the participant for whom to show the GIF.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
export function hideGif(participantId) {
|
||||||
|
return {
|
||||||
|
type: HIDE_GIF_FOR_PARTICIPANT,
|
||||||
|
participantId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set visibility of the GIF drawer.
|
||||||
|
*
|
||||||
|
* @param {boolean} visible - Whether or not it should be visible.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
export function setGifDrawerVisibility(visible) {
|
||||||
|
return {
|
||||||
|
type: SET_GIF_DRAWER_VISIBILITY,
|
||||||
|
visible
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set visibility of the GIF menu.
|
||||||
|
*
|
||||||
|
* @param {boolean} visible - Whether or not it should be visible.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
export function setGifMenuVisibility(visible) {
|
||||||
|
return {
|
||||||
|
type: SET_GIF_MENU_VISIBILITY,
|
||||||
|
visible
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './web';
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './_';
|
|
@ -0,0 +1,223 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { GiphyFetch } from '@giphy/js-fetch-api';
|
||||||
|
import { Grid } from '@giphy/react-components';
|
||||||
|
import { makeStyles } from '@material-ui/core';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { batch, useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import { createGifSentEvent, sendAnalytics } from '../../../analytics';
|
||||||
|
import InputField from '../../../base/premeeting/components/web/InputField';
|
||||||
|
import BaseTheme from '../../../base/ui/components/BaseTheme';
|
||||||
|
import { sendMessage } from '../../../chat/actions.any';
|
||||||
|
import { SCROLL_SIZE } from '../../../filmstrip';
|
||||||
|
import { toggleReactionsMenuVisibility } from '../../../reactions/actions.web';
|
||||||
|
import { setOverflowMenuVisible } from '../../../toolbox/actions.web';
|
||||||
|
import { Drawer, JitsiPortal } from '../../../toolbox/components/web';
|
||||||
|
import { showOverflowDrawer } from '../../../toolbox/functions.web';
|
||||||
|
import { setGifDrawerVisibility } from '../../actions';
|
||||||
|
import { formatGifUrlMessage, getGifAPIKey, getGifUrl } from '../../functions';
|
||||||
|
|
||||||
|
const OVERFLOW_DRAWER_PADDING = BaseTheme.spacing(3);
|
||||||
|
|
||||||
|
const useStyles = makeStyles(theme => {
|
||||||
|
return {
|
||||||
|
gifsMenu: {
|
||||||
|
width: '100%',
|
||||||
|
marginBottom: `${theme.spacing(2)}px`,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
|
||||||
|
'& div:focus': {
|
||||||
|
border: '1px solid red !important',
|
||||||
|
boxSizing: 'border-box'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
searchField: {
|
||||||
|
backgroundColor: theme.palette.field01,
|
||||||
|
borderRadius: `${theme.shape.borderRadius}px`,
|
||||||
|
border: 'none',
|
||||||
|
outline: 0,
|
||||||
|
...theme.typography.bodyShortRegular,
|
||||||
|
lineHeight: `${theme.typography.bodyShortRegular.lineHeight}px`,
|
||||||
|
color: theme.palette.text01,
|
||||||
|
padding: `${theme.spacing(2)}px ${theme.spacing(3)}px`,
|
||||||
|
width: '100%',
|
||||||
|
marginBottom: `${theme.spacing(3)}px`
|
||||||
|
},
|
||||||
|
|
||||||
|
gifContainer: {
|
||||||
|
height: '245px',
|
||||||
|
overflowY: 'auto'
|
||||||
|
},
|
||||||
|
|
||||||
|
logoContainer: {
|
||||||
|
width: `calc(100% - ${SCROLL_SIZE}px)`,
|
||||||
|
backgroundColor: '#121119',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: '#fff',
|
||||||
|
marginTop: `${theme.spacing(1)}px`
|
||||||
|
},
|
||||||
|
|
||||||
|
overflowMenu: {
|
||||||
|
padding: `${theme.spacing(3)}px`,
|
||||||
|
width: '100%',
|
||||||
|
boxSizing: 'border-box'
|
||||||
|
},
|
||||||
|
|
||||||
|
gifContainerOverflow: {
|
||||||
|
flexGrow: 1
|
||||||
|
},
|
||||||
|
|
||||||
|
drawer: {
|
||||||
|
display: 'flex',
|
||||||
|
height: '100%'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gifs menu.
|
||||||
|
*
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
function GifsMenu() {
|
||||||
|
const API_KEY = useSelector(getGifAPIKey);
|
||||||
|
const giphyFetch = new GiphyFetch(API_KEY);
|
||||||
|
const [ searchKey, setSearchKey ] = useState();
|
||||||
|
const styles = useStyles();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const overflowDrawer = useSelector(showOverflowDrawer);
|
||||||
|
const { clientWidth } = useSelector(state => state['features/base/responsive-ui']);
|
||||||
|
|
||||||
|
const fetchGifs = useCallback(async (offset = 0) => {
|
||||||
|
const options = {
|
||||||
|
rating: 'pg-13',
|
||||||
|
limit: 20,
|
||||||
|
offset
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!searchKey) {
|
||||||
|
return await giphyFetch.trending(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await giphyFetch.search(searchKey, options);
|
||||||
|
}, [ searchKey ]);
|
||||||
|
|
||||||
|
const onDrawerClose = useCallback(() => {
|
||||||
|
dispatch(setGifDrawerVisibility(false));
|
||||||
|
dispatch(setOverflowMenuVisible(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleGifClick = useCallback((gif, e) => {
|
||||||
|
e?.stopPropagation();
|
||||||
|
const url = getGifUrl(gif);
|
||||||
|
|
||||||
|
sendAnalytics(createGifSentEvent());
|
||||||
|
batch(() => {
|
||||||
|
dispatch(sendMessage(formatGifUrlMessage(url), true));
|
||||||
|
dispatch(toggleReactionsMenuVisibility());
|
||||||
|
overflowDrawer && onDrawerClose();
|
||||||
|
});
|
||||||
|
}, [ dispatch, overflowDrawer ]);
|
||||||
|
|
||||||
|
const handleGifKeyPress = useCallback((gif, e) => {
|
||||||
|
if (e.nativeEvent.keyCode === 13) {
|
||||||
|
handleGifClick(gif, null);
|
||||||
|
}
|
||||||
|
}, [ handleGifClick ]);
|
||||||
|
|
||||||
|
const handleSearchKeyChange = useCallback(value => {
|
||||||
|
setSearchKey(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(e => {
|
||||||
|
if (e.keyCode === 38) { // up arrow
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// if the first gif is focused move focus to the input
|
||||||
|
if (document.activeElement.previousElementSibling === null) {
|
||||||
|
document.querySelector('.gif-input').focus();
|
||||||
|
} else {
|
||||||
|
document.activeElement.previousElementSibling.focus();
|
||||||
|
}
|
||||||
|
} else if (e.keyCode === 40) { // down arrow
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// if the input is focused move focus to the first gif
|
||||||
|
if (document.activeElement.classList.contains('gif-input')) {
|
||||||
|
document.querySelector('.giphy-gif').focus();
|
||||||
|
} else {
|
||||||
|
document.activeElement.nextElementSibling.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// For some reason, the Grid component does not do an initial call on mobile.
|
||||||
|
// This fixes that.
|
||||||
|
useEffect(() => setSearchKey(''), []);
|
||||||
|
|
||||||
|
const gifMenu = (
|
||||||
|
<div
|
||||||
|
className = { clsx(styles.gifsMenu,
|
||||||
|
overflowDrawer && styles.overflowMenu
|
||||||
|
) }>
|
||||||
|
<InputField
|
||||||
|
autoFocus = { true }
|
||||||
|
className = { clsx(styles.searchField, 'gif-input') }
|
||||||
|
onChange = { handleSearchKeyChange }
|
||||||
|
placeHolder = { t('giphy.search') }
|
||||||
|
testId = 'gifSearch.key'
|
||||||
|
type = 'text' />
|
||||||
|
<div
|
||||||
|
className = { clsx(styles.gifContainer,
|
||||||
|
overflowDrawer && styles.gifContainerOverflow) }>
|
||||||
|
<Grid
|
||||||
|
columns = { 2 }
|
||||||
|
fetchGifs = { fetchGifs }
|
||||||
|
gutter = { 6 }
|
||||||
|
hideAttribution = { true }
|
||||||
|
key = { searchKey }
|
||||||
|
noLink = { true }
|
||||||
|
noResultsMessage = { t('giphy.noResults') }
|
||||||
|
onGifClick = { handleGifClick }
|
||||||
|
onGifKeyPress = { handleGifKeyPress }
|
||||||
|
width = { overflowDrawer
|
||||||
|
? clientWidth - (2 * OVERFLOW_DRAWER_PADDING) - SCROLL_SIZE
|
||||||
|
: 320
|
||||||
|
} />
|
||||||
|
</div>
|
||||||
|
<div className = { styles.logoContainer }>
|
||||||
|
<span>Powered by</span>
|
||||||
|
<img
|
||||||
|
alt = 'GIPHY Logo'
|
||||||
|
src = 'images/GIPHY_logo.png' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return overflowDrawer ? (
|
||||||
|
<JitsiPortal>
|
||||||
|
<Drawer
|
||||||
|
className = { styles.drawer }
|
||||||
|
isOpen = { true }
|
||||||
|
onClose = { onDrawerClose }>
|
||||||
|
{gifMenu}
|
||||||
|
</Drawer>
|
||||||
|
</JitsiPortal>
|
||||||
|
) : gifMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GifsMenu;
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import ReactionButton from '../../../reactions/components/web/ReactionButton';
|
||||||
|
import { showOverflowDrawer } from '../../../toolbox/functions.web';
|
||||||
|
import { setGifDrawerVisibility, setGifMenuVisibility } from '../../actions';
|
||||||
|
import { isGifsMenuOpen } from '../../functions';
|
||||||
|
|
||||||
|
const GifsMenuButton = () => {
|
||||||
|
const menuOpen = useSelector(isGifsMenuOpen);
|
||||||
|
const overflowDrawer = useSelector(showOverflowDrawer);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const icon = (
|
||||||
|
<img
|
||||||
|
alt = 'GIPHY Logo'
|
||||||
|
height = { 24 }
|
||||||
|
src = 'images/GIPHY_icon.png' />
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() =>
|
||||||
|
dispatch(
|
||||||
|
overflowDrawer
|
||||||
|
? setGifDrawerVisibility(!menuOpen)
|
||||||
|
: setGifMenuVisibility(!menuOpen)
|
||||||
|
)
|
||||||
|
, [ menuOpen, overflowDrawer ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactionButton
|
||||||
|
accessibilityLabel = { t('toolbar.accessibilityLabel.giphy') }
|
||||||
|
icon = { icon }
|
||||||
|
key = 'gif'
|
||||||
|
onClick = { handleClick }
|
||||||
|
toggled = { true }
|
||||||
|
tooltip = { t('toolbar.accessibilityLabel.giphy') } />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GifsMenuButton;
|
|
@ -0,0 +1,4 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
export { default as GifsMenuButton } from './GifsMenuButton';
|
||||||
|
export { default as GifsMenu } from './GifsMenu';
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* The default time that GIFs will be displayed on the tile.
|
||||||
|
*/
|
||||||
|
export const GIF_DEFAULT_TIMEOUT = 5000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The prefix for formatted GIF messages.
|
||||||
|
*/
|
||||||
|
export const GIF_PREFIX = 'gif[';
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { showOverflowDrawer } from '../toolbox/functions.web';
|
||||||
|
|
||||||
|
import { GIF_PREFIX } from './constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the URL of the GIF for the given participant or null if there's none.
|
||||||
|
*
|
||||||
|
* @param {Object} state - Redux state.
|
||||||
|
* @param {string} participantId - Id of the participant for which to remove the GIF.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
export function getGifForParticipant(state, participantId) {
|
||||||
|
return state['features/gifs'].gifList.get(participantId) || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the message is a GIF message.
|
||||||
|
*
|
||||||
|
* @param {string} message - Message to check.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isGifMessage(message) {
|
||||||
|
return message.trim().startsWith(GIF_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the visibility state of the gifs menu.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The state of the application.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isGifsMenuOpen(state) {
|
||||||
|
const overflowDrawer = showOverflowDrawer(state);
|
||||||
|
const { drawerVisible, menuOpen } = state['features/gifs'];
|
||||||
|
|
||||||
|
return overflowDrawer ? drawerVisible : menuOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the url of the gif selected in the gifs menu.
|
||||||
|
*
|
||||||
|
* @param {Object} gif - The gif data.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function getGifUrl(gif) {
|
||||||
|
const embedUrl = gif?.embed_url || '';
|
||||||
|
const idx = embedUrl.lastIndexOf('/');
|
||||||
|
const id = embedUrl.substr(idx + 1);
|
||||||
|
|
||||||
|
return `https://i.giphy.com/media/${id}/giphy.webp`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the gif message.
|
||||||
|
*
|
||||||
|
* @param {string} url - GIF url.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function formatGifUrlMessage(url) {
|
||||||
|
return `${GIF_PREFIX}${url}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Giphy API Key from config.
|
||||||
|
*
|
||||||
|
* @param {Object} state - Redux state.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getGifAPIKey(state) {
|
||||||
|
return state['features/base/config']?.giphy?.sdkKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not the feature is enabled.
|
||||||
|
*
|
||||||
|
* @param {Object} state - Redux state.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isGifEnabled(state) {
|
||||||
|
const { disableThirdPartyRequests } = state['features/base/config'];
|
||||||
|
const { giphy } = state['features/base/config'];
|
||||||
|
|
||||||
|
return !disableThirdPartyRequests && giphy?.enabled && Boolean(giphy?.sdkKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the GIF display mode.
|
||||||
|
*
|
||||||
|
* @param {Object} state - Redux state.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getGifDisplayMode(state) {
|
||||||
|
const { giphy } = state['features/base/config'];
|
||||||
|
|
||||||
|
return giphy?.displayMode || 'all';
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { MiddlewareRegistry } from '../base/redux';
|
||||||
|
|
||||||
|
import { ADD_GIF_FOR_PARTICIPANT, HIDE_GIF_FOR_PARTICIPANT, SHOW_GIF_FOR_PARTICIPANT } from './actionTypes';
|
||||||
|
import { removeGif } from './actions';
|
||||||
|
import { GIF_DEFAULT_TIMEOUT } from './constants';
|
||||||
|
import { getGifForParticipant } from './functions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware which intercepts Gifs actions to handle changes to the
|
||||||
|
* visibility timeout of the Gifs.
|
||||||
|
*
|
||||||
|
* @param {Store} store - The redux store.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
MiddlewareRegistry.register(store => next => action => {
|
||||||
|
const { dispatch, getState } = store;
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case ADD_GIF_FOR_PARTICIPANT: {
|
||||||
|
const id = action.participantId;
|
||||||
|
const { giphy } = state['features/base/config'];
|
||||||
|
|
||||||
|
_clearGifTimeout(state, id);
|
||||||
|
const timeoutID = setTimeout(() => dispatch(removeGif(id)), giphy?.tileTime || GIF_DEFAULT_TIMEOUT);
|
||||||
|
|
||||||
|
action.timeoutID = timeoutID;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SHOW_GIF_FOR_PARTICIPANT: {
|
||||||
|
const id = action.participantId;
|
||||||
|
|
||||||
|
_clearGifTimeout(state, id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case HIDE_GIF_FOR_PARTICIPANT: {
|
||||||
|
const { giphy } = state['features/base/config'];
|
||||||
|
const id = action.participantId;
|
||||||
|
const timeoutID = setTimeout(() => dispatch(removeGif(id)), giphy?.tileTime || GIF_DEFAULT_TIMEOUT);
|
||||||
|
|
||||||
|
action.timeoutID = timeoutID;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(action);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears GIF timeout.
|
||||||
|
*
|
||||||
|
* @param {Object} state - Redux state.
|
||||||
|
* @param {string} id - Id of the participant for whom to clear the timeout.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function _clearGifTimeout(state, id) {
|
||||||
|
const gif = getGifForParticipant(state, id);
|
||||||
|
|
||||||
|
clearTimeout(gif?.timeoutID);
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
|
||||||
|
import { ReducerRegistry } from '../base/redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ADD_GIF_FOR_PARTICIPANT,
|
||||||
|
HIDE_GIF_FOR_PARTICIPANT,
|
||||||
|
REMOVE_GIF_FOR_PARTICIPANT,
|
||||||
|
SET_GIF_DRAWER_VISIBILITY,
|
||||||
|
SET_GIF_MENU_VISIBILITY
|
||||||
|
} from './actionTypes';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
drawerVisible: false,
|
||||||
|
gifList: new Map(),
|
||||||
|
menuOpen: false
|
||||||
|
};
|
||||||
|
|
||||||
|
ReducerRegistry.register(
|
||||||
|
'features/gifs',
|
||||||
|
(state = initialState, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ADD_GIF_FOR_PARTICIPANT: {
|
||||||
|
const newList = state.gifList;
|
||||||
|
|
||||||
|
newList.set(action.participantId, {
|
||||||
|
gifUrl: action.gifUrl,
|
||||||
|
timeoutID: action.timeoutID
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
gifList: newList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case REMOVE_GIF_FOR_PARTICIPANT: {
|
||||||
|
const newList = state.gifList;
|
||||||
|
|
||||||
|
newList.delete(action.participantId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
gifList: newList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case HIDE_GIF_FOR_PARTICIPANT: {
|
||||||
|
const newList = state.gifList;
|
||||||
|
const gif = state.gifList.get(action.participantId);
|
||||||
|
|
||||||
|
newList.set(action.participantId, {
|
||||||
|
gifUrl: gif.gifUrl,
|
||||||
|
timeoutID: action.timeoutID
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
gifList: newList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case SET_GIF_DRAWER_VISIBILITY:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
drawerVisible: action.visible
|
||||||
|
};
|
||||||
|
case SET_GIF_MENU_VISIBILITY:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
menuOpen: action.visible
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
/* eslint-disable react/jsx-no-bind */
|
/* eslint-disable react/jsx-no-bind */
|
||||||
|
|
||||||
import { withStyles } from '@material-ui/styles';
|
import { withStyles } from '@material-ui/styles';
|
||||||
|
import clsx from 'clsx';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
|
|
||||||
|
@ -15,6 +16,8 @@ import { isMobileBrowser } from '../../../base/environment/utils';
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
import { getLocalParticipant, hasRaisedHand, raiseHand } from '../../../base/participants';
|
import { getLocalParticipant, hasRaisedHand, raiseHand } from '../../../base/participants';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
|
import { GifsMenu, GifsMenuButton } from '../../../gifs/components';
|
||||||
|
import { isGifEnabled, isGifsMenuOpen } from '../../../gifs/functions';
|
||||||
import { dockToolbox } from '../../../toolbox/actions.web';
|
import { dockToolbox } from '../../../toolbox/actions.web';
|
||||||
import { addReactionToBuffer } from '../../actions.any';
|
import { addReactionToBuffer } from '../../actions.any';
|
||||||
import { toggleReactionsMenuVisibility } from '../../actions.web';
|
import { toggleReactionsMenuVisibility } from '../../actions.web';
|
||||||
|
@ -29,6 +32,16 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
_dockToolbox: Function,
|
_dockToolbox: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the GIF feature is enabled.
|
||||||
|
*/
|
||||||
|
_isGifEnabled: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the GIF menu is visible.
|
||||||
|
*/
|
||||||
|
_isGifMenuVisible: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not it's a mobile browser.
|
* Whether or not it's a mobile browser.
|
||||||
*/
|
*/
|
||||||
|
@ -193,12 +206,16 @@ class ReactionsMenu extends Component<Props> {
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { _raisedHand, t, overflowMenu, _isMobile, classes } = this.props;
|
const { _raisedHand, t, overflowMenu, _isMobile, classes, _isGifMenuVisible, _isGifEnabled } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className = { `reactions-menu ${overflowMenu ? `overflow ${classes.overflow}` : ''}` }>
|
<div
|
||||||
|
className = { clsx('reactions-menu', _isGifEnabled && 'with-gif',
|
||||||
|
overflowMenu && `overflow ${classes.overflow}`) }>
|
||||||
|
{_isGifEnabled && _isGifMenuVisible && <GifsMenu />}
|
||||||
<div className = 'reactions-row'>
|
<div className = 'reactions-row'>
|
||||||
{ this._getReactionButtons() }
|
{ this._getReactionButtons() }
|
||||||
|
{_isGifEnabled && <GifsMenuButton />}
|
||||||
</div>
|
</div>
|
||||||
{_isMobile && (
|
{_isMobile && (
|
||||||
<div className = 'raise-hand-row'>
|
<div className = 'raise-hand-row'>
|
||||||
|
@ -231,6 +248,8 @@ function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
_localParticipantID: localParticipant.id,
|
_localParticipantID: localParticipant.id,
|
||||||
_isMobile: isMobileBrowser(),
|
_isMobile: isMobileBrowser(),
|
||||||
|
_isGifEnabled: isGifEnabled(state),
|
||||||
|
_isGifMenuVisible: isGifsMenuOpen(state),
|
||||||
_raisedHand: hasRaisedHand(localParticipant)
|
_raisedHand: hasRaisedHand(localParticipant)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,11 @@ import { DRAWER_MAX_HEIGHT } from '../../constants';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class name for custom styles.
|
||||||
|
*/
|
||||||
|
className: string,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The component(s) to be displayed within the drawer menu.
|
* The component(s) to be displayed within the drawer menu.
|
||||||
*/
|
*/
|
||||||
|
@ -40,6 +45,7 @@ const useStyles = makeStyles(theme => {
|
||||||
*/
|
*/
|
||||||
function Drawer({
|
function Drawer({
|
||||||
children,
|
children,
|
||||||
|
className = '',
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose
|
onClose
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
@ -72,7 +78,7 @@ function Drawer({
|
||||||
className = 'drawer-menu-container'
|
className = 'drawer-menu-container'
|
||||||
onClick = { handleOutsideClick }>
|
onClick = { handleOutsideClick }>
|
||||||
<div
|
<div
|
||||||
className = { `drawer-menu ${styles.drawer}` }
|
className = { `drawer-menu ${styles.drawer} ${className}` }
|
||||||
onClick = { handleInsideClick }>
|
onClick = { handleInsideClick }>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
import { withStyles } from '@material-ui/core/styles';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import { batch } from 'react-redux';
|
||||||
|
|
||||||
import keyboardShortcut from '../../../../../modules/keyboardshortcut/keyboardshortcut';
|
import keyboardShortcut from '../../../../../modules/keyboardshortcut/keyboardshortcut';
|
||||||
import {
|
import {
|
||||||
|
@ -30,6 +31,8 @@ import { ChatButton } from '../../../chat/components';
|
||||||
import { EmbedMeetingButton } from '../../../embed-meeting';
|
import { EmbedMeetingButton } from '../../../embed-meeting';
|
||||||
import { SharedDocumentButton } from '../../../etherpad';
|
import { SharedDocumentButton } from '../../../etherpad';
|
||||||
import { FeedbackButton } from '../../../feedback';
|
import { FeedbackButton } from '../../../feedback';
|
||||||
|
import { setGifMenuVisibility } from '../../../gifs/actions';
|
||||||
|
import { isGifEnabled } from '../../../gifs/functions';
|
||||||
import { InviteButton } from '../../../invite/components/add-people-dialog';
|
import { InviteButton } from '../../../invite/components/add-people-dialog';
|
||||||
import { isVpaasMeeting } from '../../../jaas/functions';
|
import { isVpaasMeeting } from '../../../jaas/functions';
|
||||||
import { KeyboardShortcutsButton } from '../../../keyboard-shortcuts';
|
import { KeyboardShortcutsButton } from '../../../keyboard-shortcuts';
|
||||||
|
@ -41,6 +44,7 @@ import {
|
||||||
import { ParticipantsPaneButton } from '../../../participants-pane/components/web';
|
import { ParticipantsPaneButton } from '../../../participants-pane/components/web';
|
||||||
import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
|
import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
|
||||||
import { addReactionToBuffer } from '../../../reactions/actions.any';
|
import { addReactionToBuffer } from '../../../reactions/actions.any';
|
||||||
|
import { toggleReactionsMenuVisibility } from '../../../reactions/actions.web';
|
||||||
import { ReactionsMenuButton } from '../../../reactions/components';
|
import { ReactionsMenuButton } from '../../../reactions/components';
|
||||||
import { REACTIONS, REACTIONS_MENU_HEIGHT } from '../../../reactions/constants';
|
import { REACTIONS, REACTIONS_MENU_HEIGHT } from '../../../reactions/constants';
|
||||||
import { isReactionsEnabled } from '../../../reactions/functions.any';
|
import { isReactionsEnabled } from '../../../reactions/functions.any';
|
||||||
|
@ -159,6 +163,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
_fullScreen: boolean,
|
_fullScreen: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the GIFs feature is enabled.
|
||||||
|
*/
|
||||||
|
_gifsEnabled: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the app has Salesforce integration.
|
* Whether the app has Salesforce integration.
|
||||||
*/
|
*/
|
||||||
|
@ -334,7 +343,7 @@ class Toolbox extends Component<Props> {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { _toolbarButtons, t, dispatch, _reactionsEnabled } = this.props;
|
const { _toolbarButtons, t, dispatch, _reactionsEnabled, _gifsEnabled } = this.props;
|
||||||
const KEYBOARD_SHORTCUTS = [
|
const KEYBOARD_SHORTCUTS = [
|
||||||
isToolbarButtonEnabled('videoquality', _toolbarButtons) && {
|
isToolbarButtonEnabled('videoquality', _toolbarButtons) && {
|
||||||
character: 'A',
|
character: 'A',
|
||||||
|
@ -408,6 +417,22 @@ class Toolbox extends Component<Props> {
|
||||||
shortcut.helpDescription,
|
shortcut.helpDescription,
|
||||||
shortcut.altKey);
|
shortcut.altKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (_gifsEnabled) {
|
||||||
|
const onGifShortcut = () => {
|
||||||
|
batch(() => {
|
||||||
|
dispatch(toggleReactionsMenuVisibility());
|
||||||
|
dispatch(setGifMenuVisibility(true));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
APP.keyboardshortcut.registerShortcut(
|
||||||
|
'G',
|
||||||
|
null,
|
||||||
|
onGifShortcut,
|
||||||
|
t('keyboardShortcuts.giphyMenu')
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1410,6 +1435,7 @@ function _mapStateToProps(state, ownProps) {
|
||||||
_disabled: Boolean(iAmRecorder || iAmSipGateway),
|
_disabled: Boolean(iAmRecorder || iAmSipGateway),
|
||||||
_feedbackConfigured: Boolean(callStatsID),
|
_feedbackConfigured: Boolean(callStatsID),
|
||||||
_fullScreen: fullScreen,
|
_fullScreen: fullScreen,
|
||||||
|
_gifsEnabled: isGifEnabled(state),
|
||||||
_isProfileDisabled: Boolean(disableProfile),
|
_isProfileDisabled: Boolean(disableProfile),
|
||||||
_isIosMobile: isIosMobileBrowser(),
|
_isIosMobile: isIosMobileBrowser(),
|
||||||
_isMobile: isMobileBrowser(),
|
_isMobile: isMobileBrowser(),
|
||||||
|
|
Loading…
Reference in New Issue