[FIX] Fix Icon picker in Mangane config, use emoji instead of legacy fa icons
This commit is contained in:
parent
c53454a919
commit
222072ec4e
9 changed files with 30 additions and 272 deletions
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||||
import { TransitionMotion, spring } from 'react-motion';
|
import { TransitionMotion, spring } from 'react-motion';
|
||||||
|
|
||||||
import { Icon } from 'soapbox/components/ui';
|
import { Icon } from 'soapbox/components/ui';
|
||||||
import EmojiPickerDropdown from 'soapbox/features/compose/containers/emoji_picker_dropdown_container';
|
import EmojiPickerDropdown from 'soapbox/containers/emoji_picker_dropdown_container';
|
||||||
import { useSettings } from 'soapbox/hooks';
|
import { useSettings } from 'soapbox/hooks';
|
||||||
|
|
||||||
import Reaction from './reaction';
|
import Reaction from './reaction';
|
||||||
|
|
|
@ -6,8 +6,8 @@ import React, { MouseEventHandler } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { IconButton } from 'soapbox/components/ui';
|
import { IconButton } from 'soapbox/components/ui';
|
||||||
import { isMobile } from 'soapbox/is_mobile';
|
|
||||||
import { useTheme } from 'soapbox/hooks';
|
import { useTheme } from 'soapbox/hooks';
|
||||||
|
import { isMobile } from 'soapbox/is_mobile';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||||
|
@ -53,15 +53,14 @@ const Wrapper: React.FC<IWrapper> = ({ target, show, onClose, children }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IEmojiPicker {
|
interface IEmojiPicker {
|
||||||
custom_emojis: ImmutableList<string>,
|
custom_emojis?: ImmutableList<string>,
|
||||||
onPickEmoji: Function,
|
|
||||||
onSkinTone: Function,
|
|
||||||
skinTone: number,
|
|
||||||
button?: React.ReactNode,
|
button?: React.ReactNode,
|
||||||
|
onPickEmoji: Function,
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmojiPickerUI : React.FC<IEmojiPicker> = ({
|
const EmojiPickerUI : React.FC<IEmojiPicker> = ({
|
||||||
custom_emojis,
|
custom_emojis = ImmutableList(),
|
||||||
|
button,
|
||||||
onPickEmoji,
|
onPickEmoji,
|
||||||
}) => {
|
}) => {
|
||||||
const root = React.useRef<HTMLDivElement>(null);
|
const root = React.useRef<HTMLDivElement>(null);
|
||||||
|
@ -112,14 +111,17 @@ const EmojiPickerUI : React.FC<IEmojiPicker> = ({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='relative' ref={root} onKeyDown={handleToggle}>
|
<div className='relative' ref={root} onKeyDown={handleToggle}>
|
||||||
<IconButton
|
<div role='button' tabIndex={0} onClick={handleToggle}>
|
||||||
|
{
|
||||||
|
button || <IconButton
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'text-gray-400 hover:text-gray-600': true,
|
'text-gray-400 hover:text-gray-600': true,
|
||||||
})}
|
})}
|
||||||
alt='😀'
|
alt='😀'
|
||||||
src={require('@tabler/icons/mood-happy.svg')}
|
src={require('@tabler/icons/mood-happy.svg')}
|
||||||
onClick={handleToggle}
|
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<Wrapper target={root} show={active} onClose={handleClose}>
|
<Wrapper target={root} show={active} onClose={handleClose}>
|
||||||
<Picker
|
<Picker
|
||||||
theme={theme}
|
theme={theme}
|
|
@ -1,8 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import { useEmoji } from '../../../actions/emojis';
|
import { useEmoji } from '../actions/emojis';
|
||||||
import { changeSetting } from '../../../actions/settings';
|
|
||||||
import EmojiPicker from '../components/emoji_picker';
|
import EmojiPicker from '../components/emoji_picker';
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@ import {
|
||||||
import { uploadMedia } from 'soapbox/actions/media';
|
import { uploadMedia } from 'soapbox/actions/media';
|
||||||
import IconButton from 'soapbox/components/icon_button';
|
import IconButton from 'soapbox/components/icon_button';
|
||||||
import UploadProgress from 'soapbox/components/upload-progress';
|
import UploadProgress from 'soapbox/components/upload-progress';
|
||||||
|
import EmojiPickerDropdown from 'soapbox/containers/emoji_picker_dropdown_container';
|
||||||
import UploadButton from 'soapbox/features/compose/components/upload_button';
|
import UploadButton from 'soapbox/features/compose/components/upload_button';
|
||||||
import EmojiPickerDropdown from 'soapbox/features/compose/containers/emoji_picker_dropdown_container';
|
|
||||||
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
||||||
import { truncateFilename } from 'soapbox/utils/media';
|
import { truncateFilename } from 'soapbox/utils/media';
|
||||||
|
|
||||||
|
|
|
@ -14,11 +14,11 @@ import Icon from 'soapbox/components/icon';
|
||||||
import { Button, Stack } from 'soapbox/components/ui';
|
import { Button, Stack } from 'soapbox/components/ui';
|
||||||
import { isMobile } from 'soapbox/is_mobile';
|
import { isMobile } from 'soapbox/is_mobile';
|
||||||
|
|
||||||
|
import EmojiPickerDropdown from '../../../containers/emoji_picker_dropdown_container';
|
||||||
import PollForm from '../components/polls/poll-form';
|
import PollForm from '../components/polls/poll-form';
|
||||||
import ReplyMentions from '../components/reply_mentions';
|
import ReplyMentions from '../components/reply_mentions';
|
||||||
import UploadForm from '../components/upload_form';
|
import UploadForm from '../components/upload_form';
|
||||||
import Warning from '../components/warning';
|
import Warning from '../components/warning';
|
||||||
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
|
|
||||||
import MarkdownButtonContainer from '../containers/markdown_button_container';
|
import MarkdownButtonContainer from '../containers/markdown_button_container';
|
||||||
import PollButtonContainer from '../containers/poll_button_container';
|
import PollButtonContainer from '../containers/poll_button_container';
|
||||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import IconPickerDropdown from './icon_picker_dropdown';
|
import EmojiPicker from 'soapbox/components/emoji_picker';
|
||||||
|
|
||||||
|
|
||||||
interface IIconPicker {
|
interface IIconPicker {
|
||||||
value: string,
|
value: string,
|
||||||
onChange: React.ChangeEventHandler,
|
onChange: React.ChangeEventHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const IconPicker: React.FC<IIconPicker> = ({ value, onChange }) => {
|
const IconPicker: React.FC<IIconPicker> = ({ value, onChange }) => {
|
||||||
return (
|
return (
|
||||||
<div className='mt-1 relative rounded-md shadow-sm dark:bg-slate-800 border border-solid border-gray-300 dark:border-gray-600 rounded-md'>
|
<div className='mt-1 relative rounded-md shadow-sm dark:bg-slate-800 border border-solid border-gray-300 dark:border-gray-600 rounded-md'>
|
||||||
<IconPickerDropdown value={value} onPickEmoji={onChange} />
|
<EmojiPicker button={<div className='grayscale h-[38px] w-[38px] text-lg flex items-center justify-center cursor-pointer'>{ value }</div>} onPickEmoji={onChange} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,245 +0,0 @@
|
||||||
import Picker from '@emoji-mart/react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
|
||||||
import Overlay from 'react-overlays/lib/Overlay';
|
|
||||||
|
|
||||||
import Icon from 'soapbox/components/icon';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
emoji: { id: 'icon_button.label', defaultMessage: 'Select icon' },
|
|
||||||
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search…' },
|
|
||||||
emoji_not_found: { id: 'icon_button.not_found', defaultMessage: 'No icons!! (╯°□°)╯︵ ┻━┻' },
|
|
||||||
custom: { id: 'icon_button.icons', defaultMessage: 'Icons' },
|
|
||||||
search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const backgroundImageFn = () => '';
|
|
||||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
|
||||||
|
|
||||||
const categoriesSort = ['custom'];
|
|
||||||
|
|
||||||
@injectIntl
|
|
||||||
class IconPickerMenu extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
custom_emojis: PropTypes.object,
|
|
||||||
loading: PropTypes.bool,
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
onPick: PropTypes.func.isRequired,
|
|
||||||
style: PropTypes.object,
|
|
||||||
placement: PropTypes.string,
|
|
||||||
arrowOffsetLeft: PropTypes.string,
|
|
||||||
arrowOffsetTop: PropTypes.string,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
style: {},
|
|
||||||
loading: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
modifierOpen: false,
|
|
||||||
placement: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleDocumentClick = e => {
|
|
||||||
if (this.node && !this.node.contains(e.target)) {
|
|
||||||
this.props.onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
setTimeout(() => {
|
|
||||||
document.addEventListener('click', this.handleDocumentClick, false);
|
|
||||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
document.removeEventListener('click', this.handleDocumentClick, false);
|
|
||||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
setRef = c => {
|
|
||||||
this.node = c;
|
|
||||||
|
|
||||||
if (!c) return;
|
|
||||||
|
|
||||||
// Nice and dirty hack to display the icons
|
|
||||||
c.querySelectorAll('button.emoji-mart-emoji > img').forEach(elem => {
|
|
||||||
const newIcon = document.createElement('span');
|
|
||||||
newIcon.innerHTML = `<i class="fa fa-${elem.parentNode.getAttribute('title')} fa-hack"></i>`;
|
|
||||||
elem.parentNode.replaceChild(newIcon, elem);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getI18n = () => {
|
|
||||||
const { intl } = this.props;
|
|
||||||
|
|
||||||
return {
|
|
||||||
search: intl.formatMessage(messages.emoji_search),
|
|
||||||
notfound: intl.formatMessage(messages.emoji_not_found),
|
|
||||||
categories: {
|
|
||||||
search: intl.formatMessage(messages.search_results),
|
|
||||||
custom: intl.formatMessage(messages.custom),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick = emoji => {
|
|
||||||
emoji.native = emoji.colons;
|
|
||||||
|
|
||||||
this.props.onClose();
|
|
||||||
this.props.onPick(emoji);
|
|
||||||
}
|
|
||||||
|
|
||||||
buildIcons = (customEmojis, autoplay = false) => {
|
|
||||||
const emojis = [];
|
|
||||||
|
|
||||||
Object.values(customEmojis).forEach(category => {
|
|
||||||
category.forEach(function(icon) {
|
|
||||||
const name = icon.replace('fa fa-', '');
|
|
||||||
if (icon !== 'email' && icon !== 'memo') {
|
|
||||||
emojis.push({
|
|
||||||
id: name,
|
|
||||||
name,
|
|
||||||
short_names: [name],
|
|
||||||
emoticons: [],
|
|
||||||
keywords: [name],
|
|
||||||
imageUrl: '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return emojis;
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { loading, style, intl, custom_emojis } = this.props;
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <div style={{ width: 299 }} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = { compressed: true, categories: [], aliases: [], emojis: [] };
|
|
||||||
const title = intl.formatMessage(messages.emoji);
|
|
||||||
const { modifierOpen } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames('font-icon-picker emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
|
|
||||||
<Picker
|
|
||||||
perLine={8}
|
|
||||||
emojiSize={22}
|
|
||||||
include={categoriesSort}
|
|
||||||
sheetSize={32}
|
|
||||||
custom={this.buildIcons(custom_emojis)}
|
|
||||||
color=''
|
|
||||||
emoji=''
|
|
||||||
set=''
|
|
||||||
title={title}
|
|
||||||
i18n={this.getI18n()}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
showPreview={false}
|
|
||||||
backgroundImageFn={backgroundImageFn}
|
|
||||||
emojiTooltip
|
|
||||||
noShowAnchors
|
|
||||||
data={data}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default @injectIntl
|
|
||||||
class IconPickerDropdown extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
onPickEmoji: PropTypes.func.isRequired,
|
|
||||||
value: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
active: false,
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
setRef = (c) => {
|
|
||||||
this.dropdown = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
onShowDropdown = ({ target }) => {
|
|
||||||
const { top } = target.getBoundingClientRect();
|
|
||||||
this.setState({active: true, placement: top * 2 < innerHeight ? 'bottom' : 'top' });
|
|
||||||
}
|
|
||||||
|
|
||||||
onHideDropdown = () => {
|
|
||||||
this.setState({ active: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggle = (e) => {
|
|
||||||
if (!this.state.loading && (!e.key || e.key === 'Enter')) {
|
|
||||||
if (this.state.active) {
|
|
||||||
this.onHideDropdown();
|
|
||||||
} else {
|
|
||||||
this.onShowDropdown(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyDown = e => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
this.onHideDropdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTargetRef = c => {
|
|
||||||
this.target = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
findTarget = () => {
|
|
||||||
return this.target;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { intl, onPickEmoji, value } = this.props;
|
|
||||||
const title = intl.formatMessage(messages.emoji);
|
|
||||||
const { active, loading, placement } = this.state;
|
|
||||||
const forkAwesomeIcons = require('../forkawesome.json');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div onKeyDown={this.handleKeyDown}>
|
|
||||||
<div
|
|
||||||
ref={this.setTargetRef}
|
|
||||||
className='h-[38px] w-[38px] text-lg flex items-center justify-center cursor-pointer'
|
|
||||||
title={title}
|
|
||||||
aria-label={title}
|
|
||||||
aria-expanded={active}
|
|
||||||
role='button'
|
|
||||||
onClick={this.onToggle}
|
|
||||||
onKeyDown={this.onToggle}
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<Icon id={value} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Overlay show={active} placement={placement} target={this.findTarget}>
|
|
||||||
<IconPickerMenu
|
|
||||||
custom_emojis={forkAwesomeIcons}
|
|
||||||
loading={loading}
|
|
||||||
onClose={this.onHideDropdown}
|
|
||||||
onPick={onPickEmoji}
|
|
||||||
/>
|
|
||||||
</Overlay>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -18,7 +18,7 @@ const PromoPanelInput: StreamfieldComponent<PromoPanelItem> = ({ value, onChange
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const handleIconChange = (icon: any) => {
|
const handleIconChange = (icon: any) => {
|
||||||
onChange(value.set('icon', icon.id));
|
onChange(value.set('icon', icon.native));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (key: 'text' | 'url'): React.ChangeEventHandler<HTMLInputElement> => {
|
const handleChange = (key: 'text' | 'url'): React.ChangeEventHandler<HTMLInputElement> => {
|
||||||
|
|
|
@ -1,26 +1,26 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Icon from 'soapbox/components/icon';
|
|
||||||
import { Widget, Stack, Text } from 'soapbox/components/ui';
|
import { Widget, Stack, Text } from 'soapbox/components/ui';
|
||||||
import { useAppSelector, useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
import { useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
||||||
|
|
||||||
const PromoPanel: React.FC = () => {
|
const PromoPanel: React.FC = () => {
|
||||||
const { promoPanel } = useSoapboxConfig();
|
const { promoPanel } = useSoapboxConfig();
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
|
||||||
const siteTitle = useAppSelector(state => state.instance.title);
|
|
||||||
const promoItems = promoPanel.get('items');
|
const promoItems = promoPanel.get('items');
|
||||||
const locale = settings.get('locale');
|
const locale = settings.get('locale');
|
||||||
|
|
||||||
if (!promoItems || promoItems.isEmpty()) return null;
|
if (!promoItems || promoItems.isEmpty()) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget title={siteTitle}>
|
<Widget title=''>
|
||||||
<Stack space={2}>
|
<Stack space={2}>
|
||||||
{promoItems.map((item, i) => (
|
{promoItems.map((item, i) => (
|
||||||
<Text key={i}>
|
<Text key={i}>
|
||||||
<a className='flex items-center' href={item.url} target='_blank'>
|
<a className='flex items-center' href={item.url} target='_blank'>
|
||||||
<Icon id={item.icon} className='flex-none text-lg mr-2' fixedWidth />
|
<div className='text-lg mr-2 grayscale'>
|
||||||
|
{ item.icon }
|
||||||
|
</div>
|
||||||
{item.textLocales.get(locale) || item.text}
|
{item.textLocales.get(locale) || item.text}
|
||||||
</a>
|
</a>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
Loading…
Reference in a new issue