@@ -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); |
@@ -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, | ||||