mirror of https://github.com/jitsi/jitsi-meet
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 analyticspull/11111/head jitsi-meet_7031
parent
b6d55571ba
commit
190041fc5a
After Width: | Height: | Size: 284 B |
After Width: | Height: | Size: 1.0 KiB |
File diff suppressed because it is too large
Load Diff
@ -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; |
@ -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; |
||||
}); |
||||
|
Loading…
Reference in new issue