Fix #372 - Emoji picker

This commit is contained in:
Eugen Rochko
2017-03-02 00:57:55 +01:00
parent 6a1b738e0b
commit 89fc2d7f48
8 changed files with 315 additions and 11 deletions

View File

@ -28,6 +28,8 @@ export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
export function changeCompose(text) {
return {
type: COMPOSE_CHANGE,
@ -260,3 +262,11 @@ export function changeComposeListability(checked) {
checked
};
};
export function insertEmojiCompose(position, emoji) {
return {
type: COMPOSE_EMOJI_INSERT,
position,
emoji
};
};

View File

@ -15,6 +15,7 @@ import UnlistedToggleContainer from '../containers/unlisted_toggle_container';
import SpoilerToggleContainer from '../containers/spoiler_toggle_container';
import PrivateToggleContainer from '../containers/private_toggle_container';
import SensitiveToggleContainer from '../containers/sensitive_toggle_container';
import EmojiPickerDropdown from './emoji_picker_dropdown';
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
@ -48,6 +49,7 @@ const ComposeForm = React.createClass({
onSuggestionSelected: React.PropTypes.func.isRequired,
onChangeSpoilerText: React.PropTypes.func.isRequired,
onPaste: React.PropTypes.func.isRequired,
onPickEmoji: React.PropTypes.func.isRequired
},
mixins: [PureRenderMixin],
@ -76,6 +78,7 @@ const ComposeForm = React.createClass({
},
onSuggestionSelected (tokenStart, token, value) {
this._restoreCaret = null;
this.props.onSuggestionSelected(tokenStart, token, value);
},
@ -88,8 +91,18 @@ const ComposeForm = React.createClass({
// If replying to zero or one users, places the cursor at the end of the textbox.
// If replying to more than one user, selects any usernames past the first;
// this provides a convenient shortcut to drop everyone else from the conversation.
const selectionEnd = this.props.text.length;
const selectionStart = (this.props.preselectDate !== prevProps.preselectDate) ? (this.props.text.search(/\s/) + 1) : selectionEnd;
let selectionEnd, selectionStart;
if (this.props.preselectDate !== prevProps.preselectDate) {
selectionEnd = this.props.text.length;
selectionStart = this.props.text.search(/\s/) + 1;
} else if (typeof this._restoreCaret === 'number') {
selectionStart = this._restoreCaret;
selectionEnd = this._restoreCaret;
} else {
selectionEnd = this.props.text.length;
selectionStart = selectionEnd;
}
this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
this.autosuggestTextarea.textarea.focus();
@ -100,6 +113,12 @@ const ComposeForm = React.createClass({
this.autosuggestTextarea = c;
},
handleEmojiPick (data) {
const position = this.autosuggestTextarea.textarea.selectionStart;
this._restoreCaret = position + data.shortname.length + 1;
this.props.onPickEmoji(position, data);
},
render () {
const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props;
const disabled = this.props.is_submitting || this.props.is_uploading;
@ -156,7 +175,10 @@ const ComposeForm = React.createClass({
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
<div style={{ float: 'right' }}><Button text={publishText} onClick={this.handleSubmit} disabled={disabled} /></div>
<div style={{ float: 'right', marginRight: '16px', lineHeight: '36px' }}><CharacterCounter max={500} text={[this.props.spoiler_text, this.props.text].join('')} /></div>
<UploadButtonContainer style={{ paddingTop: '4px' }} />
<div style={{ display: 'flex', paddingTop: '4px' }}>
<UploadButtonContainer />
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
</div>
</div>
<SpoilerToggleContainer />

View File

@ -0,0 +1,52 @@
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
import EmojiPicker from 'emojione-picker';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
emoji: { id: 'emoji_button.label', defaultMessage: 'Emoji' }
});
const settings = {
imageType: 'png',
sprites: false,
imagePathPNG: '/emoji/'
};
const EmojiPickerDropdown = React.createClass({
propTypes: {
intl: React.PropTypes.object.isRequired,
onPickEmoji: React.PropTypes.func.isRequired
},
mixins: [PureRenderMixin],
setRef (c) {
this.dropdown = c;
},
handleChange (data) {
this.dropdown.hide();
this.props.onPickEmoji(data);
},
render () {
const { intl } = this.props;
return (
<Dropdown ref={this.setRef} style={{ marginLeft: '5px' }}>
<DropdownTrigger className='icon-button' title={intl.formatMessage(messages.emoji)} style={{ fontSize: `24px`, width: `24px`, lineHeight: `24px`, marginTop: '-1px', display: 'block', marginLeft: '2px' }}>
<i className={`fa fa-smile-o`} style={{ verticalAlign: 'middle' }} />
</DropdownTrigger>
<DropdownContent>
<EmojiPicker emojione={settings} onChange={this.handleChange} />
</DropdownContent>
</Dropdown>
);
}
});
export default injectIntl(EmojiPickerDropdown);

View File

@ -9,6 +9,7 @@ import {
fetchComposeSuggestions,
selectComposeSuggestion,
changeComposeSpoilerText,
insertEmojiCompose
} from '../../../actions/compose';
const getMentionedUsernames = createSelector(state => state.getIn(['compose', 'text']), text => text.match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig));
@ -70,6 +71,10 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(uploadCompose(files));
},
onPickEmoji (position, data) {
dispatch(insertEmojiCompose(position, data));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ComposeForm);

View File

@ -20,7 +20,8 @@ import {
COMPOSE_SPOILERNESS_CHANGE,
COMPOSE_SPOILER_TEXT_CHANGE,
COMPOSE_VISIBILITY_CHANGE,
COMPOSE_LISTABILITY_CHANGE
COMPOSE_LISTABILITY_CHANGE,
COMPOSE_EMOJI_INSERT
} from '../actions/compose';
import { TIMELINE_DELETE } from '../actions/timelines';
import { STORE_HYDRATE } from '../actions/store';
@ -105,6 +106,15 @@ const insertSuggestion = (state, position, token, completion) => {
});
};
const insertEmoji = (state, position, emojiData) => {
const emoji = emojiData.shortname;
return state.withMutations(map => {
map.update('text', oldText => `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`);
map.set('focusDate', new Date());
});
};
export default function compose(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:
@ -177,6 +187,8 @@ export default function compose(state = initialState, action) {
} else {
return state;
}
case COMPOSE_EMOJI_INSERT:
return insertEmoji(state, action.position, action.emoji);
default:
return state;
}

View File

@ -65,6 +65,10 @@
}
}
.dropdown--active .icon-button {
color: $color4;
}
.invisible {
font-size: 0;
line-height: 0;
@ -547,7 +551,7 @@ a.status__content__spoiler-link {
left: 8px;
}
ul {
& > ul {
list-style: none;
background: $color2;
padding: 4px 0;
@ -559,12 +563,12 @@ a.status__content__spoiler-link {
}
&.dropdown__left {
ul {
& > ul {
left: -98px;
}
}
a {
& > ul > li > a {
font-size: 13px;
line-height: 18px;
display: block;
@ -1254,3 +1258,164 @@ button.active i.fa-retweet {
z-index: 1;
background: radial-gradient(ellipse, rgba($color4, 0.23) 0%, rgba($color4, 0) 60%);
}
.emoji-dialog {
width: 280px;
height: 220px;
background: $color2;
box-sizing: border-box;
border-radius: 2px;
overflow: hidden;
position: relative;
box-shadow: 0 0 15px rgba($color8, 0.4);
.emojione {
margin: 0;
}
.emoji-dialog-header {
padding: 0 10px;
background-color: $color3;
ul {
padding: 0;
margin: 0;
list-style: none;
}
li {
display: inline-block;
box-sizing: border-box;
height: 42px;
padding: 9px 5px;
cursor: pointer;
img, svg {
width: 22px;
height: 22px;
filter: grayscale(100%);
}
&.active {
background: lighten($color3, 6%);
img, svg {
filter: grayscale(0);
}
}
}
}
.emoji-row {
box-sizing: border-box;
overflow-y: hidden;
padding-left: 10px;
.emoji {
display: inline-block;
padding: 5px;
border-radius: 4px;
}
}
.emoji-category-header {
box-sizing: border-box;
overflow-y: hidden;
padding: 8px 16px 0;
display: table;
> * {
display: table-cell;
vertical-align: middle;
}
}
.emoji-category-title {
font-size: 14px;
font-family: sans-serif;
font-weight: normal;
color: $color1;
cursor: default;
}
.emoji-category-heading-decoration {
text-align: right;
}
.modifiers {
list-style: none;
padding: 0;
margin: 0;
vertical-align: middle;
white-space: nowrap;
margin-top: 4px;
li {
display: inline-block;
padding: 0 2px;
&:last-of-type {
padding-right: 0;
}
}
.modifier {
display: inline-block;
border-radius: 10px;
width: 15px;
height: 15px;
position: relative;
cursor: pointer;
&.active:after {
content: "";
display: block;
position: absolute;
width: 7px;
height: 7px;
border-radius: 10px;
border: 2px solid $color1;
top: 2px;
left: 2px;
}
}
}
.emoji-search-wrapper {
padding: 6px 16px;
}
.emoji-search {
font-size: 12px;
padding: 6px 4px;
width: 100%;
border: 1px solid #ddd;
border-radius: 4px;
}
.emoji-categories-wrapper {
position: absolute;
top: 42px;
bottom: 0;
left: 0;
right: 0;
}
.emoji-search-wrapper + .emoji-categories-wrapper {
top: 83px;
}
.emoji-row .emoji:hover {
background: lighten($color2, 3%);
}
.emoji {
width: 22px;
height: 22px;
cursor: pointer;
&:focus {
outline: none;
}
}
}