Replace Gfycat with GIPHY in emoji/gif picker (#24236)

* Added the package

* design

* add styled-component alias

* add contrast ratio

* added the tab icon

* review comments

* rev comments

* key added

* key added

* trans

* Added giphy sdk test key for playwright tests config

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: maria.nunez <maria.nunez@mattermost.com>
Этот коммит содержится в:
M-ZubairAhmed
2023-08-19 01:32:46 +05:30
коммит произвёл GitHub
родитель 8418eefb75
Коммит bc11b29807
25 изменённых файлов: 1255 добавлений и 593 удалений

Просмотреть файл

@@ -129,6 +129,7 @@ const defaultServerConfig: AdminConfig = {
EnableGifPicker: true, EnableGifPicker: true,
GfycatAPIKey: '2_KtH_W5', GfycatAPIKey: '2_KtH_W5',
GfycatAPISecret: '3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof', GfycatAPISecret: '3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof',
GiphySdkKey: 's0glxvzVg9azvPipKxcPLpXV0q1x1fVP',
EnableCustomEmoji: true, EnableCustomEmoji: true,
EnableEmojiPicker: true, EnableEmojiPicker: true,
PostEditTimeLimit: -1, PostEditTimeLimit: -1,

Просмотреть файл

@@ -80,6 +80,7 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
props["EnableGifPicker"] = strconv.FormatBool(*c.ServiceSettings.EnableGifPicker) props["EnableGifPicker"] = strconv.FormatBool(*c.ServiceSettings.EnableGifPicker)
props["GfycatApiKey"] = *c.ServiceSettings.GfycatAPIKey props["GfycatApiKey"] = *c.ServiceSettings.GfycatAPIKey
props["GfycatApiSecret"] = *c.ServiceSettings.GfycatAPISecret props["GfycatApiSecret"] = *c.ServiceSettings.GfycatAPISecret
props["GiphySdkKey"] = *c.ServiceSettings.GiphySdkKey
props["MaxFileSize"] = strconv.FormatInt(*c.FileSettings.MaxFileSize, 10) props["MaxFileSize"] = strconv.FormatInt(*c.FileSettings.MaxFileSize, 10)
props["MaxNotificationsPerChannel"] = strconv.FormatInt(*c.TeamSettings.MaxNotificationsPerChannel, 10) props["MaxNotificationsPerChannel"] = strconv.FormatInt(*c.TeamSettings.MaxNotificationsPerChannel, 10)

Просмотреть файл

@@ -111,6 +111,8 @@ const (
ServiceSettingsDefaultListenAndAddress = ":8065" ServiceSettingsDefaultListenAndAddress = ":8065"
ServiceSettingsDefaultGfycatAPIKey = "2_KtH_W5" ServiceSettingsDefaultGfycatAPIKey = "2_KtH_W5"
ServiceSettingsDefaultGfycatAPISecret = "3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof" ServiceSettingsDefaultGfycatAPISecret = "3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof"
ServiceSettingsDefaultGiphySdkKey = "yaRojIWaxmKhtSMBaT3uLCAHm0kpMLKw"
ServiceSettingsDefaultGiphySdkKeyTest = "s0glxvzVg9azvPipKxcPLpXV0q1x1fVP"
ServiceSettingsDefaultDeveloperFlags = "" ServiceSettingsDefaultDeveloperFlags = ""
TeamSettingsDefaultSiteName = "Mattermost" TeamSettingsDefaultSiteName = "Mattermost"
@@ -348,6 +350,7 @@ type ServiceSettings struct {
EnableGifPicker *bool `access:"integrations_gif"` EnableGifPicker *bool `access:"integrations_gif"`
GfycatAPIKey *string `access:"integrations_gif"` GfycatAPIKey *string `access:"integrations_gif"`
GfycatAPISecret *string `access:"integrations_gif"` GfycatAPISecret *string `access:"integrations_gif"`
GiphySdkKey *string `access:"integrations_gif"`
EnableCustomEmoji *bool `access:"site_emoji"` EnableCustomEmoji *bool `access:"site_emoji"`
EnableEmojiPicker *bool `access:"site_emoji"` EnableEmojiPicker *bool `access:"site_emoji"`
PostEditTimeLimit *int `access:"user_management_permissions"` PostEditTimeLimit *int `access:"user_management_permissions"`
@@ -740,6 +743,15 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) {
s.GfycatAPISecret = NewString(ServiceSettingsDefaultGfycatAPISecret) s.GfycatAPISecret = NewString(ServiceSettingsDefaultGfycatAPISecret)
} }
if s.GiphySdkKey == nil {
switch GetServiceEnvironment() {
case ServiceEnvironmentProduction:
s.GiphySdkKey = NewString(ServiceSettingsDefaultGiphySdkKey)
case ServiceEnvironmentTest, ServiceEnvironmentDev:
s.GiphySdkKey = NewString(ServiceSettingsDefaultGiphySdkKeyTest)
}
}
if s.ExperimentalEnableAuthenticationTransfer == nil { if s.ExperimentalEnableAuthenticationTransfer == nil {
s.ExperimentalEnableAuthenticationTransfer = NewBool(true) s.ExperimentalEnableAuthenticationTransfer = NewBool(true)
} }

Просмотреть файл

@@ -8,6 +8,8 @@
"dependencies": { "dependencies": {
"@floating-ui/react-dom": "1.0.0", "@floating-ui/react-dom": "1.0.0",
"@floating-ui/react-dom-interactions": "0.10.3", "@floating-ui/react-dom-interactions": "0.10.3",
"@giphy/js-fetch-api": "5.1.0",
"@giphy/react-components": "8.1.0",
"@guyplusplus/turndown-plugin-gfm": "1.0.7", "@guyplusplus/turndown-plugin-gfm": "1.0.7",
"@mattermost/client": "*", "@mattermost/client": "*",
"@mattermost/compass-components": "^0.2.12", "@mattermost/compass-components": "^0.2.12",

Просмотреть файл

@@ -6100,36 +6100,7 @@ const AdminDefinition = {
label: t('admin.customization.enableGifPickerTitle'), label: t('admin.customization.enableGifPickerTitle'),
label_default: 'Enable GIF Picker:', label_default: 'Enable GIF Picker:',
help_text: t('admin.customization.enableGifPickerDesc'), help_text: t('admin.customization.enableGifPickerDesc'),
help_text_default: 'Allow users to select GIFs from the emoji picker via a Gfycat integration.', help_text_default: 'Allows users to select GIFs from the emoji picker.',
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.INTEGRATIONS.GIF)),
},
{
type: Constants.SettingsTypes.TYPE_TEXT,
key: 'ServiceSettings.GfycatAPIKey',
label: t('admin.customization.gfycatApiKey'),
label_default: 'Gfycat API Key:',
help_text: t('admin.customization.gfycatApiKeyDescription'),
help_text_default: 'Request an API key at <link>https://developers.gfycat.com/signup/#</link>. Enter the client ID you receive via email to this field. When blank, uses the default API key provided by Gfycat.',
help_text_markdown: false,
help_text_values: {
link: (msg) => (
<ExternalLink
location='admin_console'
href='https://developers.gfycat.com/signup/#'
>
{msg}
</ExternalLink>
),
},
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.INTEGRATIONS.GIF)),
},
{
type: Constants.SettingsTypes.TYPE_TEXT,
key: 'ServiceSettings.GfycatAPISecret',
label: t('admin.customization.gfycatApiSecret'),
label_default: 'Gfycat API Secret:',
help_text: t('admin.customization.gfycatApiSecretDescription'),
help_text_default: 'The API secret generated by Gfycat for your API key. When blank, uses the default API secret provided by Gfycat.',
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.INTEGRATIONS.GIF)), isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.INTEGRATIONS.GIF)),
}, },
], ],

Просмотреть файл

@@ -5,7 +5,7 @@ import React, {useRef, useState, useEffect, useCallback, memo, useMemo} from 're
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import type {FixedSizeList} from 'react-window'; import type {FixedSizeList} from 'react-window';
import type InfiniteLoader from 'react-window-infinite-loader'; import type InfiniteLoader from 'react-window-infinite-loader';
import {throttle} from 'lodash'; import throttle from 'lodash/throttle';
import {Emoji, EmojiCategory} from '@mattermost/types/emojis'; import {Emoji, EmojiCategory} from '@mattermost/types/emojis';
import {isSystemEmoji} from 'mattermost-redux/utils/emoji_utils'; import {isSystemEmoji} from 'mattermost-redux/utils/emoji_utils';
@@ -34,7 +34,6 @@ import type {PropsFromRedux} from './index';
interface Props extends PropsFromRedux { interface Props extends PropsFromRedux {
filter: string; filter: string;
visible: boolean;
onEmojiClick: (emoji: Emoji) => void; onEmojiClick: (emoji: Emoji) => void;
handleFilterChange: (filter: string) => void; handleFilterChange: (filter: string) => void;
handleEmojiPickerClose: () => void; handleEmojiPickerClose: () => void;
@@ -42,7 +41,6 @@ interface Props extends PropsFromRedux {
const EmojiPicker = ({ const EmojiPicker = ({
filter, filter,
visible,
onEmojiClick, onEmojiClick,
handleFilterChange, handleFilterChange,
handleEmojiPickerClose, handleEmojiPickerClose,
@@ -125,10 +123,9 @@ const EmojiPicker = ({
throttledSearchCustomEmoji.current(filter, customEmojisEnabled); throttledSearchCustomEmoji.current(filter, customEmojisEnabled);
}, [filter, shouldRunCreateCategoryAndEmojiRows.current, customEmojisEnabled]); }, [filter, shouldRunCreateCategoryAndEmojiRows.current, customEmojisEnabled]);
// Hack for getting focus on search input when tab changes to emoji from gifs
useEffect(() => { useEffect(() => {
searchInputRef.current?.focus(); searchInputRef.current?.focus();
}, [visible]); }, []);
// clear out the active category on search input // clear out the active category on search input
useEffect(() => { useEffect(() => {

Просмотреть файл

@@ -1,14 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {CSSProperties, PureComponent} from 'react'; import React, {CSSProperties, PureComponent, createRef, RefObject} from 'react';
import {Tab, Tabs} from 'react-bootstrap'; import {Tab, Tabs} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import EmojiIcon from 'components/widgets/icons/emoji_icon';
import GfycatIcon from 'components/widgets/icons/gfycat_icon';
import {makeAsyncComponent} from 'components/async_load'; import {makeAsyncComponent} from 'components/async_load';
import EmojiPicker from 'components/emoji_picker'; import EmojiPicker from 'components/emoji_picker';
import EmojiPickerHeader from 'components/emoji_picker/components/emoji_picker_header'; import EmojiPickerHeader from 'components/emoji_picker/components/emoji_picker_header';
import EmojiIcon from 'components/widgets/icons/emoji_icon';
import GifIcon from 'components/widgets/icons/giphy_icon';
import {Emoji} from '@mattermost/types/emojis'; import {Emoji} from '@mattermost/types/emojis';
@@ -32,6 +33,8 @@ type State = {
} }
export default class EmojiPickerTabs extends PureComponent<Props, State> { export default class EmojiPickerTabs extends PureComponent<Props, State> {
private rootPickerNodeRef: RefObject<HTMLDivElement>;
static defaultProps = { static defaultProps = {
rightOffset: 0, rightOffset: 0,
topOffset: 0, topOffset: 0,
@@ -45,20 +48,10 @@ export default class EmojiPickerTabs extends PureComponent<Props, State> {
emojiTabVisible: true, emojiTabVisible: true,
filter: '', filter: '',
}; };
this.rootPickerNodeRef = createRef();
} }
handleEnterEmojiTab = () => {
this.setState({
emojiTabVisible: true,
});
};
handleExitEmojiTab = () => {
this.setState({
emojiTabVisible: false,
});
};
handleEmojiPickerClose = () => { handleEmojiPickerClose = () => {
this.props.onEmojiClose(); this.props.onEmojiClose();
}; };
@@ -67,6 +60,10 @@ export default class EmojiPickerTabs extends PureComponent<Props, State> {
this.setState({filter}); this.setState({filter});
}; };
getRootPickerNode = () => {
return this.rootPickerNodeRef.current;
};
render() { render() {
let pickerStyle; let pickerStyle;
if (this.props.style && !(this.props.style.left === 0 && this.props.style.top === 0)) { if (this.props.style && !(this.props.style.left === 0 && this.props.style.top === 0)) {
@@ -99,51 +96,69 @@ export default class EmojiPickerTabs extends PureComponent<Props, State> {
if (this.props.enableGifPicker && typeof this.props.onGifClick != 'undefined') { if (this.props.enableGifPicker && typeof this.props.onGifClick != 'undefined') {
return ( return (
<Tabs <div
defaultActiveKey={1} ref={this.rootPickerNodeRef}
id='emoji-picker-tabs'
style={pickerStyle} style={pickerStyle}
className={pickerClass} className={pickerClass}
justified={true}
> >
<EmojiPickerHeader handleEmojiPickerClose={this.handleEmojiPickerClose}/> <Tabs
<Tab id='emoji-picker-tabs'
eventKey={1} defaultActiveKey={1}
onEnter={this.handleEnterEmojiTab} justified={true}
onExit={this.handleExitEmojiTab} mountOnEnter={true}
title={
<div className={'custom-emoji-tab__icon__text'}>
<EmojiIcon
className='custom-emoji-tab__icon'
/>
<div>
{'Emojis'}
</div>
</div>
}
tabClassName={'custom-emoji-tab'}
>
<EmojiPicker
filter={this.state.filter}
visible={this.state.emojiTabVisible}
onEmojiClick={this.props.onEmojiClick}
handleFilterChange={this.handleFilterChange}
handleEmojiPickerClose={this.handleEmojiPickerClose}
/>
</Tab>
<Tab
eventKey={2}
title={<GfycatIcon/>}
unmountOnExit={true} unmountOnExit={true}
tabClassName={'custom-emoji-tab'}
> >
<GifPicker <EmojiPickerHeader handleEmojiPickerClose={this.handleEmojiPickerClose}/>
onGifClick={this.props.onGifClick} <Tab
defaultSearchText={this.state.filter} eventKey={1}
handleSearchTextChange={this.handleFilterChange} title={
/> <div className={'custom-emoji-tab__icon__text'}>
</Tab> <EmojiIcon
</Tabs> className='custom-emoji-tab__icon'
aria-hidden={true}
/>
<FormattedMessage
id='emoji_gif_picker.tabs.emojis'
defaultMessage='Emojis'
/>
</div>
}
unmountOnExit={true}
tabClassName={'custom-emoji-tab'}
>
<EmojiPicker
filter={this.state.filter}
onEmojiClick={this.props.onEmojiClick}
handleFilterChange={this.handleFilterChange}
handleEmojiPickerClose={this.handleEmojiPickerClose}
/>
</Tab>
<Tab
eventKey={2}
title={
<div className={'custom-emoji-tab__icon__text'}>
<GifIcon
className='custom-emoji-tab__icon'
aria-hidden={true}
/>
<FormattedMessage
id='emoji_gif_picker.tabs.gifs'
defaultMessage='GIFs'
/>
</div>
}
unmountOnExit={true}
tabClassName={'custom-emoji-tab'}
>
<GifPicker
filter={this.state.filter}
getRootPickerNode={this.getRootPickerNode}
onGifClick={this.props.onGifClick}
handleFilterChange={this.handleFilterChange}
/>
</Tab>
</Tabs>
</div>
); );
} }
@@ -156,7 +171,6 @@ export default class EmojiPickerTabs extends PureComponent<Props, State> {
<EmojiPickerHeader handleEmojiPickerClose={this.handleEmojiPickerClose}/> <EmojiPickerHeader handleEmojiPickerClose={this.handleEmojiPickerClose}/>
<EmojiPicker <EmojiPicker
filter={this.state.filter} filter={this.state.filter}
visible={this.state.emojiTabVisible}
onEmojiClick={this.props.onEmojiClick} onEmojiClick={this.props.onEmojiClick}
handleFilterChange={this.handleFilterChange} handleFilterChange={this.handleFilterChange}
handleEmojiPickerClose={this.handleEmojiPickerClose} handleEmojiPickerClose={this.handleEmojiPickerClose}

Просмотреть файл

@@ -7,14 +7,13 @@ import {connect} from 'react-redux';
import {saveAppProps} from 'mattermost-redux/actions/gifs'; import {saveAppProps} from 'mattermost-redux/actions/gifs';
import Header from 'components/gif_picker/components/Header'; import Header from 'components/gif_picker/components/Header';
import {appProps} from 'components/gif_picker/gif_picker';
const mapDispatchToProps = ({ const mapDispatchToProps = ({
saveAppProps, saveAppProps,
}); });
type Props = { type Props = {
appProps: typeof appProps; appProps: any;
action: string; action: string;
onCategories: () => void; onCategories: () => void;
onSearch?: () => void; onSearch?: () => void;

Просмотреть файл

@@ -15,7 +15,6 @@ import {trackEvent} from 'actions/telemetry_actions.jsx';
import {getImageSrc} from 'utils/post_utils'; import {getImageSrc} from 'utils/post_utils';
import InfiniteScroll from 'components/gif_picker/components/InfiniteScroll'; import InfiniteScroll from 'components/gif_picker/components/InfiniteScroll';
import {appProps} from 'components/gif_picker/gif_picker';
import './Categories.scss'; import './Categories.scss';
@@ -39,7 +38,7 @@ const mapDispatchToProps = ({
}); });
type Props = { type Props = {
appProps: typeof appProps; appProps: any;
gifs?: Record<string, GfycatAPIItem>; gifs?: Record<string, GfycatAPIItem>;
hasMore?: boolean; hasMore?: boolean;
onSearch: () => void; onSearch: () => void;

Просмотреть файл

@@ -14,7 +14,6 @@ import GifTrendingIcon from 'components/widgets/icons/gif_trending_icon';
import GifReactionsIcon from 'components/widgets/icons/gif_reactions_icon'; import GifReactionsIcon from 'components/widgets/icons/gif_reactions_icon';
import './Header.scss'; import './Header.scss';
import {GlobalState} from 'types/store'; import {GlobalState} from 'types/store';
import {appProps} from 'components/gif_picker/gif_picker';
function mapStateToProps(state: GlobalState) { function mapStateToProps(state: GlobalState) {
return { return {
@@ -57,7 +56,7 @@ const getStyle = makeStyleFromTheme((theme) => {
type Props = { type Props = {
action: string; action: string;
appProps: typeof appProps; appProps: any;
saveSearchBarText: (searchBarText: string) => void; saveSearchBarText: (searchBarText: string) => void;
searchTextUpdate: (searchText: string) => void; searchTextUpdate: (searchText: string) => void;
theme: Theme; theme: Theme;
@@ -102,7 +101,7 @@ export class Header extends PureComponent<Props, State> {
renderTabs(props: Props, style: Style) { renderTabs(props: Props, style: Style) {
const {appProps, onTrending, onCategories} = props; const {appProps, onTrending, onCategories} = props;
const {header} = appProps; const {header} = appProps;
return header.tabs.map((tab, index) => { return header.tabs.map((tab: any, index: any) => {
let link; let link;
if (tab === constants.Tab.TRENDING) { if (tab === constants.Tab.TRENDING) {
link = this.renderTab({name: 'trending', callback: onTrending, Icon: GifTrendingIcon, index, style}); link = this.renderTab({name: 'trending', callback: onTrending, Icon: GifTrendingIcon, index, style});

Просмотреть файл

@@ -10,7 +10,6 @@ import {GfycatAPIItem} from '@mattermost/types/gifs';
import {searchIfNeededInitial, searchGfycat} from 'mattermost-redux/actions/gifs'; import {searchIfNeededInitial, searchGfycat} from 'mattermost-redux/actions/gifs';
import SearchGrid from 'components/gif_picker/components/SearchGrid'; import SearchGrid from 'components/gif_picker/components/SearchGrid';
import {appProps} from 'components/gif_picker/gif_picker';
import {GlobalState} from 'types/store'; import {GlobalState} from 'types/store';
@@ -28,7 +27,7 @@ const mapDispatchToProps = ({
}); });
type Props = { type Props = {
appProps: typeof appProps; appProps: any;
onCategories?: () => void; onCategories?: () => void;
handleItemClick: (gif: GfycatAPIItem) => void; handleItemClick: (gif: GfycatAPIItem) => void;
searchText: string; searchText: string;

Просмотреть файл

@@ -13,7 +13,6 @@ import {
} from 'mattermost-redux/actions/gifs'; } from 'mattermost-redux/actions/gifs';
import SearchGrid from 'components/gif_picker/components/SearchGrid'; import SearchGrid from 'components/gif_picker/components/SearchGrid';
import {appProps} from 'components/gif_picker/gif_picker';
const mapDispatchToProps = ({ const mapDispatchToProps = ({
searchCategory, searchCategory,
@@ -22,7 +21,7 @@ const mapDispatchToProps = ({
}); });
type Props = { type Props = {
appProps: typeof appProps; appProps: any;
searchIfNeededInitial: (searchText: string) => void; searchIfNeededInitial: (searchText: string) => void;
onCategories: () => void; onCategories: () => void;
saveSearchScrollPosition: (scrollPosition: number) => void; saveSearchScrollPosition: (scrollPosition: number) => void;

Просмотреть файл

@@ -0,0 +1,62 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react';
import {useSelector} from 'react-redux';
import {EmojiVariationsListProps, Grid} from '@giphy/react-components';
import {GifsResult} from '@giphy/js-fetch-api';
import {getGiphyFetchInstance} from 'mattermost-redux/selectors/entities/general';
import NoResultsIndicator from 'components/no_results_indicator';
import {NoResultsVariant} from 'components/no_results_indicator/types';
const GUTTER_BETWEEN_GIFS = 8;
const NUM_OF_GIFS_COLUMNS = 2;
interface Props {
width: number;
filter: string;
onClick: EmojiVariationsListProps['onGifClick'];
}
function GifPickerItems(props: Props) {
const giphyFetch = useSelector(getGiphyFetchInstance);
const fetchGifs = useCallback(async (offset: number) => {
if (!giphyFetch) {
return {} as GifsResult;
}
// We dont have to throttled the fetching as the library does it for us
if (props.filter.length > 0) {
const filteredResult = await giphyFetch.search(props.filter, {offset, limit: 10});
return filteredResult;
}
const trendingResult = await giphyFetch.trending({offset, limit: 10});
return trendingResult;
}, [props.filter, giphyFetch]);
return (
<div className='emoji-picker__items gif-picker__items'>
<Grid
key={props.filter.length === 0 ? 'trending' : props.filter}
columns={NUM_OF_GIFS_COLUMNS}
gutter={GUTTER_BETWEEN_GIFS}
hideAttribution={true}
width={props.width}
noResultsMessage={
<NoResultsIndicator
variant={NoResultsVariant.ChannelSearch}
titleValues={{channelName: `"${props.filter}"`}}
/>
}
fetchGifs={fetchGifs}
onGifClick={props.onClick}
/>
</div>
);
}
export default memo(GifPickerItems);

Просмотреть файл

@@ -0,0 +1,70 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {ChangeEvent, memo, useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {useSelector} from 'react-redux';
import tinycolor from 'tinycolor2';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import giphyWhiteImage from 'images/gif_picker/powered-by-giphy-white.png';
import giphyBlackImage from 'images/gif_picker/powered-by-giphy-black.png';
interface Props {
value: string;
onChange: (value: string) => void;
}
function GifPickerSearch(props: Props) {
const theme = useSelector(getTheme);
const {formatMessage} = useIntl();
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
// remove trailing and leading colons
const value = event.target?.value?.replace(/^:|:$/g, '') ?? '';
props.onChange(value);
}, [props.onChange]);
const shouldUseWhiteLogo = useMemo(() => {
const WHITE_COLOR = '#FFFFFF';
const BLACK_COLOR = '#000000';
const mostReadableColor = tinycolor.mostReadable(theme.centerChannelBg, [WHITE_COLOR, BLACK_COLOR], {includeFallbackColors: false});
if (mostReadableColor.isLight()) {
return true;
}
return false;
}, [theme.centerChannelBg]);
return (
<div className='emoji-picker__search-container'>
<div className='emoji-picker__text-container'>
<span className='icon-magnify icon emoji-picker__search-icon'/>
<input
id='emojiPickerSearch'
className='emoji-picker__search'
aria-label={formatMessage({id: 'gif_picker.input.label', defaultMessage: 'Search for GIFs'})}
placeholder={formatMessage({id: 'gif_picker.input.placeholder', defaultMessage: 'Search GIPHY'})}
type='text'
autoFocus={true}
autoComplete='off'
onChange={handleChange}
value={props.value}
/>
</div>
<div className='gif-attribution'>
<img
src={shouldUseWhiteLogo ? giphyWhiteImage : giphyBlackImage}
alt={formatMessage({id: 'gif_picker.attribution.alt', defaultMessage: 'Powered by GIPHY'})}
/>
</div>
</div>
);
}
export default memo(GifPickerSearch);

Просмотреть файл

@@ -1,90 +1,48 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {useState} from 'react'; import React, {SyntheticEvent, useCallback, useMemo} from 'react';
import {IGif} from '@giphy/js-types';
import {GifsAppState, GfycatAPIItem} from '@mattermost/types/gifs'; import GifPickerSearch from './components/gif_picker_search';
import GifPickerItems from './components/gif_picker_items';
import App from 'components/gif_picker/components/App'; const GIF_DEFAULT_WIDTH = 350;
import Categories from 'components/gif_picker/components/Categories'; const GIF_MARGIN_ENDS = 12;
import Search from 'components/gif_picker/components/Search';
import Trending from 'components/gif_picker/components/Trending';
import constants from 'components/gif_picker/utils/constants';
export const appProps: GifsAppState = {
appName: constants.appName.mattermost,
basePath: '/mattermost',
itemTapType: constants.ItemTapAction.SHARE,
appClassName: 'gfycat',
shareEvent: 'shareMattermost',
appId: 'mattermostwebviews',
enableHistory: true,
header: {
tabs: [constants.Tab.TRENDING, constants.Tab.REACTIONS],
displayText: false,
},
};
type Props = { type Props = {
filter: string;
onGifClick?: (gif: string) => void; onGifClick?: (gif: string) => void;
defaultSearchText?: string; handleFilterChange: (filter: string) => void;
handleSearchTextChange: (text: string) => void; getRootPickerNode: () => HTMLDivElement | null;
} }
const GifPicker = (props: Props) => { const GifPicker = (props: Props) => {
const [action, setAction] = useState(props.defaultSearchText ? 'search' : 'trending'); const handleItemClick = useCallback((gif: IGif, event: SyntheticEvent<HTMLElement, Event>) => {
if (props.onGifClick) {
event.preventDefault();
const handleTrending = () => setAction('trending'); const imageWithMarkdown = `![${gif.title}](${gif.images.fixed_height.url})`;
const handleCategories = () => setAction('reactions'); props.onGifClick(imageWithMarkdown);
const handleSearch = () => setAction('search'); }
}, [props.onGifClick]);
const handleItemClick = (gif: GfycatAPIItem) => { const pickerWidth = useMemo(() => {
props.onGifClick?.('![' + gif.title?.replace(/]|\[/g, '\\$&') + '](' + gif.max5mbGif + ')'); const pickerWidth = props.getRootPickerNode?.()?.getBoundingClientRect()?.width ?? GIF_DEFAULT_WIDTH;
}; return (pickerWidth - (2 * GIF_MARGIN_ENDS));
}, [props.getRootPickerNode]);
let component;
switch (action) {
case 'reactions':
component = (
<Categories
appProps={appProps}
onTrending={handleTrending}
onSearch={handleSearch}
/>
);
break;
case 'search':
component = (
<Search
appProps={appProps}
onCategories={handleCategories}
handleItemClick={handleItemClick}
/>
);
break;
case 'trending':
component = (
<Trending
appProps={appProps}
onCategories={handleCategories}
handleItemClick={handleItemClick}
/>
);
break;
}
return ( return (
<div> <div>
<App <GifPickerSearch
appProps={appProps} value={props.filter}
action={action} onChange={props.handleFilterChange}
onTrending={handleTrending} />
onCategories={handleCategories} <GifPickerItems
onSearch={handleSearch} width={pickerWidth}
defaultSearchText={props.defaultSearchText} filter={props.filter}
handleSearchTextChange={props.handleSearchTextChange} onClick={handleItemClick}
> />
{component}
</App>
</div> </div>
); );
}; };

Просмотреть файл

@@ -0,0 +1,28 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {HTMLAttributes} from 'react';
function GiphyIcon(props: HTMLAttributes<HTMLSpanElement>) {
return (
<span {...props}>
<svg
xmlns='http://www.w3.org/2000/svg'
width='16px'
height='16px'
viewBox='0 0 20 20'
>
<path
d='M16 10V18H4V2H7.73654L9.73654 0H2V20H18V8L16 10Z'
fill='inherit'
/>
<path
d='M11 0H13.3333V2.33325H15.6667V4.66675H18V7.00008H11V0Z'
fill='inherit'
/>
</svg>
</span>
);
}
export default GiphyIcon;

Просмотреть файл

@@ -653,7 +653,7 @@
"admin.customization.enableCustomEmojiTitle": "Enable Custom Emoji:", "admin.customization.enableCustomEmojiTitle": "Enable Custom Emoji:",
"admin.customization.enableEmojiPickerDesc": "The emoji picker allows users to select emoji to add as reactions or use in messages. Enabling the emoji picker with a large number of custom emoji may slow down performance.", "admin.customization.enableEmojiPickerDesc": "The emoji picker allows users to select emoji to add as reactions or use in messages. Enabling the emoji picker with a large number of custom emoji may slow down performance.",
"admin.customization.enableEmojiPickerTitle": "Enable Emoji Picker:", "admin.customization.enableEmojiPickerTitle": "Enable Emoji Picker:",
"admin.customization.enableGifPickerDesc": "Allow users to select GIFs from the emoji picker via a Gfycat integration.", "admin.customization.enableGifPickerDesc": "Allows users to select GIFs from the emoji picker.",
"admin.customization.enableGifPickerTitle": "Enable GIF Picker:", "admin.customization.enableGifPickerTitle": "Enable GIF Picker:",
"admin.customization.enableInlineLatexDesc": "Enable rendering of inline Latex code. If false, Latex can only be rendered in a code block using syntax highlighting. Please review our <link>documentation</link> for details about text formatting.", "admin.customization.enableInlineLatexDesc": "Enable rendering of inline Latex code. If false, Latex can only be rendered in a code block using syntax highlighting. Please review our <link>documentation</link> for details about text formatting.",
"admin.customization.enableInlineLatexTitle": "Enable Inline Latex Rendering:", "admin.customization.enableInlineLatexTitle": "Enable Inline Latex Rendering:",
@@ -665,10 +665,6 @@
"admin.customization.enablePermalinkPreviewsTitle": "Enable message link previews:", "admin.customization.enablePermalinkPreviewsTitle": "Enable message link previews:",
"admin.customization.enableSVGsDesc": "Enable previews for SVG file attachments and allow them to appear in messages.\n\nEnabling SVGs is not recommended in environments where not all users are trusted.", "admin.customization.enableSVGsDesc": "Enable previews for SVG file attachments and allow them to appear in messages.\n\nEnabling SVGs is not recommended in environments where not all users are trusted.",
"admin.customization.enableSVGsTitle": "Enable SVGs:", "admin.customization.enableSVGsTitle": "Enable SVGs:",
"admin.customization.gfycatApiKey": "Gfycat API Key:",
"admin.customization.gfycatApiKeyDescription": "Request an API key at <link>https://developers.gfycat.com/signup/#</link>. Enter the client ID you receive via email to this field. When blank, uses the default API key provided by Gfycat.",
"admin.customization.gfycatApiSecret": "Gfycat API Secret:",
"admin.customization.gfycatApiSecretDescription": "The API secret generated by Gfycat for your API key. When blank, uses the default API secret provided by Gfycat.",
"admin.customization.iosAppDownloadLinkDesc": "Add a link to download the iOS app. Users who access the site on a mobile web browser will be prompted with a page giving them the option to download the app. Leave this field blank to prevent the page from appearing.", "admin.customization.iosAppDownloadLinkDesc": "Add a link to download the iOS app. Users who access the site on a mobile web browser will be prompted with a page giving them the option to download the app. Leave this field blank to prevent the page from appearing.",
"admin.customization.iosAppDownloadLinkTitle": "iOS App Download Link:", "admin.customization.iosAppDownloadLinkTitle": "iOS App Download Link:",
"admin.customization.restrictLinkPreviewsDesc": "Link previews and image link previews will not be shown for the above list of comma-separated domains.", "admin.customization.restrictLinkPreviewsDesc": "Link previews and image link previews will not be shown for the above list of comma-separated domains.",
@@ -3354,6 +3350,8 @@
"email_verify.return": "Return to log in", "email_verify.return": "Return to log in",
"email_verify.sending": "Sending email…", "email_verify.sending": "Sending email…",
"email_verify.sent": "Verification email sent", "email_verify.sent": "Verification email sent",
"emoji_gif_picker.tabs.emojis": "Emojis",
"emoji_gif_picker.tabs.gifs": "GIFs",
"emoji_list.actions": "Actions", "emoji_list.actions": "Actions",
"emoji_list.add": "Add Custom Emoji", "emoji_list.add": "Add Custom Emoji",
"emoji_list.creator": "Creator", "emoji_list.creator": "Creator",
@@ -3618,7 +3616,10 @@
"get_link.copy": "Copy Link", "get_link.copy": "Copy Link",
"get_public_link_modal.help": "The link below allows anyone to see this file without being registered on this server.", "get_public_link_modal.help": "The link below allows anyone to see this file without being registered on this server.",
"get_public_link_modal.title": "Copy Public Link", "get_public_link_modal.title": "Copy Public Link",
"gif_picker.attribution.alt": "Powered by GIPHY",
"gif_picker.gfycat": "Search Gfycat", "gif_picker.gfycat": "Search Gfycat",
"gif_picker.input.label": "Search for GIFs",
"gif_picker.input.placeholder": "Search GIPHY",
"global_header.productSettings": "Settings", "global_header.productSettings": "Settings",
"global_header.productSwitchMenu": "Product switch menu", "global_header.productSwitchMenu": "Product switch menu",
"globalThreads.heading": "Followed threads", "globalThreads.heading": "Followed threads",

Двоичные данные
webapp/channels/src/images/gif_picker/powered-by-giphy-black.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.4 KiB

Двоичные данные
webapp/channels/src/images/gif_picker/powered-by-giphy-white.png Обычный файл

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.4 KiB

Просмотреть файл

@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {GiphyFetch} from '@giphy/js-fetch-api';
import {createSelector} from 'mattermost-redux/selectors/create_selector'; import {createSelector} from 'mattermost-redux/selectors/create_selector';
import {General} from 'mattermost-redux/constants'; import {General} from 'mattermost-redux/constants';
@@ -112,3 +114,16 @@ export const isMarketplaceEnabled: (state: GlobalState) => boolean = createSelec
return config.PluginsEnabled === 'true' && config.EnableMarketplace === 'true'; return config.PluginsEnabled === 'true' && config.EnableMarketplace === 'true';
}, },
); );
export const getGiphyFetchInstance: (state: GlobalState) => GiphyFetch | null = createSelector(
'getGiphyFetchInstance',
(state) => getConfig(state).GiphySdkKey,
(giphySdkKey) => {
if (giphySdkKey) {
const giphyFetch = new GiphyFetch(giphySdkKey);
return giphyFetch;
}
return null;
},
);

Просмотреть файл

@@ -54,6 +54,8 @@
.custom-emoji-tab__icon__text { .custom-emoji-tab__icon__text {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center;
justify-content: flex-start;
} }
.custom-emoji-tab { .custom-emoji-tab {

Просмотреть файл

@@ -186,7 +186,7 @@
flex-shrink: 0; flex-shrink: 0;
justify-content: space-between; justify-content: space-between;
padding: 0 12px; padding: 0 12px;
margin: 8px 0 8px 0; margin: 0 0 8px 0;
.emoji-picker__category { .emoji-picker__category {
display: inline-flex; display: inline-flex;
@@ -266,7 +266,7 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
border-color: transparent !important; border-color: transparent !important;
margin: 12px 12px 0 12px; margin: 12px;
.skin-tones-animation { .skin-tones-animation {
&-enter { &-enter {
@@ -296,6 +296,7 @@
height: 100%; height: 100%;
align-items: center; align-items: center;
background: var(--center-channel-bg); background: var(--center-channel-bg);
margin-inline-start: 8px;
&--active { &--active {
position: absolute; position: absolute;
@@ -351,10 +352,9 @@
.emoji-picker__text-container { .emoji-picker__text-container {
position: relative; position: relative;
z-index: 5; z-index: 5;
width: 284px; width: 100%;
height: 32px; height: 32px;
border: 1px solid rgba(var(--center-channel-color-rgb), 0.24); border: 1px solid rgba(var(--center-channel-color-rgb), 0.24);
margin-right: 8px;
border-radius: 4px; border-radius: 4px;
&:focus-within { &:focus-within {
@@ -456,8 +456,23 @@
outline: none; outline: none;
} }
} }
.gif-attribution {
padding: 5px 0 5px 5px;
margin-inline-start: 8px;
& > img {
width: 100%;
height: auto;
}
}
} }
$emoji-picker-footer-margin: 12px;
$emoji-footer-border-width: 1px;
$emoji-half-height: 33px;
$emoji-footer-height: $emoji-footer-border-width + $emoji-half-height + $emoji-picker-footer-margin * 2;
.emoji-picker__items { .emoji-picker__items {
position: relative; position: relative;
width: 100%; width: 100%;
@@ -469,6 +484,13 @@
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
&.gif-picker__items {
display: flex;
height: 380px;
justify-content: center;
padding: 12px 12px 0 12px;
}
.emoji-picker__container { .emoji-picker__container {
position: relative; position: relative;
min-width: 325px; min-width: 325px;
@@ -547,17 +569,12 @@
z-index: 99999; z-index: 99999;
} }
$emoji-picker-footer-margin: 12px;
$emoji-half-height: 33px;
.emoji-picker__footer { .emoji-picker__footer {
$border-width: 1px;
display: flex; display: flex;
min-height: $border-width + $emoji-half-height + $emoji-picker-footer-margin * 2; // prevents tiny layout shift when emoji replaces placeholder min-height: $emoji-footer-height; // prevents tiny layout shift when emoji replaces placeholder
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
border-top: solid $border-width rgba(61, 60, 64, 0.2); border-top: solid $emoji-footer-border-width rgba(61, 60, 64, 0.2);
} }
.emoji-picker__custom { .emoji-picker__custom {

Просмотреть файл

@@ -3,11 +3,8 @@
/* eslint-disable no-console, no-process-env */ /* eslint-disable no-console, no-process-env */
const fs = require('fs');
const path = require('path'); const path = require('path');
const url = require('url'); const url = require('url');
const CopyWebpackPlugin = require('copy-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin');
const ExternalTemplateRemotesPlugin = require('external-remotes-plugin'); const ExternalTemplateRemotesPlugin = require('external-remotes-plugin');
const webpack = require('webpack'); const webpack = require('webpack');
@@ -134,6 +131,9 @@ var config = {
'mattermost-redux/test': 'packages/mattermost-redux/test', 'mattermost-redux/test': 'packages/mattermost-redux/test',
'mattermost-redux': 'packages/mattermost-redux/src', 'mattermost-redux': 'packages/mattermost-redux/src',
'@mui/styled-engine': '@mui/styled-engine-sc', '@mui/styled-engine': '@mui/styled-engine-sc',
// This alias restricts single version of styled components acros all packages
'styled-components': path.resolve(__dirname, '..', 'node_modules', 'styled-components'),
}, },
extensions: ['.ts', '.tsx', '.js', '.jsx'], extensions: ['.ts', '.tsx', '.js', '.jsx'],
fallback: { fallback: {

1316
webapp/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@@ -123,6 +123,7 @@ export type ClientConfig = {
ForgotPasswordLink: string; ForgotPasswordLink: string;
GfycatAPIKey: string; GfycatAPIKey: string;
GfycatAPISecret: string; GfycatAPISecret: string;
GiphySdkKey: string;
GoogleDeveloperKey: string; GoogleDeveloperKey: string;
GuestAccountsEnforceMultifactorAuthentication: string; GuestAccountsEnforceMultifactorAuthentication: string;
HasImageProxy: string; HasImageProxy: string;
@@ -336,6 +337,7 @@ export type ServiceSettings = {
EnableGifPicker: boolean; EnableGifPicker: boolean;
GfycatAPIKey: string; GfycatAPIKey: string;
GfycatAPISecret: string; GfycatAPISecret: string;
GiphySdkKey: string;
PostEditTimeLimit: number; PostEditTimeLimit: number;
TimeBetweenUserTypingUpdatesMilliseconds: number; TimeBetweenUserTypingUpdatesMilliseconds: number;
EnablePostSearch: boolean; EnablePostSearch: boolean;