diff options
author | Eugen Rochko <eugen@zeonfederated.com> | 2017-09-23 01:41:00 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-09-23 01:41:00 +0200 |
commit | 846cd4e8381c891816cf814582304b534db4ee5f (patch) | |
tree | fb7c718bd3071bf5128f6c270901623aab54a0f0 /app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js | |
parent | 0de82dd316839ed329504bfbf9bd0f2d3d96e654 (diff) |
Switch from EmojiOne to Twemoji, different emoji picker (#5046)
* Switch from EmojiOne to Twemoji, different emoji picker * Make emoji-mart use a local spritesheet * Fix emojify test * yarn manage:translations
Diffstat (limited to 'app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js')
-rw-r--r-- | app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js | 324 |
1 files changed, 258 insertions, 66 deletions
diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index 9d05b7a34..43e175be5 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -1,12 +1,17 @@ import React from 'react'; -import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; +import { Overlay } from 'react-overlays'; +import classNames from 'classnames'; 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' }, @@ -17,13 +22,234 @@ const messages = defineMessages({ flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' }, }); -const settings = { - imageType: 'png', - sprites: false, - imagePathPNG: '/emoji/', -}; +const assetHost = process.env.CDN_HOST || ''; -let EmojiPicker; // load asynchronously +let EmojiPicker, Emoji; // load asynchronously + +const backgroundImageFn = () => `${assetHost}/emoji/sheet.png`; + +class ModifierPickerMenu extends React.PureComponent { + + static propTypes = { + active: PropTypes.bool, + onSelect: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + }; + + handleClick = (e) => { + const modifier = [].slice.call(e.currentTarget.parentNode.children).indexOf(e.target) + 1; + this.props.onSelect(modifier); + } + + componentWillReceiveProps (nextProps) { + if (nextProps.active) { + this.attachListeners(); + } else { + this.removeListeners(); + } + } + + componentWillUnmount () { + this.removeListeners(); + } + + handleDocumentClick = e => { + if (this.node && !this.node.contains(e.target)) { + this.props.onClose(); + } + } + + attachListeners () { + document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('touchend', this.handleDocumentClick, false); + } + + removeListeners () { + document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('touchend', this.handleDocumentClick, false); + } + + setRef = c => { + this.node = c; + } + + render () { + const { active } = this.props; + + return ( + <div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}> + <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button> + <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button> + <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button> + <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button> + <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button> + <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button> + </div> + ); + } + +} + +class ModifierPicker extends React.PureComponent { + + static propTypes = { + active: PropTypes.bool, + modifier: PropTypes.number, + onChange: PropTypes.func, + onClose: PropTypes.func, + onOpen: PropTypes.func, + }; + + handleClick = () => { + if (this.props.active) { + this.props.onClose(); + } else { + this.props.onOpen(); + } + } + + handleSelect = modifier => { + this.props.onChange(modifier); + this.props.onClose(); + } + + render () { + const { active, modifier } = this.props; + + return ( + <div className='emoji-picker-dropdown__modifiers'> + <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} /> + <ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} /> + </div> + ); + } + +} + +@injectIntl +class EmojiPickerMenu extends React.PureComponent { + + static propTypes = { + 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, + placement: 'bottom', + }; + + state = { + modifierOpen: false, + modifier: 1, + }; + + 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, false); + } + + componentWillUnmount () { + document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('touchend', this.handleDocumentClick, false); + } + + 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 => { + this.props.onClose(); + this.props.onPick(emoji); + } + + handleModifierOpen = () => { + this.setState({ modifierOpen: true }); + } + + handleModifierClose = () => { + this.setState({ modifierOpen: false }); + } + + handleModifierChange = modifier => { + if (modifier !== this.state.modifier) { + this.setState({ modifier }); + } + } + + render () { + const { loading, style, intl } = this.props; + + if (loading) { + return <div style={{ width: 299 }} />; + } + + const title = intl.formatMessage(messages.emoji); + const { modifierOpen, modifier } = this.state; + + return ( + <div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}> + <EmojiPicker + perLine={8} + emojiSize={22} + sheetSize={32} + color='' + emoji='' + set='twitter' + title={title} + i18n={this.getI18n()} + onClick={this.handleClick} + skin={modifier} + backgroundImageFn={backgroundImageFn} + /> + + <ModifierPicker + active={modifierOpen} + modifier={modifier} + onOpen={this.handleModifierOpen} + onClose={this.handleModifierClose} + onChange={this.handleModifierChange} + /> + </div> + ); + } + +} @injectIntl export default class EmojiPickerDropdown extends React.PureComponent { @@ -42,20 +268,17 @@ export default class EmojiPickerDropdown extends React.PureComponent { this.dropdown = c; } - handleChange = (data) => { - this.dropdown.hide(); - this.props.onPickEmoji(data); - } - onShowDropdown = () => { this.setState({ active: true }); + if (!EmojiPicker) { this.setState({ loading: true }); - EmojiPickerAsync().then(TheEmojiPicker => { - EmojiPicker = TheEmojiPicker.default; + + EmojiPickerAsync().then(EmojiMart => { + EmojiPicker = EmojiMart.Picker; + Emoji = EmojiMart.Emoji; this.setState({ loading: false }); }).catch(() => { - // TODO: show the user an error? this.setState({ loading: false }); }); } @@ -75,70 +298,39 @@ export default class EmojiPickerDropdown extends React.PureComponent { } } - onEmojiPickerKeyDown = (e) => { + handleKeyDown = e => { if (e.key === 'Escape') { this.onHideDropdown(); } } - render () { - const { intl } = this.props; + setTargetRef = c => { + this.target = c; + } - const categories = { - people: { - title: intl.formatMessage(messages.people), - emoji: 'smile', - }, - nature: { - title: intl.formatMessage(messages.nature), - emoji: 'hamster', - }, - food: { - title: intl.formatMessage(messages.food), - emoji: 'pizza', - }, - activity: { - title: intl.formatMessage(messages.activity), - emoji: 'soccer', - }, - travel: { - title: intl.formatMessage(messages.travel), - emoji: 'earth_americas', - }, - objects: { - title: intl.formatMessage(messages.objects), - emoji: 'bulb', - }, - symbols: { - title: intl.formatMessage(messages.symbols), - emoji: 'clock9', - }, - flags: { - title: intl.formatMessage(messages.flags), - emoji: 'flag_gb', - }, - }; + findTarget = () => { + return this.target; + } - const { active, loading } = this.state; + render () { + const { intl, onPickEmoji } = this.props; const title = intl.formatMessage(messages.emoji); + const { active, loading } = this.state; return ( - <Dropdown ref={this.setRef} className='emoji-picker__dropdown' active={active && !loading} onShow={this.onShowDropdown} onHide={this.onHideDropdown}> - <DropdownTrigger className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onKeyDown={this.onToggle} tabIndex={0} > + <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}> + <div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}> <img - className={`emojione ${active && loading ? 'pulse-loading' : ''}`} + className={classNames('emojione', { 'pulse-loading': active && loading })} alt='🙂' - src='/emoji/1f602.svg' + src={`${assetHost}/emoji/1f602.svg`} /> - </DropdownTrigger> - - <DropdownContent className='dropdown__left'> - { - this.state.active && !this.state.loading && - (<EmojiPicker emojione={settings} onChange={this.handleChange} searchPlaceholder={intl.formatMessage(messages.emoji_search)} onKeyDown={this.onEmojiPickerKeyDown} categories={categories} search />) - } - </DropdownContent> - </Dropdown> + </div> + + <Overlay show={active} placement='bottom' target={this.findTarget}> + <EmojiPickerMenu loading={loading} onClose={this.onHideDropdown} onPick={onPickEmoji} /> + </Overlay> + </div> ); } |