This commit is contained in:
46
react/features/gifs/actionTypes.ts
Normal file
46
react/features/gifs/actionTypes.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 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';
|
||||
74
react/features/gifs/actions.ts
Normal file
74
react/features/gifs/actions.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
ADD_GIF_FOR_PARTICIPANT,
|
||||
HIDE_GIF_FOR_PARTICIPANT,
|
||||
REMOVE_GIF_FOR_PARTICIPANT,
|
||||
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: string, gifUrl: string) {
|
||||
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: string) {
|
||||
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: string) {
|
||||
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: string) {
|
||||
return {
|
||||
type: HIDE_GIF_FOR_PARTICIPANT,
|
||||
participantId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set visibility of the GIF menu.
|
||||
*
|
||||
* @param {boolean} visible - Whether or not it should be visible.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setGifMenuVisibility(visible: boolean) {
|
||||
return {
|
||||
type: SET_GIF_MENU_VISIBILITY,
|
||||
visible
|
||||
};
|
||||
}
|
||||
63
react/features/gifs/components/native/GifsMenu.tsx
Normal file
63
react/features/gifs/components/native/GifsMenu.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { GiphyContent, GiphyGridView, GiphyMediaType, GiphyRating } from '@giphy/react-native-sdk';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { createGifSentEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import Input from '../../../base/ui/components/native/Input';
|
||||
import { sendMessage } from '../../../chat/actions.any';
|
||||
import { goBack } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { formatGifUrlMessage, getGifRating, getGifUrl } from '../../functions.native';
|
||||
|
||||
import GifsMenuFooter from './GifsMenuFooter';
|
||||
import styles from './styles';
|
||||
|
||||
const GifsMenu = () => {
|
||||
const [ searchQuery, setSearchQuery ] = useState('');
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const rating = useSelector(getGifRating) as GiphyRating;
|
||||
const options = {
|
||||
mediaType: GiphyMediaType.Gif,
|
||||
limit: 20,
|
||||
rating
|
||||
};
|
||||
|
||||
const content = searchQuery === ''
|
||||
? GiphyContent.trending(options)
|
||||
: GiphyContent.search({
|
||||
...options,
|
||||
searchQuery
|
||||
});
|
||||
|
||||
const sendGif = useCallback(e => {
|
||||
const url = getGifUrl(e.nativeEvent.media);
|
||||
|
||||
sendAnalytics(createGifSentEvent());
|
||||
|
||||
dispatch(sendMessage(formatGifUrlMessage(url), true));
|
||||
goBack();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<JitsiScreen
|
||||
footerComponent = { GifsMenuFooter }
|
||||
style = { styles.container }>
|
||||
<Input
|
||||
clearable = { true }
|
||||
customStyles = {{ container: styles.customContainer }}
|
||||
onChange = { setSearchQuery }
|
||||
placeholder = { t('giphy.search') }
|
||||
value = { searchQuery } />
|
||||
<GiphyGridView
|
||||
cellPadding = { 5 }
|
||||
content = { content }
|
||||
onMediaSelect = { sendGif }
|
||||
style = { styles.grid } />
|
||||
</JitsiScreen>
|
||||
);
|
||||
};
|
||||
|
||||
export default GifsMenu;
|
||||
27
react/features/gifs/components/native/GifsMenuFooter.tsx
Normal file
27
react/features/gifs/components/native/GifsMenuFooter.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Image, Text, TextStyle, View, ViewStyle } from 'react-native';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
/**
|
||||
* Implements the gifs menu footer component.
|
||||
*
|
||||
* @returns { JSX.Element} - The gifs menu footer component.
|
||||
*/
|
||||
const GifsMenuFooter = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View style = { styles.credit as ViewStyle }>
|
||||
<Text style = { styles.creditText as TextStyle }>
|
||||
{ t('poweredby') }
|
||||
</Text>
|
||||
<Image
|
||||
source = { require('../../../../../images/GIPHY_logo.png') } />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default GifsMenuFooter;
|
||||
36
react/features/gifs/components/native/styles.ts
Normal file
36
react/features/gifs/components/native/styles.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
|
||||
export default {
|
||||
container: {
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
flex: 1
|
||||
},
|
||||
|
||||
customContainer: {
|
||||
marginHorizontal: BaseTheme.spacing[3],
|
||||
marginVertical: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
grid: {
|
||||
flex: 1,
|
||||
marginLeft: BaseTheme.spacing[3],
|
||||
marginRight: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
credit: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
height: 56,
|
||||
justifyContent: 'center',
|
||||
marginBottom: BaseTheme.spacing[0],
|
||||
paddingBottom: BaseTheme.spacing[4],
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
creditText: {
|
||||
color: BaseTheme.palette.text01,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
};
|
||||
253
react/features/gifs/components/web/GifsMenu.tsx
Normal file
253
react/features/gifs/components/web/GifsMenu.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import { GiphyFetch, TrendingOptions } from '@giphy/js-fetch-api';
|
||||
import { Grid } from '@giphy/react-components';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { batch, useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { createGifSentEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Input from '../../../base/ui/components/web/Input';
|
||||
import { sendMessage } from '../../../chat/actions.any';
|
||||
import { SCROLL_SIZE } from '../../../filmstrip/constants';
|
||||
import { toggleReactionsMenuVisibility } from '../../../reactions/actions.web';
|
||||
import { IReactionsMenuParent } from '../../../reactions/types';
|
||||
import Drawer from '../../../toolbox/components/web/Drawer';
|
||||
import JitsiPortal from '../../../toolbox/components/web/JitsiPortal';
|
||||
import { setGifMenuVisibility } from '../../actions';
|
||||
import {
|
||||
formatGifUrlMessage,
|
||||
getGifAPIKey,
|
||||
getGifRating,
|
||||
getGifUrl
|
||||
} from '../../function.any';
|
||||
|
||||
const OVERFLOW_DRAWER_PADDING = 16;
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
gifsMenu: {
|
||||
width: '100%',
|
||||
marginBottom: theme.spacing(2),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
'& div:focus': {
|
||||
border: '1px solid red !important',
|
||||
boxSizing: 'border-box'
|
||||
}
|
||||
},
|
||||
|
||||
searchField: {
|
||||
marginBottom: theme.spacing(3)
|
||||
},
|
||||
|
||||
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)
|
||||
},
|
||||
|
||||
overflowDrawerMenu: {
|
||||
padding: theme.spacing(3),
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
height: '100%'
|
||||
},
|
||||
|
||||
overflowMenu: {
|
||||
height: '200px',
|
||||
width: '201px',
|
||||
marginBottom: '0px'
|
||||
},
|
||||
|
||||
gifContainerOverflow: {
|
||||
flexGrow: 1
|
||||
},
|
||||
|
||||
drawer: {
|
||||
display: 'flex',
|
||||
height: '100%'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
interface IProps {
|
||||
columns?: number;
|
||||
parent: IReactionsMenuParent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gifs menu.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function GifsMenu({ columns = 2, parent }: IProps) {
|
||||
const API_KEY = useSelector(getGifAPIKey);
|
||||
const giphyFetch = new GiphyFetch(API_KEY);
|
||||
const [ searchKey, setSearchKey ] = useState<string>();
|
||||
const { classes: styles, cx } = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const isInOverflowMenu
|
||||
= parent === IReactionsMenuParent.OverflowDrawer || parent === IReactionsMenuParent.OverflowMenu;
|
||||
const { videoSpaceWidth } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
|
||||
const rating = useSelector(getGifRating);
|
||||
|
||||
const fetchGifs = useCallback(async (offset = 0) => {
|
||||
const options: TrendingOptions = {
|
||||
limit: 20,
|
||||
offset,
|
||||
rating
|
||||
};
|
||||
|
||||
if (!searchKey) {
|
||||
return await giphyFetch.trending(options);
|
||||
}
|
||||
|
||||
return await giphyFetch.search(searchKey, options);
|
||||
}, [ searchKey ]);
|
||||
|
||||
const onDrawerClose = useCallback(() => {
|
||||
dispatch(setGifMenuVisibility(false));
|
||||
}, []);
|
||||
|
||||
const handleGifClick = useCallback((gif, e) => {
|
||||
e?.stopPropagation();
|
||||
const url = getGifUrl(gif);
|
||||
|
||||
sendAnalytics(createGifSentEvent());
|
||||
batch(() => {
|
||||
dispatch(sendMessage(formatGifUrlMessage(url), true));
|
||||
dispatch(toggleReactionsMenuVisibility());
|
||||
isInOverflowMenu && onDrawerClose();
|
||||
});
|
||||
}, [ dispatch, isInOverflowMenu ]);
|
||||
|
||||
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 (!document.activeElement) {
|
||||
return;
|
||||
}
|
||||
if (e.keyCode === 38) { // up arrow
|
||||
e.preventDefault();
|
||||
|
||||
// if the first gif is focused move focus to the input
|
||||
if (document.activeElement.previousElementSibling === null) {
|
||||
const element = document.querySelector('.gif-input') as HTMLElement;
|
||||
|
||||
element?.focus();
|
||||
} else {
|
||||
const element = document.activeElement.previousElementSibling as HTMLElement;
|
||||
|
||||
element?.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')) {
|
||||
const element = document.querySelector('.giphy-gif') as HTMLElement;
|
||||
|
||||
element?.focus();
|
||||
} else {
|
||||
const element = document.activeElement.nextElementSibling as HTMLElement;
|
||||
|
||||
element?.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 onInputKeyPress = useCallback((e: React.KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const gifMenu = (
|
||||
<div
|
||||
className = { cx(styles.gifsMenu,
|
||||
parent === IReactionsMenuParent.OverflowDrawer && styles.overflowDrawerMenu,
|
||||
parent === IReactionsMenuParent.OverflowMenu && styles.overflowMenu
|
||||
) }>
|
||||
<Input
|
||||
autoFocus = { true }
|
||||
className = { cx(styles.searchField, 'gif-input') }
|
||||
id = 'gif-search-input'
|
||||
onChange = { handleSearchKeyChange }
|
||||
onKeyPress = { onInputKeyPress }
|
||||
placeholder = { t('giphy.search') }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
ref = { inputElement => {
|
||||
inputElement?.focus();
|
||||
setTimeout(() => inputElement?.focus(), 200);
|
||||
} }
|
||||
type = 'text'
|
||||
value = { searchKey ?? '' } />
|
||||
<div
|
||||
className = { cx(styles.gifContainer,
|
||||
parent === IReactionsMenuParent.OverflowDrawer && styles.gifContainerOverflow) }>
|
||||
<Grid
|
||||
columns = { columns }
|
||||
fetchGifs = { fetchGifs }
|
||||
gutter = { 6 }
|
||||
hideAttribution = { true }
|
||||
key = { searchKey }
|
||||
noLink = { true }
|
||||
noResultsMessage = { t('giphy.noResults') }
|
||||
onGifClick = { handleGifClick }
|
||||
onGifKeyPress = { handleGifKeyPress }
|
||||
width = { parent === IReactionsMenuParent.OverflowDrawer
|
||||
? videoSpaceWidth - (2 * OVERFLOW_DRAWER_PADDING) - SCROLL_SIZE
|
||||
: parent === IReactionsMenuParent.OverflowMenu ? 201 : 320
|
||||
} />
|
||||
</div>
|
||||
<div className = { styles.logoContainer }>
|
||||
<span>Powered by</span>
|
||||
<img
|
||||
alt = 'GIPHY Logo'
|
||||
src = 'images/GIPHY_logo.png' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return parent === IReactionsMenuParent.OverflowDrawer ? (
|
||||
<JitsiPortal>
|
||||
<Drawer
|
||||
className = { styles.drawer }
|
||||
isOpen = { true }
|
||||
onClose = { onDrawerClose }>
|
||||
{gifMenu}
|
||||
</Drawer>
|
||||
</JitsiPortal>
|
||||
) : gifMenu;
|
||||
}
|
||||
|
||||
export default GifsMenu;
|
||||
41
react/features/gifs/components/web/GifsMenuButton.tsx
Normal file
41
react/features/gifs/components/web/GifsMenuButton.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import ReactionButton from '../../../reactions/components/web/ReactionButton';
|
||||
import { IReactionsMenuParent } from '../../../reactions/types';
|
||||
import { setGifMenuVisibility } from '../../actions';
|
||||
import { isGifsMenuOpen } from '../../functions.web';
|
||||
|
||||
interface IProps {
|
||||
parent: IReactionsMenuParent;
|
||||
}
|
||||
|
||||
const GifsMenuButton = ({ parent }: IProps) => {
|
||||
const menuOpen = useSelector(isGifsMenuOpen);
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const icon = (
|
||||
<img
|
||||
alt = 'GIPHY Logo'
|
||||
height = { parent === IReactionsMenuParent.OverflowMenu ? 16 : 24 }
|
||||
src = 'images/GIPHY_icon.png' />
|
||||
);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
dispatch(setGifMenuVisibility(!menuOpen));
|
||||
}, [ menuOpen, parent ]);
|
||||
|
||||
return (
|
||||
<ReactionButton
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.giphy') }
|
||||
icon = { icon }
|
||||
key = 'gif'
|
||||
onClick = { handleClick }
|
||||
toggled = { true }
|
||||
tooltip = { t('toolbar.accessibilityLabel.giphy') } />
|
||||
);
|
||||
};
|
||||
|
||||
export default GifsMenuButton;
|
||||
14
react/features/gifs/constants.ts
Normal file
14
react/features/gifs/constants.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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[';
|
||||
|
||||
/**
|
||||
* The Giphy default option for audience rating.
|
||||
*/
|
||||
export const GIF_DEFAULT_RATING = 'g';
|
||||
150
react/features/gifs/function.any.ts
Normal file
150
react/features/gifs/function.any.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
|
||||
import { GIF_DEFAULT_RATING, GIF_PREFIX } from './constants';
|
||||
import { IGif } from './reducer';
|
||||
|
||||
/**
|
||||
* Returns the gif config.
|
||||
*
|
||||
* @param {IReduxState} state - Redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getGifConfig(state: IReduxState) {
|
||||
return state['features/base/config'].giphy || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the GIF display mode.
|
||||
*
|
||||
* @param {IReduxState} state - Redux state.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getGifDisplayMode(state: IReduxState) {
|
||||
return getGifConfig(state).displayMode || 'all';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the GIF audience rating.
|
||||
*
|
||||
* @param {IReduxState} state - Redux state.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getGifRating(state: IReduxState) {
|
||||
return getGifConfig(state).rating || GIF_DEFAULT_RATING;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the URL of the GIF for the given participant or null if there's none.
|
||||
*
|
||||
* @param {IReduxState} state - Redux state.
|
||||
* @param {string} participantId - Id of the participant for which to remove the GIF.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getGifForParticipant(state: IReduxState, participantId: string): IGif {
|
||||
return isGifEnabled(state) ? state['features/gifs'].gifList.get(participantId) || {} : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a given URL is allowed to be rendered as gif and false otherwise.
|
||||
*
|
||||
* @param {string} url - The URL to be validated.
|
||||
* @returns {boolean} - True if a given URL is allowed to be rendered as gif and false otherwise.
|
||||
*/
|
||||
export function isGifUrlAllowed(url: string) {
|
||||
let hostname: string | undefined;
|
||||
|
||||
try {
|
||||
const urlObject = new URL(url);
|
||||
|
||||
hostname = urlObject?.hostname;
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hostname === 'i.giphy.com';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the message is a GIF message.
|
||||
*
|
||||
* @param {string} message - Message to check.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isGifMessage(message = '') {
|
||||
const trimmedMessage = message.trim();
|
||||
|
||||
if (!trimmedMessage.toLowerCase().startsWith(GIF_PREFIX)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const url = extractGifURL(trimmedMessage);
|
||||
|
||||
return isGifUrlAllowed(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the URL from a gif message.
|
||||
*
|
||||
* @param {string} message - The message.
|
||||
* @returns {string} - The URL.
|
||||
*/
|
||||
export function extractGifURL(message = '') {
|
||||
const trimmedMessage = message.trim();
|
||||
|
||||
return trimmedMessage.substring(GIF_PREFIX.length, trimmedMessage.length - 1);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url of the gif selected in the gifs menu.
|
||||
*
|
||||
* @param {Object} gif - The gif data.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getGifUrl(gif?: { data?: { embed_url: string; }; embed_url?: string; }) {
|
||||
const embedUrl = gif?.embed_url || gif?.data?.embed_url || '';
|
||||
const idx = embedUrl.lastIndexOf('/');
|
||||
const id = embedUrl.substr(idx + 1);
|
||||
|
||||
return `https://i.giphy.com/media/${id}/giphy.gif`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the gif message.
|
||||
*
|
||||
* @param {string} url - GIF url.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatGifUrlMessage(url: string) {
|
||||
return `${GIF_PREFIX}${url}]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Giphy API Key from config.
|
||||
*
|
||||
* @param {IReduxState} state - Redux state.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getGifAPIKey(state: IReduxState) {
|
||||
return getGifConfig(state).sdkKey ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the feature is enabled.
|
||||
*
|
||||
* @param {IReduxState} state - Redux state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isGifEnabled(state: IReduxState) {
|
||||
const { disableThirdPartyRequests } = state['features/base/config'];
|
||||
const { giphy } = state['features/base/config'];
|
||||
const showGiphyIntegration = state['features/dynamic-branding']?.showGiphyIntegration !== false;
|
||||
|
||||
if (navigator.product === 'ReactNative' && window.JITSI_MEET_LITE_SDK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return showGiphyIntegration && Boolean(!disableThirdPartyRequests && giphy?.enabled && Boolean(giphy?.sdkKey));
|
||||
}
|
||||
|
||||
1
react/features/gifs/functions.native.ts
Normal file
1
react/features/gifs/functions.native.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './function.any';
|
||||
13
react/features/gifs/functions.web.ts
Normal file
13
react/features/gifs/functions.web.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
|
||||
export * from './function.any';
|
||||
|
||||
/**
|
||||
* Returns the visibility state of the gifs menu.
|
||||
*
|
||||
* @param {IReduxState} state - The state of the application.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isGifsMenuOpen(state: IReduxState) {
|
||||
return state['features/gifs'].menuOpen;
|
||||
}
|
||||
61
react/features/gifs/middleware.any.ts
Normal file
61
react/features/gifs/middleware.any.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
|
||||
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 './function.any';
|
||||
|
||||
/**
|
||||
* 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 {IReduxState} state - Redux state.
|
||||
* @param {string} id - Id of the participant for whom to clear the timeout.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _clearGifTimeout(state: IReduxState, id: string) {
|
||||
const gif = getGifForParticipant(state, id);
|
||||
|
||||
clearTimeout(gif?.timeoutID ?? -1);
|
||||
}
|
||||
3
react/features/gifs/middleware.native.ts
Normal file
3
react/features/gifs/middleware.native.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
import './middleware.any';
|
||||
import './subscriber.native';
|
||||
2
react/features/gifs/middleware.web.ts
Normal file
2
react/features/gifs/middleware.web.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
import './middleware.any';
|
||||
75
react/features/gifs/reducer.ts
Normal file
75
react/features/gifs/reducer.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import {
|
||||
ADD_GIF_FOR_PARTICIPANT,
|
||||
HIDE_GIF_FOR_PARTICIPANT,
|
||||
REMOVE_GIF_FOR_PARTICIPANT,
|
||||
SET_GIF_MENU_VISIBILITY
|
||||
} from './actionTypes';
|
||||
|
||||
const initialState = {
|
||||
gifList: new Map(),
|
||||
menuOpen: false
|
||||
};
|
||||
|
||||
export interface IGif {
|
||||
gifUrl?: string;
|
||||
timeoutID?: number;
|
||||
}
|
||||
|
||||
export interface IGifsState {
|
||||
gifList: Map<string, IGif>;
|
||||
menuOpen: boolean;
|
||||
}
|
||||
|
||||
ReducerRegistry.register<IGifsState>(
|
||||
'features/gifs',
|
||||
(state = initialState, action): IGifsState => {
|
||||
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_MENU_VISIBILITY:
|
||||
return {
|
||||
...state,
|
||||
menuOpen: action.visible
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
20
react/features/gifs/subscriber.native.ts
Normal file
20
react/features/gifs/subscriber.native.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { GiphySDK } from '@giphy/react-native-sdk';
|
||||
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
|
||||
import { getGifConfig, isGifEnabled } from './function.any';
|
||||
|
||||
/**
|
||||
* Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => getGifConfig(state),
|
||||
/* listener */ (_, store) => {
|
||||
const state = store.getState();
|
||||
|
||||
if (isGifEnabled(state)) {
|
||||
GiphySDK.configure({ apiKey: state['features/base/config'].giphy?.sdkKey ?? '' });
|
||||
}
|
||||
}, {
|
||||
deepEquals: true
|
||||
});
|
||||
Reference in New Issue
Block a user