mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
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:
parent
8418eefb75
commit
bc11b29807
@ -129,6 +129,7 @@ const defaultServerConfig: AdminConfig = {
|
||||
EnableGifPicker: true,
|
||||
GfycatAPIKey: '2_KtH_W5',
|
||||
GfycatAPISecret: '3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof',
|
||||
GiphySdkKey: 's0glxvzVg9azvPipKxcPLpXV0q1x1fVP',
|
||||
EnableCustomEmoji: true,
|
||||
EnableEmojiPicker: true,
|
||||
PostEditTimeLimit: -1,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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)),
|
||||
},
|
||||
],
|
||||
|
@ -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(() => {
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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});
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
// 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 = ``;
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
28
webapp/channels/src/components/widgets/icons/giphy_icon.tsx
Normal file
28
webapp/channels/src/components/widgets/icons/giphy_icon.tsx
Normal 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;
|
@ -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",
|
||||
|
BIN
webapp/channels/src/images/gif_picker/powered-by-giphy-black.png
Normal file
BIN
webapp/channels/src/images/gif_picker/powered-by-giphy-black.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
webapp/channels/src/images/gif_picker/powered-by-giphy-white.png
Normal file
BIN
webapp/channels/src/images/gif_picker/powered-by-giphy-white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
@ -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;
|
||||
},
|
||||
);
|
||||
|
@ -54,6 +54,8 @@
|
||||
.custom-emoji-tab__icon__text {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.custom-emoji-tab {
|
||||
|
@ -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 {
|
||||
|
@ -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
1316
webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user