From 809455aaaed1e576ca2613828ad009a93224afa0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 28 Feb 2017 00:43:36 +0100 Subject: Add elephant friend to missing indicator --- app/assets/stylesheets/components.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'app/assets/stylesheets/components.scss') diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index 5fc67d9c1..3b7c6ddf4 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -1074,8 +1074,10 @@ button.active i.fa-retweet { text-align: center; font-size: 16px; font-weight: 500; - color: lighten($color1, 26%); - padding-top: 120px; + color: lighten($color1, 16%); + padding-top: 210px; + background: image-url('mastodon-not-found.png') no-repeat center -50px; + cursor: default; } .column-header { -- cgit From 92569b1f0d2a1e4a5b131ce560766b023a71ccb1 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 1 Mar 2017 00:53:11 +0100 Subject: Improved dropdowns --- .../components/components/dropdown_menu.jsx | 40 +++++++++++++++++---- .../components/components/status_action_bar.jsx | 16 +++++---- .../features/account/components/action_bar.jsx | 20 +++++------ .../features/status/components/action_bar.jsx | 9 ++--- app/assets/javascripts/components/locales/en.jsx | 8 ++--- app/assets/stylesheets/components.scss | 41 +++++++++++++++------- 6 files changed, 90 insertions(+), 44 deletions(-) (limited to 'app/assets/stylesheets/components.scss') diff --git a/app/assets/javascripts/components/components/dropdown_menu.jsx b/app/assets/javascripts/components/components/dropdown_menu.jsx index 0a8492b56..2b42eaa60 100644 --- a/app/assets/javascripts/components/components/dropdown_menu.jsx +++ b/app/assets/javascripts/components/components/dropdown_menu.jsx @@ -10,12 +10,44 @@ const DropdownMenu = React.createClass({ direction: React.PropTypes.string }, + getDefaultProps () { + return { + direction: 'left' + }; + }, + mixins: [PureRenderMixin], setRef (c) { this.dropdown = c; }, + handleClick (i, e) { + const { action } = this.props.items[i]; + + if (typeof action === 'function') { + e.preventDefault(); + action(); + this.dropdown.hide(); + } + }, + + renderItem (item, i) { + if (item === null) { + return
  • ; + } + + const { text, action, href = '#' } = item; + + return ( +
  • + + {text} + +
  • + ); + }, + render () { const { icon, items, size, direction } = this.props; const directionClass = (direction === "left") ? "dropdown__left" : "dropdown__right"; @@ -28,13 +60,7 @@ const DropdownMenu = React.createClass({ diff --git a/app/assets/javascripts/components/components/status_action_bar.jsx b/app/assets/javascripts/components/components/status_action_bar.jsx index 35c458b5e..469506f2f 100644 --- a/app/assets/javascripts/components/components/status_action_bar.jsx +++ b/app/assets/javascripts/components/components/status_action_bar.jsx @@ -6,13 +6,13 @@ import { defineMessages, injectIntl } from 'react-intl'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, - mention: { id: 'status.mention', defaultMessage: 'Mention' }, - block: { id: 'account.block', defaultMessage: 'Block' }, + mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, + block: { id: 'account.block', defaultMessage: 'Block @{name}' }, reply: { id: 'status.reply', defaultMessage: 'Reply' }, reblog: { id: 'status.reblog', defaultMessage: 'Reblog' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, - open: { id: 'status.open', defaultMessage: 'Expand' }, - report: { id: 'status.report', defaultMessage: 'Report' } + open: { id: 'status.open', defaultMessage: 'Expand this status' }, + report: { id: 'status.report', defaultMessage: 'Report @{name}' } }); const StatusActionBar = React.createClass({ @@ -74,13 +74,15 @@ const StatusActionBar = React.createClass({ let menu = []; menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); + menu.push(null); if (status.getIn(['account', 'id']) === me) { menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); } else { - menu.push({ text: intl.formatMessage(messages.mention), action: this.handleMentionClick }); - menu.push({ text: intl.formatMessage(messages.block), action: this.handleBlockClick }); - menu.push({ text: intl.formatMessage(messages.report), action: this.handleReport }); + menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); + menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); } return ( diff --git a/app/assets/javascripts/components/features/account/components/action_bar.jsx b/app/assets/javascripts/components/features/account/components/action_bar.jsx index a2ab8172b..48925817c 100644 --- a/app/assets/javascripts/components/features/account/components/action_bar.jsx +++ b/app/assets/javascripts/components/features/account/components/action_bar.jsx @@ -5,14 +5,13 @@ import { Link } from 'react-router'; import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; const messages = defineMessages({ - mention: { id: 'account.mention', defaultMessage: 'Mention' }, + mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock' }, + unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - block: { id: 'account.block', defaultMessage: 'Block' }, + block: { id: 'account.block', defaultMessage: 'Block @{name}' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, - block: { id: 'account.block', defaultMessage: 'Block' }, - report: { id: 'account.report', defaultMessage: 'Report' } + report: { id: 'account.report', defaultMessage: 'Report @{name}' } }); const outerDropdownStyle = { @@ -45,20 +44,21 @@ const ActionBar = React.createClass({ let menu = []; - menu.push({ text: intl.formatMessage(messages.mention), action: this.props.onMention }); + menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); + menu.push(null); if (account.get('id') === me) { menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); } else if (account.getIn(['relationship', 'blocking'])) { - menu.push({ text: intl.formatMessage(messages.unblock), action: this.props.onBlock }); + menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock }); } else if (account.getIn(['relationship', 'following'])) { - menu.push({ text: intl.formatMessage(messages.block), action: this.props.onBlock }); + menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock }); } else { - menu.push({ text: intl.formatMessage(messages.block), action: this.props.onBlock }); + menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock }); } if (account.get('id') !== me) { - menu.push({ text: intl.formatMessage(messages.report), action: this.props.onReport }); + menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); } return ( diff --git a/app/assets/javascripts/components/features/status/components/action_bar.jsx b/app/assets/javascripts/components/features/status/components/action_bar.jsx index cc4d5cca4..2acf94274 100644 --- a/app/assets/javascripts/components/features/status/components/action_bar.jsx +++ b/app/assets/javascripts/components/features/status/components/action_bar.jsx @@ -6,11 +6,11 @@ import { defineMessages, injectIntl } from 'react-intl'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, - mention: { id: 'status.mention', defaultMessage: 'Mention' }, + mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, reply: { id: 'status.reply', defaultMessage: 'Reply' }, reblog: { id: 'status.reblog', defaultMessage: 'Reblog' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, - report: { id: 'status.report', defaultMessage: 'Report' } + report: { id: 'status.report', defaultMessage: 'Report @{name}' } }); const ActionBar = React.createClass({ @@ -66,8 +66,9 @@ const ActionBar = React.createClass({ if (me === status.getIn(['account', 'id'])) { menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); } else { - menu.push({ text: intl.formatMessage(messages.mention), action: this.handleMentionClick }); - menu.push({ text: intl.formatMessage(messages.report), action: this.handleReport }); + menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); } return ( diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx index f1d6a6dbc..e2aa4247e 100644 --- a/app/assets/javascripts/components/locales/en.jsx +++ b/app/assets/javascripts/components/locales/en.jsx @@ -2,7 +2,7 @@ const en = { "column_back_button.label": "Back", "lightbox.close": "Close", "loading_indicator.label": "Loading...", - "status.mention": "Mention", + "status.mention": "Mention @{name}", "status.delete": "Delete", "status.reply": "Reply", "status.reblog": "Boost", @@ -11,11 +11,11 @@ const en = { "status.sensitive_warning": "Sensitive content", "status.sensitive_toggle": "Click to view", "video_player.toggle_sound": "Toggle sound", - "account.mention": "Mention", + "account.mention": "Mention @{name}", "account.edit_profile": "Edit profile", - "account.unblock": "Unblock", + "account.unblock": "Unblock @{name}", "account.unfollow": "Unfollow", - "account.block": "Block", + "account.block": "Block @{name}", "account.follow": "Follow", "account.posts": "Posts", "account.follows": "Follows", diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index 3b7c6ddf4..bb3001a15 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -59,6 +59,10 @@ &.active { color: $color4; } + + &:focus { + outline: none; + } } .invisible { @@ -516,6 +520,12 @@ a.status__content__spoiler-link { position: absolute; } +.dropdown__sep { + border-bottom: 1px solid darken($color2, 8%); + margin: 5px 7px 6px; + padding-top: 1px; +} + .dropdown--active .dropdown__content { display: block; z-index: 9999; @@ -539,17 +549,33 @@ a.status__content__spoiler-link { padding: 4px 0; border-radius: 4px; box-shadow: 0 0 15px rgba($color8, 0.4); - min-width: 100px; + min-width: 140px; + position: relative; + left: -10px; + } + + &.dropdown__left { + ul { + left: -98px; + } } a { font-size: 13px; + line-height: 18px; display: block; - padding: 6px 16px; - width: 100px; + padding: 4px 14px; + box-sizing: border-box; + width: 140px; text-decoration: none; background: $color2; color: $color1; + overflow: hidden; + text-overflow: ellipsis; + + &:focus { + outline: none; + } &:hover { background: $color4; @@ -983,15 +1009,6 @@ a.status__content__spoiler-link { } } -.dropdown__content.dropdown__left { - transform: translateX(-108px); - - &::before { - right: 8px !important; - left: initial !important; - } -} - .setting-text { color: $color3; background: transparent; -- cgit From e1b00757a69ff1cb49311addb7aa76650d680e43 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 1 Mar 2017 01:18:34 +0100 Subject: Fix #291 - Add visual indication that numbers for remote users may be inaccurate --- .../components/features/account/components/action_bar.jsx | 14 ++++++++++---- app/assets/stylesheets/components.scss | 4 ++++ 2 files changed, 14 insertions(+), 4 deletions(-) (limited to 'app/assets/stylesheets/components.scss') diff --git a/app/assets/javascripts/components/features/account/components/action_bar.jsx b/app/assets/javascripts/components/features/account/components/action_bar.jsx index 48925817c..3668a0f13 100644 --- a/app/assets/javascripts/components/features/account/components/action_bar.jsx +++ b/app/assets/javascripts/components/features/account/components/action_bar.jsx @@ -11,7 +11,8 @@ const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, block: { id: 'account.block', defaultMessage: 'Block @{name}' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, - report: { id: 'account.report', defaultMessage: 'Report @{name}' } + report: { id: 'account.report', defaultMessage: 'Report @{name}' }, + disclaimer: { id: 'account.disclaimer', defaultMessage: 'This user is from another instance. This number may be larger.' } }); const outerDropdownStyle = { @@ -43,6 +44,7 @@ const ActionBar = React.createClass({ const { account, me, intl } = this.props; let menu = []; + let extraInfo = ''; menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); menu.push(null); @@ -61,6 +63,10 @@ const ActionBar = React.createClass({ menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); } + if (account.get('domain') !== null) { + extraInfo = *; + } + return (
    @@ -70,17 +76,17 @@ const ActionBar = React.createClass({
    - + {extraInfo} - + {extraInfo} - + {extraInfo}
    diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index bb3001a15..bf8411ae7 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -391,6 +391,10 @@ a.status__content__spoiler-link { font-weight: 500; color: $color5; } + + abbr { + color: lighten($color1, 26%); + } } .status__display-name, .status__relative-time, .detailed-status__display-name, .detailed-status__datetime, .detailed-status__application, .account__display-name { -- cgit From fbdb3bcf1ede96d97693165c485f1eabc44b9f8b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 1 Mar 2017 01:43:29 +0100 Subject: Revert infinite scroll in timelines back to looking at ID of oldest loaded status; do not preload submitted statuses into community/public timelines, unless those timelines have already been loaded; do not close streaming API connections for community/public timelines, once they have been established (most users navigate back to them eventually) --- app/assets/javascripts/components/actions/compose.jsx | 9 +++++++-- app/assets/javascripts/components/actions/timelines.jsx | 12 +++++++----- .../components/features/community_timeline/index.jsx | 16 +++++++++++----- .../components/features/public_timeline/index.jsx | 16 +++++++++++----- app/assets/stylesheets/components.scss | 1 + 5 files changed, 37 insertions(+), 17 deletions(-) (limited to 'app/assets/stylesheets/components.scss') diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/assets/javascripts/components/actions/compose.jsx index 8d030fd30..54ec7b915 100644 --- a/app/assets/javascripts/components/actions/compose.jsx +++ b/app/assets/javascripts/components/actions/compose.jsx @@ -85,8 +85,13 @@ export function submitCompose() { dispatch(updateTimeline('home', { ...response.data })); if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { - dispatch(updateTimeline('community', { ...response.data })); - dispatch(updateTimeline('public', { ...response.data })); + if (getState.getIn(['timelines', 'community', 'loaded'])) { + dispatch(updateTimeline('community', { ...response.data })); + } + + if (getState.getIn(['timelines', 'public', 'loaded'])) { + dispatch(updateTimeline('public', { ...response.data })); + } } }).catch(function (error) { dispatch(submitComposeFail(error)); diff --git a/app/assets/javascripts/components/actions/timelines.jsx b/app/assets/javascripts/components/actions/timelines.jsx index 311b08033..3e2d4ff43 100644 --- a/app/assets/javascripts/components/actions/timelines.jsx +++ b/app/assets/javascripts/components/actions/timelines.jsx @@ -106,18 +106,20 @@ export function expandTimeline(timeline) { return; } - const next = getState().getIn(['timelines', timeline, 'next']); - const params = getState().getIn(['timelines', timeline, 'params'], {}); - - if (next === null) { + if (getState().getIn(['timelines', timeline, 'items']).size === 0) { return; } + const path = getState().getIn(['timelines', timeline, 'path'])(getState().getIn(['timelines', timeline, 'id'])); + const params = getState().getIn(['timelines', timeline, 'params'], {}); + const lastId = getState().getIn(['timelines', timeline, 'items']).last(); + dispatch(expandTimelineRequest(timeline)); - api(getState).get(next, { + api(getState).get(path, { params: { ...params, + max_id: lastId, limit: 10 } }).then(response => { diff --git a/app/assets/javascripts/components/features/community_timeline/index.jsx b/app/assets/javascripts/components/features/community_timeline/index.jsx index aa1b8368e..2cfd7b2fe 100644 --- a/app/assets/javascripts/components/features/community_timeline/index.jsx +++ b/app/assets/javascripts/components/features/community_timeline/index.jsx @@ -20,6 +20,8 @@ const mapStateToProps = state => ({ accessToken: state.getIn(['meta', 'access_token']) }); +let subscription; + const CommunityTimeline = React.createClass({ propTypes: { @@ -36,7 +38,11 @@ const CommunityTimeline = React.createClass({ dispatch(refreshTimeline('community')); - this.subscription = createStream(accessToken, 'public:local', { + if (typeof subscription !== 'undefined') { + return; + } + + subscription = createStream(accessToken, 'public:local', { received (data) { switch(data.event) { @@ -53,10 +59,10 @@ const CommunityTimeline = React.createClass({ }, componentWillUnmount () { - if (typeof this.subscription !== 'undefined') { - this.subscription.close(); - this.subscription = null; - } + // if (typeof subscription !== 'undefined') { + // subscription.close(); + // subscription = null; + // } }, render () { diff --git a/app/assets/javascripts/components/features/public_timeline/index.jsx b/app/assets/javascripts/components/features/public_timeline/index.jsx index ce4eacc92..b2342abbd 100644 --- a/app/assets/javascripts/components/features/public_timeline/index.jsx +++ b/app/assets/javascripts/components/features/public_timeline/index.jsx @@ -20,6 +20,8 @@ const mapStateToProps = state => ({ accessToken: state.getIn(['meta', 'access_token']) }); +let subscription; + const PublicTimeline = React.createClass({ propTypes: { @@ -36,7 +38,11 @@ const PublicTimeline = React.createClass({ dispatch(refreshTimeline('public')); - this.subscription = createStream(accessToken, 'public', { + if (typeof subscription !== 'undefined') { + return; + } + + subscription = createStream(accessToken, 'public', { received (data) { switch(data.event) { @@ -53,10 +59,10 @@ const PublicTimeline = React.createClass({ }, componentWillUnmount () { - if (typeof this.subscription !== 'undefined') { - this.subscription.close(); - this.subscription = null; - } + // if (typeof subscription !== 'undefined') { + // subscription.close(); + // subscription = null; + // } }, render () { diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index bf8411ae7..0056cfcd2 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -576,6 +576,7 @@ a.status__content__spoiler-link { color: $color1; overflow: hidden; text-overflow: ellipsis; + white-space: nowrap; &:focus { outline: none; -- cgit From 89fc2d7f4810ecdf66b17543f4603c1089a0c3f5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 2 Mar 2017 00:57:55 +0100 Subject: Fix #372 - Emoji picker --- .../javascripts/components/actions/compose.jsx | 10 ++ .../features/compose/components/compose_form.jsx | 28 +++- .../compose/components/emoji_picker_dropdown.jsx | 52 +++++++ .../compose/containers/compose_form_container.jsx | 5 + .../javascripts/components/reducers/compose.jsx | 14 +- app/assets/stylesheets/components.scss | 171 ++++++++++++++++++++- package.json | 2 + yarn.lock | 44 +++++- 8 files changed, 315 insertions(+), 11 deletions(-) create mode 100644 app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx (limited to 'app/assets/stylesheets/components.scss') 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({
    - +
    + + +
    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 ( + + + + + + + + + + ); + } + +}); + +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; + } + } +} diff --git a/package.json b/package.json index 45702d5f4..2deebe9e4 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "css-loader": "^0.26.2", "dotenv": "^4.0.0", "emojione": "latest", + "emojione-picker": "^2.0.1", "enzyme": "^2.7.1", "es6-promise": "^3.2.1", "escape-html": "^1.0.3", @@ -40,6 +41,7 @@ "react": "^15.4.2", "react-addons-perf": "^15.4.2", "react-addons-pure-render-mixin": "^15.4.2", + "react-addons-shallow-compare": "^15.4.2", "react-addons-test-utils": "^15.4.2", "react-autosuggest": "^7.0.1", "react-decoration": "^1.4.0", diff --git a/yarn.lock b/yarn.lock index a77fe59eb..011f1ec0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2204,7 +2204,7 @@ doctrine@^2.0.0: esutils "^2.0.2" isarray "^1.0.0" -dom-helpers@^2.4.0: +dom-helpers@^2.4.0, "dom-helpers@^2.4.0 || ^3.0.0": version "2.4.0" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-2.4.0.tgz#9bb4b245f637367b1fa670274272aa28fe06c367" @@ -2287,7 +2287,17 @@ elliptic@^6.0.0: hash.js "^1.0.0" inherits "^2.0.1" -emojione@latest: +emojione-picker@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/emojione-picker/-/emojione-picker-2.0.1.tgz#62e58db67d37a400a883c82d39abb1cc1c8ed65a" + dependencies: + emojione "^2.2.6" + escape-string-regexp "^1.0.5" + lodash "^4.15.0" + react-virtualized "^8.11.4" + store "^1.3.20" + +emojione@^2.2.6, emojione@latest: version "2.2.7" resolved "https://registry.yarnpkg.com/emojione/-/emojione-2.2.7.tgz#46457cf6b9b2f8da13ae8a2e4e547de06ee15e96" @@ -2413,7 +2423,7 @@ escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2: +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -3628,7 +3638,7 @@ lodash.tail@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" -lodash@4.x.x, lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.1: +lodash@4.x.x, lodash@^4.0.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.1: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -3650,6 +3660,12 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0: dependencies: js-tokens "^1.0.1" +loose-envify@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" + dependencies: + js-tokens "^3.0.0" + loud-rejection@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" @@ -4833,6 +4849,13 @@ react-addons-pure-render-mixin@>=0.14.0, react-addons-pure-render-mixin@^15.4.2: fbjs "^0.8.4" object-assign "^4.1.0" +react-addons-shallow-compare@^15.4.2: + version "15.4.2" + resolved "https://registry.yarnpkg.com/react-addons-shallow-compare/-/react-addons-shallow-compare-15.4.2.tgz#027ffd9720e3a1e0b328dcd8fc62e214a0d174a5" + dependencies: + fbjs "^0.8.4" + object-assign "^4.1.0" + react-addons-test-utils@^15.4.2: version "15.4.2" resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.4.2.tgz#93bcaa718fcae7360d42e8fb1c09756cc36302a2" @@ -5051,6 +5074,15 @@ react-toggle@^2.1.1: classnames "~2.2" react-addons-pure-render-mixin ">=0.14.0" +react-virtualized@^8.11.4: + version "8.11.4" + resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-8.11.4.tgz#0bb94f1ecbd286d07145ce63983d0a11724522c0" + dependencies: + babel-runtime "^6.11.6" + classnames "^2.2.3" + dom-helpers "^2.4.0 || ^3.0.0" + loose-envify "^1.3.0" + react@^15.4.2: version "15.4.2" resolved "https://registry.yarnpkg.com/react/-/react-15.4.2.tgz#41f7991b26185392ba9bae96c8889e7e018397ef" @@ -5623,6 +5655,10 @@ stdout-stream@^1.4.0: dependencies: readable-stream "^2.0.1" +store@^1.3.20: + version "1.3.20" + resolved "https://registry.yarnpkg.com/store/-/store-1.3.20.tgz#13ea7e3fb2d6c239868265d686b1d84e99c5be3e" + stream-browserify@^2.0.0, stream-browserify@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" -- cgit From 4d23a85c29c6cfd1340b3365c081a940766e50bc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 2 Mar 2017 18:55:15 +0100 Subject: Fix up storybook --- app/assets/stylesheets/components.scss | 2 ++ storybook/stories/autosuggest_textarea.story.jsx | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'app/assets/stylesheets/components.scss') diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index ecf510916..4b1e86aca 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -1,3 +1,5 @@ +@import 'variables'; + .button { background-color: darken($color4, 3%); font-family: inherit; diff --git a/storybook/stories/autosuggest_textarea.story.jsx b/storybook/stories/autosuggest_textarea.story.jsx index 7d84ff1e1..72a4b525d 100644 --- a/storybook/stories/autosuggest_textarea.story.jsx +++ b/storybook/stories/autosuggest_textarea.story.jsx @@ -2,5 +2,5 @@ import { storiesOf } from '@kadira/storybook'; import AutosuggestTextarea from '../../app/assets/javascripts/components/components/autosuggest_textarea.jsx' storiesOf('AutosuggestTextarea', module) - .add('default state', () => ) - .add('with text', () => ) + .add('default state', () => ) + .add('with text', () => ) -- cgit