Add custom emojis to the emoji picker (#5052)
This commit is contained in:
parent
293972f716
commit
66126f3021
8 changed files with 91 additions and 2 deletions
|
@ -47,3 +47,43 @@ const emojify = (str, customEmojis = {}) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default emojify;
|
export default emojify;
|
||||||
|
|
||||||
|
export const toCodePoint = (unicodeSurrogates, sep = '-') => {
|
||||||
|
let r = [], c = 0, p = 0, i = 0;
|
||||||
|
|
||||||
|
while (i < unicodeSurrogates.length) {
|
||||||
|
c = unicodeSurrogates.charCodeAt(i++);
|
||||||
|
|
||||||
|
if (p) {
|
||||||
|
r.push((0x10000 + ((p - 0xD800) << 10) + (c - 0xDC00)).toString(16));
|
||||||
|
p = 0;
|
||||||
|
} else if (0xD800 <= c && c <= 0xDBFF) {
|
||||||
|
p = c;
|
||||||
|
} else {
|
||||||
|
r.push(c.toString(16));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.join(sep);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildCustomEmojis = customEmojis => {
|
||||||
|
const emojis = [];
|
||||||
|
|
||||||
|
customEmojis.forEach(emoji => {
|
||||||
|
const shortcode = emoji.get('shortcode');
|
||||||
|
const url = emoji.get('url');
|
||||||
|
const name = shortcode.replace(':', '');
|
||||||
|
|
||||||
|
emojis.push({
|
||||||
|
name,
|
||||||
|
short_names: [name],
|
||||||
|
text: '',
|
||||||
|
emoticons: [],
|
||||||
|
keywords: [name],
|
||||||
|
imageUrl: url,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return emojis;
|
||||||
|
};
|
||||||
|
|
|
@ -12,7 +12,7 @@ import Collapsable from '../../../components/collapsable';
|
||||||
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
||||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||||
import SensitiveButtonContainer from '../containers/sensitive_button_container';
|
import SensitiveButtonContainer from '../containers/sensitive_button_container';
|
||||||
import EmojiPickerDropdown from './emoji_picker_dropdown';
|
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
|
||||||
import UploadFormContainer from '../containers/upload_form_container';
|
import UploadFormContainer from '../containers/upload_form_container';
|
||||||
import WarningContainer from '../containers/warning_container';
|
import WarningContainer from '../containers/warning_container';
|
||||||
import { isMobile } from '../../../is_mobile';
|
import { isMobile } from '../../../is_mobile';
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
||||||
import { Overlay } from 'react-overlays';
|
import { Overlay } from 'react-overlays';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { buildCustomEmojis } from '../../../emoji';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||||
|
@ -130,6 +132,7 @@ class ModifierPicker extends React.PureComponent {
|
||||||
class EmojiPickerMenu extends React.PureComponent {
|
class EmojiPickerMenu extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
custom_emojis: ImmutablePropTypes.list,
|
||||||
loading: PropTypes.bool,
|
loading: PropTypes.bool,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
onPick: PropTypes.func.isRequired,
|
onPick: PropTypes.func.isRequired,
|
||||||
|
@ -194,6 +197,10 @@ class EmojiPickerMenu extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick = emoji => {
|
handleClick = emoji => {
|
||||||
|
if (!emoji.native) {
|
||||||
|
emoji.native = emoji.colons;
|
||||||
|
}
|
||||||
|
|
||||||
this.props.onClose();
|
this.props.onClose();
|
||||||
this.props.onPick(emoji);
|
this.props.onPick(emoji);
|
||||||
}
|
}
|
||||||
|
@ -225,6 +232,7 @@ class EmojiPickerMenu extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
|
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
|
custom={buildCustomEmojis(this.props.custom_emojis)}
|
||||||
perLine={8}
|
perLine={8}
|
||||||
emojiSize={22}
|
emojiSize={22}
|
||||||
sheetSize={32}
|
sheetSize={32}
|
||||||
|
@ -255,6 +263,7 @@ class EmojiPickerMenu extends React.PureComponent {
|
||||||
export default class EmojiPickerDropdown extends React.PureComponent {
|
export default class EmojiPickerDropdown extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
custom_emojis: ImmutablePropTypes.list,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
onPickEmoji: PropTypes.func.isRequired,
|
onPickEmoji: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -328,7 +337,12 @@ export default class EmojiPickerDropdown extends React.PureComponent {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Overlay show={active} placement='bottom' target={this.findTarget}>
|
<Overlay show={active} placement='bottom' target={this.findTarget}>
|
||||||
<EmojiPickerMenu loading={loading} onClose={this.onHideDropdown} onPick={onPickEmoji} />
|
<EmojiPickerMenu
|
||||||
|
custom_emojis={this.props.custom_emojis}
|
||||||
|
loading={loading}
|
||||||
|
onClose={this.onHideDropdown}
|
||||||
|
onPick={onPickEmoji}
|
||||||
|
/>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import EmojiPickerDropdown from '../components/emoji_picker_dropdown';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
custom_emojis: state.get('custom_emojis'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(EmojiPickerDropdown);
|
13
app/javascript/mastodon/reducers/custom_emojis.js
Normal file
13
app/javascript/mastodon/reducers/custom_emojis.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
import { STORE_HYDRATE } from '../actions/store';
|
||||||
|
|
||||||
|
const initialState = ImmutableList();
|
||||||
|
|
||||||
|
export default function statuses(state = initialState, action) {
|
||||||
|
switch(action.type) {
|
||||||
|
case STORE_HYDRATE:
|
||||||
|
return action.state.get('custom_emojis');
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
|
@ -20,6 +20,7 @@ import search from './search';
|
||||||
import media_attachments from './media_attachments';
|
import media_attachments from './media_attachments';
|
||||||
import notifications from './notifications';
|
import notifications from './notifications';
|
||||||
import height_cache from './height_cache';
|
import height_cache from './height_cache';
|
||||||
|
import custom_emojis from './custom_emojis';
|
||||||
|
|
||||||
const reducers = {
|
const reducers = {
|
||||||
timelines,
|
timelines,
|
||||||
|
@ -43,6 +44,7 @@ const reducers = {
|
||||||
media_attachments,
|
media_attachments,
|
||||||
notifications,
|
notifications,
|
||||||
height_cache,
|
height_cache,
|
||||||
|
custom_emojis,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default combineReducers(reducers);
|
export default combineReducers(reducers);
|
||||||
|
|
|
@ -2629,6 +2629,12 @@ button.icon-button.active i.fa-retweet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emoji-mart-emoji {
|
||||||
|
span {
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.upload-area {
|
.upload-area {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: rgba($base-overlay-background, 0.8);
|
background: rgba($base-overlay-background, 0.8);
|
||||||
|
|
|
@ -4,6 +4,12 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
attributes :meta, :compose, :accounts,
|
attributes :meta, :compose, :accounts,
|
||||||
:media_attachments, :settings, :push_subscription
|
:media_attachments, :settings, :push_subscription
|
||||||
|
|
||||||
|
has_many :custom_emojis, serializer: REST::CustomEmojiSerializer
|
||||||
|
|
||||||
|
def custom_emojis
|
||||||
|
CustomEmoji.local
|
||||||
|
end
|
||||||
|
|
||||||
def meta
|
def meta
|
||||||
store = {
|
store = {
|
||||||
streaming_api_base_url: Rails.configuration.x.streaming_api_base_url,
|
streaming_api_base_url: Rails.configuration.x.streaming_api_base_url,
|
||||||
|
|
Loading…
Reference in a new issue