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>
This commit is contained in:
M-ZubairAhmed 2023-08-19 01:32:46 +05:30 committed by GitHub
parent 8418eefb75
commit bc11b29807
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1255 additions and 593 deletions

View File

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

View File

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

View File

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

View File

@ -8,6 +8,8 @@
"dependencies": {
"@floating-ui/react-dom": "1.0.0",
"@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",
"@mattermost/client": "*",
"@mattermost/compass-components": "^0.2.12",

View File

@ -6100,36 +6100,7 @@ const AdminDefinition = {
label: t('admin.customization.enableGifPickerTitle'),
label_default: 'Enable GIF Picker:',
help_text: t('admin.customization.enableGifPickerDesc'),
help_text_default: 'Allow users to select GIFs from the emoji picker via a Gfycat integration.',
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.',
help_text_default: 'Allows users to select GIFs from the emoji picker.',
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.INTEGRATIONS.GIF)),
},
],

View File

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

View File

@ -1,14 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// 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 {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 EmojiPicker from 'components/emoji_picker';
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';
@ -32,6 +33,8 @@ type State = {
}
export default class EmojiPickerTabs extends PureComponent<Props, State> {
private rootPickerNodeRef: RefObject<HTMLDivElement>;
static defaultProps = {
rightOffset: 0,
topOffset: 0,
@ -45,20 +48,10 @@ export default class EmojiPickerTabs extends PureComponent<Props, State> {
emojiTabVisible: true,
filter: '',
};
this.rootPickerNodeRef = createRef();
}
handleEnterEmojiTab = () => {
this.setState({
emojiTabVisible: true,
});
};
handleExitEmojiTab = () => {
this.setState({
emojiTabVisible: false,
});
};
handleEmojiPickerClose = () => {
this.props.onEmojiClose();
};
@ -67,6 +60,10 @@ export default class EmojiPickerTabs extends PureComponent<Props, State> {
this.setState({filter});
};
getRootPickerNode = () => {
return this.rootPickerNodeRef.current;
};
render() {
let pickerStyle;
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') {
return (
<Tabs
defaultActiveKey={1}
id='emoji-picker-tabs'
<div
ref={this.rootPickerNodeRef}
style={pickerStyle}
className={pickerClass}
justified={true}
>
<EmojiPickerHeader handleEmojiPickerClose={this.handleEmojiPickerClose}/>
<Tab
eventKey={1}
onEnter={this.handleEnterEmojiTab}
onExit={this.handleExitEmojiTab}
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/>}
<Tabs
id='emoji-picker-tabs'
defaultActiveKey={1}
justified={true}
mountOnEnter={true}
unmountOnExit={true}
tabClassName={'custom-emoji-tab'}
>
<GifPicker
onGifClick={this.props.onGifClick}
defaultSearchText={this.state.filter}
handleSearchTextChange={this.handleFilterChange}
/>
</Tab>
</Tabs>
<EmojiPickerHeader handleEmojiPickerClose={this.handleEmojiPickerClose}/>
<Tab
eventKey={1}
title={
<div className={'custom-emoji-tab__icon__text'}>
<EmojiIcon
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}/>
<EmojiPicker
filter={this.state.filter}
visible={this.state.emojiTabVisible}
onEmojiClick={this.props.onEmojiClick}
handleFilterChange={this.handleFilterChange}
handleEmojiPickerClose={this.handleEmojiPickerClose}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);

View File

@ -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);

View File

@ -1,90 +1,48 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// 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';
import Categories from 'components/gif_picker/components/Categories';
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,
},
};
const GIF_DEFAULT_WIDTH = 350;
const GIF_MARGIN_ENDS = 12;
type Props = {
filter: string;
onGifClick?: (gif: string) => void;
defaultSearchText?: string;
handleSearchTextChange: (text: string) => void;
handleFilterChange: (filter: string) => void;
getRootPickerNode: () => HTMLDivElement | null;
}
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 handleCategories = () => setAction('reactions');
const handleSearch = () => setAction('search');
const imageWithMarkdown = `![${gif.title}](${gif.images.fixed_height.url})`;
props.onGifClick(imageWithMarkdown);
}
}, [props.onGifClick]);
const handleItemClick = (gif: GfycatAPIItem) => {
props.onGifClick?.('![' + gif.title?.replace(/]|\[/g, '\\$&') + '](' + gif.max5mbGif + ')');
};
const pickerWidth = useMemo(() => {
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 (
<div>
<App
appProps={appProps}
action={action}
onTrending={handleTrending}
onCategories={handleCategories}
onSearch={handleSearch}
defaultSearchText={props.defaultSearchText}
handleSearchTextChange={props.handleSearchTextChange}
>
{component}
</App>
<GifPickerSearch
value={props.filter}
onChange={props.handleFilterChange}
/>
<GifPickerItems
width={pickerWidth}
filter={props.filter}
onClick={handleItemClick}
/>
</div>
);
};

View File

@ -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;

View File

@ -653,7 +653,7 @@
"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.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.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:",
@ -665,10 +665,6 @@
"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.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.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.",
@ -3354,6 +3350,8 @@
"email_verify.return": "Return to log in",
"email_verify.sending": "Sending email…",
"email_verify.sent": "Verification email sent",
"emoji_gif_picker.tabs.emojis": "Emojis",
"emoji_gif_picker.tabs.gifs": "GIFs",
"emoji_list.actions": "Actions",
"emoji_list.add": "Add Custom Emoji",
"emoji_list.creator": "Creator",
@ -3618,7 +3616,10 @@
"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.title": "Copy Public Link",
"gif_picker.attribution.alt": "Powered by GIPHY",
"gif_picker.gfycat": "Search Gfycat",
"gif_picker.input.label": "Search for GIFs",
"gif_picker.input.placeholder": "Search GIPHY",
"global_header.productSettings": "Settings",
"global_header.productSwitchMenu": "Product switch menu",
"globalThreads.heading": "Followed threads",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {GiphyFetch} from '@giphy/js-fetch-api';
import {createSelector} from 'mattermost-redux/selectors/create_selector';
import {General} from 'mattermost-redux/constants';
@ -112,3 +114,16 @@ export const isMarketplaceEnabled: (state: GlobalState) => boolean = createSelec
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;
},
);

View File

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

View File

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

View File

@ -3,11 +3,8 @@
/* eslint-disable no-console, no-process-env */
const fs = require('fs');
const path = require('path');
const url = require('url');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ExternalTemplateRemotesPlugin = require('external-remotes-plugin');
const webpack = require('webpack');
@ -134,6 +131,9 @@ var config = {
'mattermost-redux/test': 'packages/mattermost-redux/test',
'mattermost-redux': 'packages/mattermost-redux/src',
'@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'],
fallback: {

1316
webapp/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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