Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

GifsMenu.tsx 7.6KB

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