fix reactionpicker mobile ui
This commit is contained in:
parent
ba2953bc9d
commit
e5790822ee
1 changed files with 268 additions and 0 deletions
268
app/javascript/mastodon/features/ui/components/reaction_modal.js
Normal file
268
app/javascript/mastodon/features/ui/components/reaction_modal.js
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import StatusContent from '../../../components/status_content';
|
||||||
|
import Avatar from '../../../components/avatar';
|
||||||
|
import RelativeTimestamp from '../../../components/relative_timestamp';
|
||||||
|
import DisplayName from '../../../components/display_name';
|
||||||
|
import IconButton from '../../../components/icon_button';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
|
import { EmojiPicker as EmojiPickerAsync } from '../util/async-components';
|
||||||
|
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
|
||||||
|
import { assetHost } from 'mastodon/utils/config';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||||
|
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
|
||||||
|
emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emojos!! (╯°□°)╯︵ ┻━┻' },
|
||||||
|
custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' },
|
||||||
|
recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' },
|
||||||
|
search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
|
||||||
|
people: { id: 'emoji_button.people', defaultMessage: 'People' },
|
||||||
|
nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' },
|
||||||
|
food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' },
|
||||||
|
activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' },
|
||||||
|
travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' },
|
||||||
|
objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' },
|
||||||
|
symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' },
|
||||||
|
flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
|
||||||
|
});
|
||||||
|
|
||||||
|
let EmojiPicker, Emoji; // load asynchronously
|
||||||
|
|
||||||
|
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||||
|
|
||||||
|
const notFoundFn = () => (
|
||||||
|
<div className='emoji-mart-no-results'>
|
||||||
|
<Emoji
|
||||||
|
emoji='sleuth_or_spy'
|
||||||
|
set='twitter'
|
||||||
|
size={32}
|
||||||
|
sheetSize={32}
|
||||||
|
backgroundImageFn={backgroundImageFn}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='emoji-mart-no-results-label'>
|
||||||
|
<FormattedMessage id='emoji_mart_button.not_found' defaultMessage='No matching emojis found' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
@injectIntl
|
||||||
|
class ReactionPicker extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
custom_emojis: ImmutablePropTypes.list,
|
||||||
|
onPickEmoji: PropTypes.func.isRequired,
|
||||||
|
onSkinTone: PropTypes.func.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
skinTone: PropTypes.number.isRequired,
|
||||||
|
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
loading: PropTypes.bool,
|
||||||
|
style: PropTypes.object,
|
||||||
|
placement: PropTypes.string,
|
||||||
|
arrowOffsetLeft: PropTypes.string,
|
||||||
|
arrowOffsetTop: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
style: {},
|
||||||
|
loading: true,
|
||||||
|
frequentlyUsedEmojis: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
modifierOpen: false,
|
||||||
|
placement: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDocumentClick = e => {
|
||||||
|
if (this.node && !this.node.contains(e.target)) {
|
||||||
|
this.props.onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
document.addEventListener('click', this.handleDocumentClick, false);
|
||||||
|
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
document.removeEventListener('click', this.handleDocumentClick, false);
|
||||||
|
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.node = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
recent: intl.formatMessage(messages.recent),
|
||||||
|
people: intl.formatMessage(messages.people),
|
||||||
|
nature: intl.formatMessage(messages.nature),
|
||||||
|
foods: intl.formatMessage(messages.food),
|
||||||
|
activity: intl.formatMessage(messages.activity),
|
||||||
|
places: intl.formatMessage(messages.travel),
|
||||||
|
objects: intl.formatMessage(messages.objects),
|
||||||
|
symbols: intl.formatMessage(messages.symbols),
|
||||||
|
flags: intl.formatMessage(messages.flags),
|
||||||
|
custom: intl.formatMessage(messages.custom),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick = (emoji, event) => {
|
||||||
|
if (!emoji.native) {
|
||||||
|
emoji.native = emoji.colons;
|
||||||
|
}
|
||||||
|
if (!(event.ctrlKey || event.metaKey)) {
|
||||||
|
this.props.onClose();
|
||||||
|
}
|
||||||
|
this.props.onPickEmoji(emoji);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleModifierOpen = () => {
|
||||||
|
this.setState({ modifierOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleModifierClose = () => {
|
||||||
|
this.setState({ modifierOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleModifierChange = modifier => {
|
||||||
|
this.props.onSkinTone(modifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div style={{ width: '100%' ,height: '50%'}} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = intl.formatMessage(messages.emoji);
|
||||||
|
|
||||||
|
const { modifierOpen } = this.state;
|
||||||
|
|
||||||
|
const categoriesSort = [
|
||||||
|
'recent',
|
||||||
|
'people',
|
||||||
|
'nature',
|
||||||
|
'foods',
|
||||||
|
'activity',
|
||||||
|
'places',
|
||||||
|
'objects',
|
||||||
|
'symbols',
|
||||||
|
'flags',
|
||||||
|
];
|
||||||
|
|
||||||
|
categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(custom_emojis)).sort());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmojiPicker
|
||||||
|
perLine={8}
|
||||||
|
emojiSize={22}
|
||||||
|
sheetSize={32}
|
||||||
|
custom={buildCustomEmojis(custom_emojis)}
|
||||||
|
color=''
|
||||||
|
emoji=''
|
||||||
|
set='twitter'
|
||||||
|
title={title}
|
||||||
|
i18n={this.getI18n()}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
include={categoriesSort}
|
||||||
|
recent={frequentlyUsedEmojis}
|
||||||
|
skin={skinTone}
|
||||||
|
showPreview={false}
|
||||||
|
showSkinTones={false}
|
||||||
|
notFound={notFoundFn}
|
||||||
|
autoFocus
|
||||||
|
emojiTooltip
|
||||||
|
style={{width: '100%'}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ReactionModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
status: ImmutablePropTypes.map,
|
||||||
|
custom_emojis: ImmutablePropTypes.list,
|
||||||
|
onPickEmoji: PropTypes.func.isRequired,
|
||||||
|
onSkinTone: PropTypes.func.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
skinTone: PropTypes.number.isRequired,
|
||||||
|
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
loading: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.setState({ loading: true });
|
||||||
|
EmojiPickerAsync().then(EmojiMart => {
|
||||||
|
EmojiPicker = EmojiMart.Picker;
|
||||||
|
Emoji = EmojiMart.Emoji;
|
||||||
|
this.setState({ loading: false });
|
||||||
|
}).catch(() => {
|
||||||
|
this.setState({ loading: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { custom_emojis, onPickEmoji, onSkinTone, onClose, skinTone, frequentlyUsedEmojis } = this.props;
|
||||||
|
const loading = this.state.loading;
|
||||||
|
const status = this.props.status && (
|
||||||
|
<div className='status light'>
|
||||||
|
<div className='boost-modal__status-header'>
|
||||||
|
<div className='boost-modal__status-time'>
|
||||||
|
<a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
||||||
|
<RelativeTimestamp timestamp={this.props.status.get('created_at')} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href={this.props.status.getIn(['account', 'url'])} className='status__display-name'>
|
||||||
|
<div className='status__avatar'>
|
||||||
|
<Avatar account={this.props.status.get('account')} size={48} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DisplayName account={this.props.status.get('account')} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StatusContent status={this.props.status} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal reaction-modal'>
|
||||||
|
{status}
|
||||||
|
<ReactionPicker
|
||||||
|
custom_emojis={custom_emojis}
|
||||||
|
loading={loading}
|
||||||
|
onClose={onClose}
|
||||||
|
onPickEmoji={onPickEmoji}
|
||||||
|
onSkinTone={onSkinTone}
|
||||||
|
skinTone={skinTone}
|
||||||
|
frequentlyUsedEmojis={frequentlyUsedEmojis}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue