diff options
Diffstat (limited to 'app')
6 files changed, 273 insertions, 7 deletions
diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/assets/javascripts/components/actions/compose.jsx index b7f225170..165e811e3 100644 --- a/app/assets/javascripts/components/actions/compose.jsx +++ b/app/assets/javascripts/components/actions/compose.jsx @@ -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 + }; +}; diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx index bcc4fe1e7..047c974f2 100644 --- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx +++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx @@ -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 /> diff --git a/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx new file mode 100644 index 000000000..6419ff08a --- /dev/null +++ b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx @@ -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); diff --git a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx b/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx index a34201747..a67adbdd6 100644 --- a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx +++ b/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx @@ -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); diff --git a/app/assets/javascripts/components/reducers/compose.jsx b/app/assets/javascripts/components/reducers/compose.jsx index dead5fd77..b0001351f 100644 --- a/app/assets/javascripts/components/reducers/compose.jsx +++ b/app/assets/javascripts/components/reducers/compose.jsx @@ -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; } diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index 0056cfcd2..ecf510916 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -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; + } + } +} |