From 4d2af0d664b155f5ca2e9259920ed70eec63b8b0 Mon Sep 17 00:00:00 2001 From: Kibigo Date: Mon, 27 Feb 2017 04:32:41 -0800 Subject: Character counter fix --- .../components/features/compose/components/character_counter.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/features/compose/components/character_counter.jsx b/app/assets/javascripts/components/features/compose/components/character_counter.jsx index f0c1b7c8d..e6b675354 100644 --- a/app/assets/javascripts/components/features/compose/components/character_counter.jsx +++ b/app/assets/javascripts/components/features/compose/components/character_counter.jsx @@ -10,7 +10,7 @@ const CharacterCounter = React.createClass({ mixins: [PureRenderMixin], render () { - const diff = this.props.max - this.props.text.length; + const diff = this.props.max - this.props.text.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "_").length; return ( -- cgit From 955e9088d7dcc488aa27707e43eb581b45e19582 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 28 Feb 2017 23:48:41 +0100 Subject: Fix #561 - Detect presence of audio in video, hide mute toggle when none --- .../components/components/video_player.jsx | 61 +++++++++++++++++++--- .../features/status/components/detailed_status.jsx | 2 +- 2 files changed, 55 insertions(+), 8 deletions(-) (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx index ccd67ddf0..5c2cef21a 100644 --- a/app/assets/javascripts/components/components/video_player.jsx +++ b/app/assets/javascripts/components/components/video_player.jsx @@ -61,12 +61,14 @@ const VideoPlayer = React.createClass({ media: ImmutablePropTypes.map.isRequired, width: React.PropTypes.number, height: React.PropTypes.number, - sensitive: React.PropTypes.bool + sensitive: React.PropTypes.bool, + intl: React.PropTypes.object.isRequired, + autoplay: React.PropTypes.bool }, getDefaultProps () { return { - width: 196, + width: 239, height: 110 }; }, @@ -75,7 +77,8 @@ const VideoPlayer = React.createClass({ return { visible: !this.props.sensitive, preview: true, - muted: true + muted: true, + hasAudio: true }; }, @@ -108,8 +111,42 @@ const VideoPlayer = React.createClass({ }); }, + setRef (c) { + this.video = c; + }, + + handleLoadedData () { + if (('WebkitAppearance' in document.documentElement.style && this.video.audioTracks.length === 0) || this.video.mozHasAudio === false) { + this.setState({ hasAudio: false }); + } + }, + + componentDidMount () { + if (!this.video) { + return; + } + + this.video.addEventListener('loadeddata', this.handleLoadedData); + }, + + componentDidUpdate () { + if (!this.video) { + return; + } + + this.video.addEventListener('loadeddata', this.handleLoadedData); + }, + + componentWillUnmount () { + if (!this.video) { + return; + } + + this.video.removeEventListener('loadeddata', this.handleLoadedData); + }, + render () { - const { media, intl, width, height, sensitive } = this.props; + const { media, intl, width, height, sensitive, autoplay } = this.props; let spoilerButton = (
@@ -117,6 +154,16 @@ const VideoPlayer = React.createClass({
); + let muteButton = ''; + + if (this.state.hasAudio) { + muteButton = ( +
+ +
+ ); + } + if (!this.state.visible) { if (sensitive) { return ( @@ -137,7 +184,7 @@ const VideoPlayer = React.createClass({ } } - if (this.state.preview) { + if (this.state.preview && !autoplay) { return (
{spoilerButton} @@ -149,8 +196,8 @@ const VideoPlayer = React.createClass({ return (
{spoilerButton} -
-
); } diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx index 8a7c0c5d5..caa46ff3c 100644 --- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx +++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx @@ -39,7 +39,7 @@ const DetailedStatus = React.createClass({ if (status.get('media_attachments').size > 0) { if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - media = ; + media = ; } else { media = ; } -- 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/javascripts/components/features') 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/javascripts/components/features') 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/javascripts/components/features') 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 5960bac11ea62f9066b338ecef46733fea85b1f6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 1 Mar 2017 02:00:21 +0100 Subject: Fix incorrect detection of local vs remote user in web UI --- .../javascripts/components/features/account/components/action_bar.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/assets/javascripts/components/features') 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 3668a0f13..93ca97119 100644 --- a/app/assets/javascripts/components/features/account/components/action_bar.jsx +++ b/app/assets/javascripts/components/features/account/components/action_bar.jsx @@ -63,7 +63,7 @@ const ActionBar = React.createClass({ menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); } - if (account.get('domain') !== null) { + if (account.get('acct') !== account.get('username')) { extraInfo = *; } -- cgit From 91c79f2445a10d0f43254fc44656152ae88fd9cd Mon Sep 17 00:00:00 2001 From: Kibigo Date: Wed, 1 Mar 2017 02:56:15 -0800 Subject: Better smart/dumb component separation --- .../javascripts/components/components/autosuggest_textarea.jsx | 10 ++++------ .../components/features/compose/components/compose_form.jsx | 6 ++++++ .../features/compose/containers/compose_form_container.jsx | 5 +++++ 3 files changed, 15 insertions(+), 6 deletions(-) (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/components/autosuggest_textarea.jsx b/app/assets/javascripts/components/components/autosuggest_textarea.jsx index 51be39379..38deeae0e 100644 --- a/app/assets/javascripts/components/components/autosuggest_textarea.jsx +++ b/app/assets/javascripts/components/components/autosuggest_textarea.jsx @@ -1,8 +1,6 @@ import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { isRtl } from '../rtl'; -import { connect } from 'react-redux'; -import { uploadCompose } from '../actions/compose'; const textAtCursorMatchesToken = (str, caretPosition) => { let word; @@ -35,7 +33,6 @@ const AutosuggestTextarea = React.createClass({ value: React.PropTypes.string, suggestions: ImmutablePropTypes.list, disabled: React.PropTypes.bool, - dispatch: React.PropTypes.func.isRequired, fileDropDate: React.PropTypes.instanceOf(Date), placeholder: React.PropTypes.string, onSuggestionSelected: React.PropTypes.func.isRequired, @@ -43,7 +40,8 @@ const AutosuggestTextarea = React.createClass({ onSuggestionsFetchRequested: React.PropTypes.func.isRequired, onChange: React.PropTypes.func.isRequired, onKeyUp: React.PropTypes.func, - onKeyDown: React.PropTypes.func + onKeyDown: React.PropTypes.func, + onPaste: React.PropTypes.func.isRequired, }, getInitialState () { @@ -178,7 +176,7 @@ const AutosuggestTextarea = React.createClass({ onPaste (e) { if (e.clipboardData && e.clipboardData.files.length === 1) { - this.props.dispatch(uploadCompose(e.clipboardData.files)); + this.props.onPaste(e.clipboardData.files) e.preventDefault(); } }, @@ -225,4 +223,4 @@ const AutosuggestTextarea = React.createClass({ }); -export default connect()(AutosuggestTextarea); +export default AutosuggestTextarea; 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 31ae8e034..a71f5b0fd 100644 --- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx +++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx @@ -47,6 +47,7 @@ const ComposeForm = React.createClass({ onFetchSuggestions: React.PropTypes.func.isRequired, onSuggestionSelected: React.PropTypes.func.isRequired, onChangeSpoilerText: React.PropTypes.func.isRequired, + onPaste: React.PropTypes.func.isRequired, }, mixins: [PureRenderMixin], @@ -82,6 +83,10 @@ const ComposeForm = React.createClass({ this.props.onChangeSpoilerText(e.target.value); }, + onPaste (files) { + this.props.onPaste(files); + }, + componentDidUpdate (prevProps) { if (this.props.focusDate !== prevProps.focusDate) { // If replying to zero or one users, places the cursor at the end of the textbox. @@ -149,6 +154,7 @@ const ComposeForm = React.createClass({ onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} onSuggestionsClearRequested={this.onSuggestionsClearRequested} onSuggestionSelected={this.onSuggestionSelected} + onPaste={this.onPaste} />
    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 53129af6e..a34201747 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 @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; import ComposeForm from '../components/compose_form'; +import { uploadCompose } from '../../../actions/compose'; import { createSelector } from 'reselect'; import { changeCompose, @@ -65,6 +66,10 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(changeComposeSpoilerText(checked)); }, + onPaste (files) { + dispatch(uploadCompose(files)); + }, + }); export default connect(mapStateToProps, mapDispatchToProps)(ComposeForm); -- cgit From 95ebfa5610577426a5a8f5b6aee83af83dd4a9b4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 1 Mar 2017 13:57:30 +0100 Subject: Simplify passing of prop --- .../components/features/compose/components/compose_form.jsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'app/assets/javascripts/components/features') 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 a71f5b0fd..bcc4fe1e7 100644 --- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx +++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx @@ -83,10 +83,6 @@ const ComposeForm = React.createClass({ this.props.onChangeSpoilerText(e.target.value); }, - onPaste (files) { - this.props.onPaste(files); - }, - componentDidUpdate (prevProps) { if (this.props.focusDate !== prevProps.focusDate) { // If replying to zero or one users, places the cursor at the end of the textbox. @@ -105,7 +101,7 @@ const ComposeForm = React.createClass({ }, render () { - const { intl, needsPrivacyWarning, mentionedDomains } = this.props; + const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props; const disabled = this.props.is_submitting || this.props.is_uploading; let publishText = ''; @@ -154,7 +150,7 @@ const ComposeForm = React.createClass({ onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} onSuggestionsClearRequested={this.onSuggestionsClearRequested} onSuggestionSelected={this.onSuggestionSelected} - onPaste={this.onPaste} + onPaste={onPaste} />
    -- 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/javascripts/components/features') 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 442fdbfc5309f46c23a073829e5fe16d10c7c6ca Mon Sep 17 00:00:00 2001 From: Kit Redgrave Date: Sun, 5 Feb 2017 19:51:56 -0600 Subject: Mute button progress so far. WIP, doesn't entirely work correctly. --- .../javascripts/components/actions/accounts.jsx | 78 ++++++++++++++++++++++ .../components/containers/account_container.jsx | 12 +++- .../components/containers/status_container.jsx | 11 ++- .../features/account/components/action_bar.jsx | 9 +++ .../account_timeline/components/header.jsx | 6 ++ .../containers/header_container.jsx | 12 +++- .../components/reducers/relationships.jsx | 4 ++ app/controllers/api/v1/accounts_controller.rb | 19 +++++- app/controllers/api/v1/mutes_controller.rb | 21 ++++++ app/lib/feed_manager.rb | 50 ++++++++------ app/models/account.rb | 21 ++++++ app/models/mute.rb | 32 +++++++++ app/models/status.rb | 13 ++-- app/services/mute_service.rb | 23 +++++++ app/services/unmute_service.rb | 9 +++ app/views/api/v1/accounts/relationship.rabl | 1 + app/views/api/v1/mutes/index.rabl | 2 + config/routes.rb | 3 + db/migrate/20170301222600_create_mutes.rb | 12 ++++ db/schema.rb | 10 ++- .../controllers/api/v1/accounts_controller_spec.rb | 38 +++++++++++ spec/controllers/api/v1/mutes_controller_spec.rb | 19 ++++++ spec/fabricators/mute_fabricator.rb | 3 + spec/models/mute_spec.rb | 5 ++ spec/services/mute_service_spec.rb | 5 ++ spec/services/unmute_service_spec.rb | 5 ++ 26 files changed, 390 insertions(+), 33 deletions(-) create mode 100644 app/controllers/api/v1/mutes_controller.rb create mode 100644 app/models/mute.rb create mode 100644 app/services/mute_service.rb create mode 100644 app/services/unmute_service.rb create mode 100644 app/views/api/v1/mutes/index.rabl create mode 100644 db/migrate/20170301222600_create_mutes.rb create mode 100644 spec/controllers/api/v1/mutes_controller_spec.rb create mode 100644 spec/fabricators/mute_fabricator.rb create mode 100644 spec/models/mute_spec.rb create mode 100644 spec/services/mute_service_spec.rb create mode 100644 spec/services/unmute_service_spec.rb (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/assets/javascripts/components/actions/accounts.jsx index 47c0d9f85..8af0b15d8 100644 --- a/app/assets/javascripts/components/actions/accounts.jsx +++ b/app/assets/javascripts/components/actions/accounts.jsx @@ -21,6 +21,14 @@ export const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST'; export const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS'; export const ACCOUNT_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL'; +export const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST'; +export const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS'; +export const ACCOUNT_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL'; + +export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST'; +export const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS'; +export const ACCOUNT_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL'; + export const ACCOUNT_TIMELINE_FETCH_REQUEST = 'ACCOUNT_TIMELINE_FETCH_REQUEST'; export const ACCOUNT_TIMELINE_FETCH_SUCCESS = 'ACCOUNT_TIMELINE_FETCH_SUCCESS'; export const ACCOUNT_TIMELINE_FETCH_FAIL = 'ACCOUNT_TIMELINE_FETCH_FAIL'; @@ -328,6 +336,76 @@ export function unblockAccountFail(error) { }; }; + +export function muteAccount(id) { + return (dispatch, getState) => { + dispatch(muteAccountRequest(id)); + + api(getState).post(`/api/v1/accounts/${id}/mute`).then(response => { + // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers + dispatch(muteAccountSuccess(response.data, getState().get('statuses'))); + }).catch(error => { + dispatch(muteAccountFail(id, error)); + }); + }; +}; + +export function unmuteAccount(id) { + return (dispatch, getState) => { + dispatch(unmuteAccountRequest(id)); + + api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => { + dispatch(unmuteAccountSuccess(response.data)); + }).catch(error => { + dispatch(unmuteAccountFail(id, error)); + }); + }; +}; + +export function muteAccountRequest(id) { + return { + type: ACCOUNT_MUTE_REQUEST, + id + }; +}; + +export function muteAccountSuccess(relationship, statuses) { + return { + type: ACCOUNT_MUTE_SUCCESS, + relationship, + statuses + }; +}; + +export function muteAccountFail(error) { + return { + type: ACCOUNT_MUTE_FAIL, + error + }; +}; + +export function unmuteAccountRequest(id) { + return { + type: ACCOUNT_UNMUTE_REQUEST, + id + }; +}; + +export function unmuteAccountSuccess(relationship) { + return { + type: ACCOUNT_UNMUTE_SUCCESS, + relationship + }; +}; + +export function unmuteAccountFail(error) { + return { + type: ACCOUNT_UNMUTE_FAIL, + error + }; +}; + + export function fetchFollowers(id) { return (dispatch, getState) => { dispatch(fetchFollowersRequest(id)); diff --git a/app/assets/javascripts/components/containers/account_container.jsx b/app/assets/javascripts/components/containers/account_container.jsx index 889c0ac4c..3c30be715 100644 --- a/app/assets/javascripts/components/containers/account_container.jsx +++ b/app/assets/javascripts/components/containers/account_container.jsx @@ -5,7 +5,9 @@ import { followAccount, unfollowAccount, blockAccount, - unblockAccount + unblockAccount, + muteAccount, + unmuteAccount, } from '../actions/accounts'; const makeMapStateToProps = () => { @@ -34,6 +36,14 @@ const mapDispatchToProps = (dispatch) => ({ } else { dispatch(blockAccount(account.get('id'))); } + }, + + onMute (account) { + if (account.getIn(['relationship', 'muting'])) { + dispatch(unmuteAccount(account.get('id'))); + } else { + dispatch(muteAccount(account.get('id'))); + } } }); diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx index 81265bc50..e7543bc39 100644 --- a/app/assets/javascripts/components/containers/status_container.jsx +++ b/app/assets/javascripts/components/containers/status_container.jsx @@ -11,7 +11,10 @@ import { unreblog, unfavourite } from '../actions/interactions'; -import { blockAccount } from '../actions/accounts'; +import { + blockAccount, + muteAccount +} from '../actions/accounts'; import { deleteStatus } from '../actions/statuses'; import { initReport } from '../actions/reports'; import { openMedia } from '../actions/modal'; @@ -69,7 +72,11 @@ const mapDispatchToProps = (dispatch) => ({ onReport (status) { dispatch(initReport(status.get('account'), status)); - } + }, + + onMute (account) { + dispatch(muteAccount(account.get('id'))); + }, }); 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 93ca97119..60947767c 100644 --- a/app/assets/javascripts/components/features/account/components/action_bar.jsx +++ b/app/assets/javascripts/components/features/account/components/action_bar.jsx @@ -9,7 +9,9 @@ const messages = defineMessages({ edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute' }, block: { id: 'account.block', defaultMessage: 'Block @{name}' }, + mute: { id: 'account.mute', defaultMessage: 'Mute' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, report: { id: 'account.report', defaultMessage: 'Report @{name}' }, disclaimer: { id: 'account.disclaimer', defaultMessage: 'This user is from another instance. This number may be larger.' } @@ -35,6 +37,7 @@ const ActionBar = React.createClass({ onBlock: React.PropTypes.func.isRequired, onMention: React.PropTypes.func.isRequired, onReport: React.PropTypes.func.isRequired, + onMute: React.PropTypes.func.isRequired, intl: React.PropTypes.object.isRequired }, @@ -67,6 +70,12 @@ const ActionBar = React.createClass({ extraInfo = *; } + if (account.getIn(['relationship', 'muting'])) { + menu.push({ text: intl.formatMessage(messages.unmute), action: this.props.onMute }); + } else { + menu.push({ text: intl.formatMessage(messages.mute), action: this.props.onMute }); + } + return (
    diff --git a/app/assets/javascripts/components/features/account_timeline/components/header.jsx b/app/assets/javascripts/components/features/account_timeline/components/header.jsx index 2dd3ca7b1..f436a180b 100644 --- a/app/assets/javascripts/components/features/account_timeline/components/header.jsx +++ b/app/assets/javascripts/components/features/account_timeline/components/header.jsx @@ -16,6 +16,7 @@ const Header = React.createClass({ onBlock: React.PropTypes.func.isRequired, onMention: React.PropTypes.func.isRequired, onReport: React.PropTypes.func.isRequired + onMute: React.PropTypes.func.isRequired, }, mixins: [PureRenderMixin], @@ -37,6 +38,10 @@ const Header = React.createClass({ this.context.router.push('/report'); }, + handleMute() { + this.props.onMute(this.props.account); + }, + render () { const { account, me } = this.props; @@ -58,6 +63,7 @@ const Header = React.createClass({ onBlock={this.handleBlock} onMention={this.handleMention} onReport={this.handleReport} + onMute={this.handleMute} />
    ); diff --git a/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx b/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx index e4ce905fe..8472d25a5 100644 --- a/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx +++ b/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx @@ -5,7 +5,9 @@ import { followAccount, unfollowAccount, blockAccount, - unblockAccount + unblockAccount, + muteAccount, + unmuteAccount } from '../../../actions/accounts'; import { mentionCompose } from '../../../actions/compose'; import { initReport } from '../../../actions/reports'; @@ -44,6 +46,14 @@ const mapDispatchToProps = dispatch => ({ onReport (account) { dispatch(initReport(account)); + }, + + onMute (account) { + if (account.getIn(['relationship', 'muting'])) { + dispatch(unmuteAccount(account.get('id'))); + } else { + dispatch(muteAccount(account.get('id'))); + } } }); diff --git a/app/assets/javascripts/components/reducers/relationships.jsx b/app/assets/javascripts/components/reducers/relationships.jsx index e4af1f028..591f8034b 100644 --- a/app/assets/javascripts/components/reducers/relationships.jsx +++ b/app/assets/javascripts/components/reducers/relationships.jsx @@ -3,6 +3,8 @@ import { ACCOUNT_UNFOLLOW_SUCCESS, ACCOUNT_BLOCK_SUCCESS, ACCOUNT_UNBLOCK_SUCCESS, + ACCOUNT_MUTE_SUCCESS, + ACCOUNT_UNMUTE_SUCCESS, RELATIONSHIPS_FETCH_SUCCESS } from '../actions/accounts'; import Immutable from 'immutable'; @@ -25,6 +27,8 @@ export default function relationships(state = initialState, action) { case ACCOUNT_UNFOLLOW_SUCCESS: case ACCOUNT_BLOCK_SUCCESS: case ACCOUNT_UNBLOCK_SUCCESS: + case ACCOUNT_MUTE_SUCCESS: + case ACCOUNT_UNMUTE_SUCCESS: return normalizeRelationship(state, action.relationship); case RELATIONSHIPS_FETCH_SUCCESS: return normalizeRelationships(state, action.relationships); diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 94dba1d03..d691ac987 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class Api::V1::AccountsController < ApiController - before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock] - before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock] + before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute] + before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock, :mute, :unmute] before_action :require_user!, except: [:show, :following, :followers, :statuses] before_action :set_account, except: [:verify_credentials, :suggestions, :search] @@ -86,10 +86,17 @@ class Api::V1::AccountsController < ApiController @followed_by = { @account.id => false } @blocking = { @account.id => true } @requested = { @account.id => false } + @muting = { @account.id => current_user.account.muting?(@account.id) } render action: :relationship end + def mute + MuteService.new.call(current_user.account, @account) + set_relationship + render action: :relationship + end + def unfollow UnfollowService.new.call(current_user.account, @account) set_relationship @@ -102,6 +109,12 @@ class Api::V1::AccountsController < ApiController render action: :relationship end + def unmute + UnmuteService.new.call(current_user.account, @account) + set_relationship + render action: :relationship + end + def relationships ids = params[:id].is_a?(Enumerable) ? params[:id].map(&:to_i) : [params[:id].to_i] @@ -109,6 +122,7 @@ class Api::V1::AccountsController < ApiController @following = Account.following_map(ids, current_user.account_id) @followed_by = Account.followed_by_map(ids, current_user.account_id) @blocking = Account.blocking_map(ids, current_user.account_id) + @muting = Account.muting_map(ids, current_user.account_id) @requested = Account.requested_map(ids, current_user.account_id) end @@ -130,6 +144,7 @@ class Api::V1::AccountsController < ApiController @following = Account.following_map([@account.id], current_user.account_id) @followed_by = Account.followed_by_map([@account.id], current_user.account_id) @blocking = Account.blocking_map([@account.id], current_user.account_id) + @muting = Account.muting_map([@account.id], current_user.account_id) @requested = Account.requested_map([@account.id], current_user.account_id) end end diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb new file mode 100644 index 000000000..42a9ed412 --- /dev/null +++ b/app/controllers/api/v1/mutes_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class Api::V1::MutesController < ApiController + before_action -> { doorkeeper_authorize! :follow } + before_action :require_user! + + respond_to :json + + def index + results = Mute.where(account: current_account).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) + accounts = Account.where(id: results.map(&:target_account_id)).map { |a| [a.id, a] }.to_h + @accounts = results.map { |f| accounts[f.target_account_id] } + + set_account_counters_maps(@accounts) + + next_path = api_v1_mutes_url(max_id: results.last.id) if results.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) + prev_path = api_v1_mutes_url(since_id: results.first.id) unless results.empty? + + set_pagination_headers(next_path, prev_path) + end +end diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 016ef0235..d52260713 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -22,18 +22,8 @@ class FeedManager end def push(timeline_type, account, status) - timeline_key = key(timeline_type, account.id) - - if status.reblog? - # If the original status is within 40 statuses from top, do not re-insert it into the feed - rank = redis.zrevrank(timeline_key, status.reblog_of_id) - return if !rank.nil? && rank < 40 - redis.zadd(timeline_key, status.id, status.reblog_of_id) - else - redis.zadd(timeline_key, status.id, status.id) - trim(timeline_type, account.id) - end - + redis.zadd(key(timeline_type, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id) + trim(timeline_type, account.id) broadcast(account.id, event: 'update', payload: inline_render(account, 'api/v1/statuses/show', status)) end @@ -95,31 +85,47 @@ class FeedManager end def filter_from_home?(status, receiver) - should_filter = false + should_filter = receiver.muting?(status.account_id) # Filter if I'm muting this person - if status.reply? && status.in_reply_to_id.nil? + if status.reply? && status.in_reply_to_id.nil? # Filter out replies to nobody should_filter = true - elsif status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply - should_filter = !receiver.following?(status.in_reply_to_account) # and I'm not following the person it's a reply to + elsif status.reply? && !status.in_reply_to_account_id.nil? # If it's a reply + should_filter = !receiver.following?(status.in_reply_to_account) # filter if I'm not following the person it's a reply to should_filter &&= !(receiver.id == status.in_reply_to_account_id) # and it's not a reply to me should_filter &&= !(status.account_id == status.in_reply_to_account_id) # and it's not a self-reply - elsif status.reblog? # Filter out a reblog - should_filter = receiver.blocking?(status.reblog.account) # if I'm blocking the reblogged person + elsif status.reblog? # If it's a reblog + should_filter = receiver.blocking?(status.reblog.account) # filter if I'm blocking the reblogged person + should_filter ||= receiver.muting?(status.reblog.account) # or if I'm muting the reblogged person end - should_filter ||= receiver.blocking?(status.mentions.map(&:account_id)) # or if it mentions someone I blocked - + should_filter ||= receiver.blocking?(status.mentions.map(&:account_id)) # Filter if it mentions someone I blocked should_filter end def filter_from_mentions?(status, receiver) - should_filter = receiver.id == status.account_id # Filter if I'm mentioning myself + should_filter = receiver.id == status.account_id # Filter out if I'm mentioning myself should_filter ||= receiver.blocking?(status.account) # or it's from someone I blocked should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account)) # or if it mentions someone I blocked should_filter ||= (status.account.silenced? && !receiver.following?(status.account)) # of if the account is silenced and I'm not following them + if status.reply? && !status.in_reply_to_account_id.nil? + should_filter ||= receiver.blocking?(status.in_reply_to_account) # or if it's a reply to a user I blocked + end + + should_filter + end + + def filter_from_public?(status, receiver) + should_filter = receiver.blocking?(status.account) # Filter out if I'm blocking that account + should_filter ||= receiver.muting?(status.account_id) # or if I'm muting this person + should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account)) # or if it mentions someone I blocked + if status.reply? && !status.in_reply_to_account_id.nil? # or it's a reply - should_filter ||= receiver.blocking?(status.in_reply_to_account) # to a user I blocked + should_filter ||= receiver.blocking?(status.in_reply_to_account) # to somebody I've blocked + should_filter ||= receiver.muting?(status.in_reply_to_account) # or to somebody I'm muting + elsif status.reblog? # or if it's a reblog + should_filter ||= receiver.blocking?(status.reblog.account) # if I'm blocking the reblogged person + should_filter ||= receiver.muting?(status.reblog.account) # or if I'm muting the reblogged person end should_filter diff --git a/app/models/account.rb b/app/models/account.rb index a93a0668a..2fa6bab71 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -46,6 +46,10 @@ class Account < ApplicationRecord has_many :block_relationships, class_name: 'Block', foreign_key: 'account_id', dependent: :destroy has_many :blocking, -> { order('blocks.id desc') }, through: :block_relationships, source: :target_account + # Mute relationships + has_many :mute_relationships, class_name: 'Mute', foreign_key: 'account_id', dependent: :destroy + has_many :muting, -> { order('mutes.id desc') }, through: :mute_relationships, source: :target_account + # Media has_many :media_attachments, dependent: :destroy @@ -73,6 +77,10 @@ class Account < ApplicationRecord block_relationships.where(target_account: other_account).first_or_create!(target_account: other_account) end + def mute!(other_account) + mute_relationships.where(target_account: other_account).first_or_create!(target_account: other_account) + end + def unfollow!(other_account) follow = active_relationships.find_by(target_account: other_account) follow&.destroy @@ -83,6 +91,11 @@ class Account < ApplicationRecord block&.destroy end + def unmute!(other_account) + mute = mute_relationships.find_by(target_account: other_account) + mute&.destroy + end + def following?(other_account) following.include?(other_account) end @@ -91,6 +104,10 @@ class Account < ApplicationRecord blocking.include?(other_account) end + def muting?(other_account) + muting.include?(other_account) + end + def requested?(other_account) follow_requests.where(target_account: other_account).exists? end @@ -188,6 +205,10 @@ class Account < ApplicationRecord follow_mapping(Block.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) end + def muting_map(target_account_ids, account_id) + follow_mapping(Mute.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) + end + def requested_map(target_account_ids, account_id) follow_mapping(FollowRequest.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) end diff --git a/app/models/mute.rb b/app/models/mute.rb new file mode 100644 index 000000000..505196453 --- /dev/null +++ b/app/models/mute.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class Mute < ApplicationRecord + include Paginable + include Streamable + + belongs_to :account + belongs_to :target_account, class_name: 'Account' + + validates :account, :target_account, presence: true + validates :account_id, uniqueness: { scope: :target_account_id } + + def verb + destroyed? ? :unmute : :mute + end + + def target + target_account + end + + def object_type + :person + end + + def hidden? + true + end + + def title + destroyed? ? "#{account.acct} is no longer muting #{target_account.acct}" : "#{account.acct} muted #{target_account.acct}" + end +end diff --git a/app/models/status.rb b/app/models/status.rb index 1b40897f3..b3424f36d 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -103,7 +103,10 @@ class Status < ApplicationRecord class << self def as_home_timeline(account) - where(account: [account] + account.following) + muted = Mute.where(account: account).pluck(:target_account_id) + query = where(account: [account] + account.following) + query = query.where('statuses.account_id NOT IN (?)', muted) unless muted.empty? + query end def as_public_timeline(account = nil, local_only = false) @@ -169,8 +172,10 @@ class Status < ApplicationRecord def filter_timeline(query, account) blocked = Block.where(account: account).pluck(:target_account_id) + Block.where(target_account: account).pluck(:account_id) - query = query.where('statuses.account_id NOT IN (?)', blocked) unless blocked.empty? - query = query.where('accounts.silenced = TRUE') if account.silenced? + muted = Mute.where(account: account).pluck(:target_account_id) + query = query.where('statuses.account_id NOT IN (?)', blocked) unless blocked.empty? # Only give us statuses from people we haven't blocked + query = query.where('statuses.account_id NOT IN (?)', muted) unless muted.empty? # and out of those, only people we haven't muted + query = query.where('accounts.silenced = TRUE') if account.silenced? # and if we're hellbanned, only people who are also hellbanned query end @@ -192,6 +197,6 @@ class Status < ApplicationRecord private def filter_from_context?(status, account) - account&.blocking?(status.account_id) || (status.account.silenced? && !account&.following?(status.account_id)) || !status.permitted?(account) + account&.blocking?(status.account_id) || account&.muting?(status.account_id) || (status.account.silenced? && !account&.following?(status.account_id)) || !status.permitted?(account) end end diff --git a/app/services/mute_service.rb b/app/services/mute_service.rb new file mode 100644 index 000000000..0050cfc8d --- /dev/null +++ b/app/services/mute_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class MuteService < BaseService + def call(account, target_account) + return if account.id == target_account.id + clear_home_timeline(account, target_account) + account.mute!(target_account) + end + + private + + def clear_home_timeline(account, target_account) + home_key = FeedManager.instance.key(:home, account.id) + + target_account.statuses.select('id').find_each do |status| + redis.zrem(home_key, status.id) + end + end + + def redis + Redis.current + end +end diff --git a/app/services/unmute_service.rb b/app/services/unmute_service.rb new file mode 100644 index 000000000..ed268b7c5 --- /dev/null +++ b/app/services/unmute_service.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class UnmuteService < BaseService + def call(account, target_account) + return unless account.muting?(target_account) + + account.unmute!(target_account) + end +end diff --git a/app/views/api/v1/accounts/relationship.rabl b/app/views/api/v1/accounts/relationship.rabl index 22b37586e..d6f1dd48a 100644 --- a/app/views/api/v1/accounts/relationship.rabl +++ b/app/views/api/v1/accounts/relationship.rabl @@ -4,4 +4,5 @@ attribute :id node(:following) { |account| @following[account.id] || false } node(:followed_by) { |account| @followed_by[account.id] || false } node(:blocking) { |account| @blocking[account.id] || false } +node(:muting) { |account| @muting[account.id] || false } node(:requested) { |account| @requested[account.id] || false } diff --git a/app/views/api/v1/mutes/index.rabl b/app/views/api/v1/mutes/index.rabl new file mode 100644 index 000000000..9f3b13a53 --- /dev/null +++ b/app/views/api/v1/mutes/index.rabl @@ -0,0 +1,2 @@ +collection @accounts +extends 'api/v1/accounts/show' diff --git a/config/routes.rb b/config/routes.rb index 870d8afd4..f6e2dce5c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -127,6 +127,7 @@ Rails.application.routes.draw do resources :media, only: [:create] resources :apps, only: [:create] resources :blocks, only: [:index] + resources :mutes, only: [:index] resources :favourites, only: [:index] resources :reports, only: [:index, :create] @@ -160,6 +161,8 @@ Rails.application.routes.draw do post :unfollow post :block post :unblock + post :mute + post :unmute end end end diff --git a/db/migrate/20170301222600_create_mutes.rb b/db/migrate/20170301222600_create_mutes.rb new file mode 100644 index 000000000..8f1bb22f5 --- /dev/null +++ b/db/migrate/20170301222600_create_mutes.rb @@ -0,0 +1,12 @@ +class CreateMutes < ActiveRecord::Migration[5.0] + def change + create_table :mutes do |t| + t.integer :account_id, null: false + t.integer :target_account_id, null: false + t.timestamps null: false + end + + add_index :mutes, [:account_id, :target_account_id], unique: true + + end +end diff --git a/db/schema.rb b/db/schema.rb index fa5c40774..c2d88ac13 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170217012631) do +ActiveRecord::Schema.define(version: 20170301222600) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -110,6 +110,14 @@ ActiveRecord::Schema.define(version: 20170217012631) do t.index ["account_id", "status_id"], name: "index_mentions_on_account_id_and_status_id", unique: true, using: :btree end + create_table "mutes", force: :cascade do |t| + t.integer "account_id", null: false + t.integer "target_account_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id", "target_account_id"], name: "index_mutes_on_account_id_and_target_account_id", unique: true, using: :btree + end + create_table "notifications", force: :cascade do |t| t.integer "account_id" t.integer "activity_id" diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index 98b284f7a..5d36b0159 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -116,6 +116,44 @@ RSpec.describe Api::V1::AccountsController, type: :controller do end end + describe 'POST #mute' do + let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } + + before do + user.account.follow!(other_account) + post :mute, params: {id: other_account.id } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'does not remove the following relation between user and target user' do + expect(user.account.following?(other_account)).to be true + end + + it 'creates a muting relation' do + expect(user.account.muting?(other_account)).to be true + end + end + + describe 'POST #unmute' do + let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } + + before do + user.account.mute!(other_account) + post :unmute, params: { id: other_account.id } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'removes the muting relation between user and target user' do + expect(user.account.muting?(other_account)).to be false + end + end + describe 'GET #relationships' do let(:simon) { Fabricate(:user, email: 'simon@example.com', account: Fabricate(:account, username: 'simon')).account } let(:lewis) { Fabricate(:user, email: 'lewis@example.com', account: Fabricate(:account, username: 'lewis')).account } diff --git a/spec/controllers/api/v1/mutes_controller_spec.rb b/spec/controllers/api/v1/mutes_controller_spec.rb new file mode 100644 index 000000000..be8a5e7dd --- /dev/null +++ b/spec/controllers/api/v1/mutes_controller_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' + +RSpec.describe Api::V1::MutesController, type: :controller do + render_views + + let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } + let(:token) { double acceptable?: true, resource_owner_id: user.id } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + it 'returns http success' do + get :index + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/fabricators/mute_fabricator.rb b/spec/fabricators/mute_fabricator.rb new file mode 100644 index 000000000..fc150c1d6 --- /dev/null +++ b/spec/fabricators/mute_fabricator.rb @@ -0,0 +1,3 @@ +Fabricator(:mute) do + +end diff --git a/spec/models/mute_spec.rb b/spec/models/mute_spec.rb new file mode 100644 index 000000000..83ba793b2 --- /dev/null +++ b/spec/models/mute_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Mute, type: :model do + +end diff --git a/spec/services/mute_service_spec.rb b/spec/services/mute_service_spec.rb new file mode 100644 index 000000000..397368416 --- /dev/null +++ b/spec/services/mute_service_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe MuteService do + subject { MuteService.new } +end diff --git a/spec/services/unmute_service_spec.rb b/spec/services/unmute_service_spec.rb new file mode 100644 index 000000000..5dc971fb1 --- /dev/null +++ b/spec/services/unmute_service_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe UnmuteService do + subject { UnmuteService.new } +end -- cgit From c64a1c25c4e9a07c694863a38334ed66e368752e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 2 Mar 2017 18:49:32 +0100 Subject: Fix #231 - Muting --- .../features/account/components/action_bar.jsx | 28 ++++++------ .../account_timeline/components/header.jsx | 4 +- .../compose/components/emoji_picker_dropdown.jsx | 2 +- .../javascripts/components/reducers/timelines.jsx | 4 +- app/lib/feed_manager.rb | 53 ++++++++++------------ app/models/mute.rb | 21 --------- app/models/status.rb | 11 ++--- app/services/unmute_service.rb | 2 + streaming/index.js | 2 +- 9 files changed, 50 insertions(+), 77 deletions(-) (limited to 'app/assets/javascripts/components/features') 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 60947767c..80a32d3e2 100644 --- a/app/assets/javascripts/components/features/account/components/action_bar.jsx +++ b/app/assets/javascripts/components/features/account/components/action_bar.jsx @@ -9,9 +9,9 @@ const messages = defineMessages({ edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, block: { id: 'account.block', defaultMessage: 'Block @{name}' }, - mute: { id: 'account.mute', defaultMessage: 'Mute' }, + mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, report: { id: 'account.report', defaultMessage: 'Report @{name}' }, disclaimer: { id: 'account.disclaimer', defaultMessage: 'This user is from another instance. This number may be larger.' } @@ -54,15 +54,19 @@ const ActionBar = React.createClass({ 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, { name: account.get('username') }), action: this.props.onBlock }); - } else if (account.getIn(['relationship', 'following'])) { - menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock }); } else { - menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock }); - } + if (account.getIn(['relationship', 'muting'])) { + menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute }); + } else { + menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute }); + } + + if (account.getIn(['relationship', 'blocking'])) { + menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock }); + } else { + 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, { name: account.get('username') }), action: this.props.onReport }); } @@ -70,12 +74,6 @@ const ActionBar = React.createClass({ extraInfo = *; } - if (account.getIn(['relationship', 'muting'])) { - menu.push({ text: intl.formatMessage(messages.unmute), action: this.props.onMute }); - } else { - menu.push({ text: intl.formatMessage(messages.mute), action: this.props.onMute }); - } - return (
    diff --git a/app/assets/javascripts/components/features/account_timeline/components/header.jsx b/app/assets/javascripts/components/features/account_timeline/components/header.jsx index f436a180b..99a10562e 100644 --- a/app/assets/javascripts/components/features/account_timeline/components/header.jsx +++ b/app/assets/javascripts/components/features/account_timeline/components/header.jsx @@ -15,8 +15,8 @@ const Header = React.createClass({ onFollow: React.PropTypes.func.isRequired, onBlock: React.PropTypes.func.isRequired, onMention: React.PropTypes.func.isRequired, - onReport: React.PropTypes.func.isRequired - onMute: React.PropTypes.func.isRequired, + onReport: React.PropTypes.func.isRequired, + onMute: React.PropTypes.func.isRequired }, mixins: [PureRenderMixin], 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 index 6419ff08a..3a454a5fb 100644 --- a/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx +++ b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx @@ -36,7 +36,7 @@ const EmojiPickerDropdown = React.createClass({ return ( - + diff --git a/app/assets/javascripts/components/reducers/timelines.jsx b/app/assets/javascripts/components/reducers/timelines.jsx index 6472ac6a0..c67d05423 100644 --- a/app/assets/javascripts/components/reducers/timelines.jsx +++ b/app/assets/javascripts/components/reducers/timelines.jsx @@ -22,7 +22,8 @@ import { ACCOUNT_TIMELINE_EXPAND_REQUEST, ACCOUNT_TIMELINE_EXPAND_SUCCESS, ACCOUNT_TIMELINE_EXPAND_FAIL, - ACCOUNT_BLOCK_SUCCESS + ACCOUNT_BLOCK_SUCCESS, + ACCOUNT_MUTE_SUCCESS } from '../actions/accounts'; import { CONTEXT_FETCH_SUCCESS @@ -295,6 +296,7 @@ export default function timelines(state = initialState, action) { case ACCOUNT_TIMELINE_EXPAND_SUCCESS: return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses)); case ACCOUNT_BLOCK_SUCCESS: + case ACCOUNT_MUTE_SUCCESS: return filterTimelines(state, action.relationship, action.statuses); case TIMELINE_SCROLL_TOP: return updateTop(state, action.timeline, action.top); diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index d52260713..3a26c5c05 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -22,8 +22,18 @@ class FeedManager end def push(timeline_type, account, status) - redis.zadd(key(timeline_type, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id) - trim(timeline_type, account.id) + timeline_key = key(timeline_type, account.id) + + if status.reblog? + # If the original status is within 40 statuses from top, do not re-insert it into the feed + rank = redis.zrevrank(timeline_key, status.reblog_of_id) + return if !rank.nil? && rank < 40 + redis.zadd(timeline_key, status.id, status.reblog_of_id) + else + redis.zadd(timeline_key, status.id, status.id) + trim(timeline_type, account.id) + end + broadcast(account.id, event: 'update', payload: inline_render(account, 'api/v1/statuses/show', status)) end @@ -85,47 +95,34 @@ class FeedManager end def filter_from_home?(status, receiver) - should_filter = receiver.muting?(status.account_id) # Filter if I'm muting this person + return true if receiver.muting?(status.account) + + should_filter = false - if status.reply? && status.in_reply_to_id.nil? # Filter out replies to nobody + if status.reply? && status.in_reply_to_id.nil? should_filter = true - elsif status.reply? && !status.in_reply_to_account_id.nil? # If it's a reply - should_filter = !receiver.following?(status.in_reply_to_account) # filter if I'm not following the person it's a reply to + elsif status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply + should_filter = !receiver.following?(status.in_reply_to_account) # and I'm not following the person it's a reply to should_filter &&= !(receiver.id == status.in_reply_to_account_id) # and it's not a reply to me should_filter &&= !(status.account_id == status.in_reply_to_account_id) # and it's not a self-reply - elsif status.reblog? # If it's a reblog - should_filter = receiver.blocking?(status.reblog.account) # filter if I'm blocking the reblogged person - should_filter ||= receiver.muting?(status.reblog.account) # or if I'm muting the reblogged person + elsif status.reblog? # Filter out a reblog + should_filter = receiver.blocking?(status.reblog.account) # if I'm blocking the reblogged person + should_filter ||= receiver.muting?(status.reblog.account) # or muting that person end - should_filter ||= receiver.blocking?(status.mentions.map(&:account_id)) # Filter if it mentions someone I blocked + should_filter ||= receiver.blocking?(status.mentions.map(&:account_id)) # or if it mentions someone I blocked + should_filter end def filter_from_mentions?(status, receiver) - should_filter = receiver.id == status.account_id # Filter out if I'm mentioning myself + should_filter = receiver.id == status.account_id # Filter if I'm mentioning myself should_filter ||= receiver.blocking?(status.account) # or it's from someone I blocked should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account)) # or if it mentions someone I blocked should_filter ||= (status.account.silenced? && !receiver.following?(status.account)) # of if the account is silenced and I'm not following them - if status.reply? && !status.in_reply_to_account_id.nil? - should_filter ||= receiver.blocking?(status.in_reply_to_account) # or if it's a reply to a user I blocked - end - - should_filter - end - - def filter_from_public?(status, receiver) - should_filter = receiver.blocking?(status.account) # Filter out if I'm blocking that account - should_filter ||= receiver.muting?(status.account_id) # or if I'm muting this person - should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account)) # or if it mentions someone I blocked - if status.reply? && !status.in_reply_to_account_id.nil? # or it's a reply - should_filter ||= receiver.blocking?(status.in_reply_to_account) # to somebody I've blocked - should_filter ||= receiver.muting?(status.in_reply_to_account) # or to somebody I'm muting - elsif status.reblog? # or if it's a reblog - should_filter ||= receiver.blocking?(status.reblog.account) # if I'm blocking the reblogged person - should_filter ||= receiver.muting?(status.reblog.account) # or if I'm muting the reblogged person + should_filter ||= receiver.blocking?(status.in_reply_to_account) # to a user I blocked end should_filter diff --git a/app/models/mute.rb b/app/models/mute.rb index 505196453..a5b334c85 100644 --- a/app/models/mute.rb +++ b/app/models/mute.rb @@ -2,31 +2,10 @@ class Mute < ApplicationRecord include Paginable - include Streamable belongs_to :account belongs_to :target_account, class_name: 'Account' validates :account, :target_account, presence: true validates :account_id, uniqueness: { scope: :target_account_id } - - def verb - destroyed? ? :unmute : :mute - end - - def target - target_account - end - - def object_type - :person - end - - def hidden? - true - end - - def title - destroyed? ? "#{account.acct} is no longer muting #{target_account.acct}" : "#{account.acct} muted #{target_account.acct}" - end end diff --git a/app/models/status.rb b/app/models/status.rb index b3424f36d..e5e740360 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -103,10 +103,7 @@ class Status < ApplicationRecord class << self def as_home_timeline(account) - muted = Mute.where(account: account).pluck(:target_account_id) - query = where(account: [account] + account.following) - query = query.where('statuses.account_id NOT IN (?)', muted) unless muted.empty? - query + where(account: [account] + account.following) end def as_public_timeline(account = nil, local_only = false) @@ -171,10 +168,8 @@ class Status < ApplicationRecord private def filter_timeline(query, account) - blocked = Block.where(account: account).pluck(:target_account_id) + Block.where(target_account: account).pluck(:account_id) - muted = Mute.where(account: account).pluck(:target_account_id) - query = query.where('statuses.account_id NOT IN (?)', blocked) unless blocked.empty? # Only give us statuses from people we haven't blocked - query = query.where('statuses.account_id NOT IN (?)', muted) unless muted.empty? # and out of those, only people we haven't muted + blocked = Block.where(account: account).pluck(:target_account_id) + Block.where(target_account: account).pluck(:account_id) + Mute.where(account: account).pluck(:target_account_id) + query = query.where('statuses.account_id NOT IN (?)', blocked) unless blocked.empty? # Only give us statuses from people we haven't blocked, or muted, or that have blocked us query = query.where('accounts.silenced = TRUE') if account.silenced? # and if we're hellbanned, only people who are also hellbanned query end diff --git a/app/services/unmute_service.rb b/app/services/unmute_service.rb index ed268b7c5..6aeea358f 100644 --- a/app/services/unmute_service.rb +++ b/app/services/unmute_service.rb @@ -5,5 +5,7 @@ class UnmuteService < BaseService return unless account.muting?(target_account) account.unmute!(target_account) + + MergeWorker.perform_async(target_account.id, account.id) if account.following?(target_account) end end diff --git a/streaming/index.js b/streaming/index.js index 125b35bb4..0f838e411 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -164,7 +164,7 @@ const streamFrom = (id, req, output, attachCloseHandler, needsFiltering = false) const unpackedPayload = JSON.parse(payload) const targetAccountIds = [unpackedPayload.account.id].concat(unpackedPayload.mentions.map(item => item.id)).concat(unpackedPayload.reblog ? [unpackedPayload.reblog.account.id] : []) - client.query(`SELECT target_account_id FROM blocks WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)})`, [req.accountId].concat(targetAccountIds), (err, result) => { + client.query(`SELECT target_account_id FROM blocks WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)}) UNION SELECT target_account_id FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)})`, [req.accountId].concat(targetAccountIds), (err, result) => { done() if (err) { -- cgit From b9345b3fc60c5d48828af422d3ee027abc2dea82 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 2 Mar 2017 19:24:12 +0100 Subject: Fix #620 - Add confirmation dialog for notifications clear --- .../features/notifications/components/clear_column_button.jsx | 3 ++- app/assets/javascripts/components/features/notifications/index.jsx | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx index 0b7c737c6..d75149a0e 100644 --- a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx +++ b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx @@ -4,7 +4,8 @@ const iconStyle = { position: 'absolute', right: '48px', top: '0', - cursor: 'pointer' + cursor: 'pointer', + zIndex: '2' }; const ClearColumnButton = ({ onClick }) => ( diff --git a/app/assets/javascripts/components/features/notifications/index.jsx b/app/assets/javascripts/components/features/notifications/index.jsx index 0da3544f6..8d3f4233d 100644 --- a/app/assets/javascripts/components/features/notifications/index.jsx +++ b/app/assets/javascripts/components/features/notifications/index.jsx @@ -13,7 +13,8 @@ import LoadMore from '../../components/load_more'; import ClearColumnButton from './components/clear_column_button'; const messages = defineMessages({ - title: { id: 'column.notifications', defaultMessage: 'Notifications' } + title: { id: 'column.notifications', defaultMessage: 'Notifications' }, + confirm: { id: 'confirmation.label', defaultMessage: 'Are you sure?' } }); const getNotifications = createSelector([ @@ -72,7 +73,9 @@ const Notifications = React.createClass({ }, handleClear () { - this.props.dispatch(clearNotifications()); + if (window.confirm(this.props.intl.formatMessage(messages.confirm))) { + this.props.dispatch(clearNotifications()); + } }, setRef (c) { -- cgit From 46902367247cdb9644d47d4ac9c94d8225b86569 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 2 Mar 2017 19:25:11 +0100 Subject: Make confirmation text more specific --- app/assets/javascripts/components/features/notifications/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/features/notifications/index.jsx b/app/assets/javascripts/components/features/notifications/index.jsx index 8d3f4233d..74b914ffd 100644 --- a/app/assets/javascripts/components/features/notifications/index.jsx +++ b/app/assets/javascripts/components/features/notifications/index.jsx @@ -14,7 +14,7 @@ import ClearColumnButton from './components/clear_column_button'; const messages = defineMessages({ title: { id: 'column.notifications', defaultMessage: 'Notifications' }, - confirm: { id: 'confirmation.label', defaultMessage: 'Are you sure?' } + confirm: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to clear all your notifications?' } }); const getNotifications = createSelector([ -- cgit From 5d100293fba56f2c704436e20a0359da9d3e721e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 4 Mar 2017 00:19:18 +0100 Subject: Rename "the whole known network" timeline to "federated timeline" Remove note about following Gargron@mastodon.social from getting started text --- .../javascripts/components/features/getting_started/index.jsx | 3 +-- app/assets/javascripts/components/locales/en.jsx | 11 +++++------ 2 files changed, 6 insertions(+), 8 deletions(-) (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/features/getting_started/index.jsx b/app/assets/javascripts/components/features/getting_started/index.jsx index f8433b8f4..48b4a6b8e 100644 --- a/app/assets/javascripts/components/features/getting_started/index.jsx +++ b/app/assets/javascripts/components/features/getting_started/index.jsx @@ -45,8 +45,7 @@ const GettingStarted = ({ intl, me }) => {

    -

    -

    tootsuite/mastodon }} />

    +

    tootsuite/mastodon }} />

    diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx index e2aa4247e..3131dca1a 100644 --- a/app/assets/javascripts/components/locales/en.jsx +++ b/app/assets/javascripts/components/locales/en.jsx @@ -25,16 +25,15 @@ const en = { "getting_started.heading": "Getting started", "getting_started.about_addressing": "You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the search form.", "getting_started.about_shortcuts": "If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.", - "getting_started.about_developer": "The developer of this project can be followed as Gargron@mastodon.social", - "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on github at {github}.", + "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on github at {github}. Various apps are available.", "column.home": "Home", - "column.community": "Local", - "column.public": "Whole Known Network", + "column.community": "Local timeline", + "column.public": "Federated timeline", "column.notifications": "Notifications", "tabs_bar.compose": "Compose", "tabs_bar.home": "Home", "tabs_bar.mentions": "Mentions", - "tabs_bar.public": "Whole Known Network", + "tabs_bar.public": "Federated timeline", "tabs_bar.notifications": "Notifications", "compose_form.placeholder": "What is on your mind?", "compose_form.publish": "Toot", @@ -46,7 +45,7 @@ const en = { "navigation_bar.edit_profile": "Edit profile", "navigation_bar.preferences": "Preferences", "navigation_bar.community_timeline": "Local timeline", - "navigation_bar.public_timeline": "Whole Known Network", + "navigation_bar.public_timeline": "Federated timeline", "navigation_bar.logout": "Logout", "reply_indicator.cancel": "Cancel", "search.placeholder": "Search", -- cgit From caf5b8e9757679b93b9a34b0c55f43cb47910201 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 4 Mar 2017 22:17:10 +0100 Subject: Fix #431 - convert gif to webm during upload. Web UI treats them like it did before. In the API, attachments now can be either image, video or gifv. Gifv is to be treated like images in terms of behaviour, but are videos by file type. --- .../javascripts/components/actions/accounts.jsx | 7 +- .../components/extended_video_player.jsx | 21 ++ .../components/components/media_gallery.jsx | 232 ++++++++++++++------- .../javascripts/components/components/status.jsx | 4 +- .../components/components/video_player.jsx | 4 +- .../features/status/components/detailed_status.jsx | 2 +- .../features/ui/containers/modal_container.jsx | 22 +- app/assets/stylesheets/stream_entries.scss | 31 ++- app/models/media_attachment.rb | 69 +++--- app/views/api/v1/media/create.rabl | 4 +- .../stream_entries/_detailed_status.html.haml | 6 +- app/views/stream_entries/_media.html.haml | 4 + app/views/stream_entries/_simple_status.html.haml | 15 +- config/application.rb | 5 +- ...20170304202101_add_type_to_media_attachments.rb | 12 ++ db/schema.rb | 3 +- lib/paperclip/gif_transcoder.rb | 21 ++ 17 files changed, 325 insertions(+), 137 deletions(-) create mode 100644 app/assets/javascripts/components/components/extended_video_player.jsx create mode 100644 app/views/stream_entries/_media.html.haml create mode 100644 db/migrate/20170304202101_add_type_to_media_attachments.rb create mode 100644 lib/paperclip/gif_transcoder.rb (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/assets/javascripts/components/actions/accounts.jsx index 8af0b15d8..05fa8e68d 100644 --- a/app/assets/javascripts/components/actions/accounts.jsx +++ b/app/assets/javascripts/components/actions/accounts.jsx @@ -75,11 +75,16 @@ export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL'; export function fetchAccount(id) { return (dispatch, getState) => { + dispatch(fetchRelationships([id])); + + if (getState().getIn(['accounts', id], null) !== null) { + return; + } + dispatch(fetchAccountRequest(id)); api(getState).get(`/api/v1/accounts/${id}`).then(response => { dispatch(fetchAccountSuccess(response.data)); - dispatch(fetchRelationships([id])); }).catch(error => { dispatch(fetchAccountFail(id, error)); }); diff --git a/app/assets/javascripts/components/components/extended_video_player.jsx b/app/assets/javascripts/components/components/extended_video_player.jsx new file mode 100644 index 000000000..66e5dee16 --- /dev/null +++ b/app/assets/javascripts/components/components/extended_video_player.jsx @@ -0,0 +1,21 @@ +import PureRenderMixin from 'react-addons-pure-render-mixin'; + +const ExtendedVideoPlayer = React.createClass({ + + propTypes: { + src: React.PropTypes.string.isRequired + }, + + mixins: [PureRenderMixin], + + render () { + return ( +
    +
    + ); + }, + +}); + +export default ExtendedVideoPlayer; diff --git a/app/assets/javascripts/components/components/media_gallery.jsx b/app/assets/javascripts/components/components/media_gallery.jsx index b0e397e80..cd2394023 100644 --- a/app/assets/javascripts/components/components/media_gallery.jsx +++ b/app/assets/javascripts/components/components/media_gallery.jsx @@ -43,6 +43,141 @@ const spoilerButtonStyle = { zIndex: '100' }; +const itemStyle = { + boxSizing: 'border-box', + position: 'relative', + float: 'left', + border: 'none', + display: 'block' +}; + +const thumbStyle = { + display: 'block', + width: '100%', + height: '100%', + textDecoration: 'none', + backgroundSize: 'cover', + cursor: 'zoom-in' +}; + +const gifvThumbStyle = { + position: 'relative', + zIndex: '1', + width: '100%', + height: '100%', + objectFit: 'cover', + top: '50%', + transform: 'translateY(-50%)', + cursor: 'zoom-in' +}; + +const Item = React.createClass({ + + propTypes: { + attachment: ImmutablePropTypes.map.isRequired, + index: React.PropTypes.number.isRequired, + size: React.PropTypes.number.isRequired, + onClick: React.PropTypes.func.isRequired + }, + + mixins: [PureRenderMixin], + + handleClick (e) { + const { index, onClick } = this.props; + + if (e.button === 0) { + e.preventDefault(); + onClick(index); + } + + e.stopPropagation(); + }, + + render () { + const { attachment, index, size } = this.props; + + let width = 50; + let height = 100; + let top = 'auto'; + let left = 'auto'; + let bottom = 'auto'; + let right = 'auto'; + + if (size === 1) { + width = 100; + } + + if (size === 4 || (size === 3 && index > 0)) { + height = 50; + } + + if (size === 2) { + if (index === 0) { + right = '2px'; + } else { + left = '2px'; + } + } else if (size === 3) { + if (index === 0) { + right = '2px'; + } else if (index > 0) { + left = '2px'; + } + + if (index === 1) { + bottom = '2px'; + } else if (index > 1) { + top = '2px'; + } + } else if (size === 4) { + if (index === 0 || index === 2) { + right = '2px'; + } + + if (index === 1 || index === 3) { + left = '2px'; + } + + if (index < 2) { + bottom = '2px'; + } else { + top = '2px'; + } + } + + let thumbnail = ''; + + if (attachment.get('type') === 'image') { + thumbnail = ( + + ); + } else if (attachment.get('type') === 'gifv') { + thumbnail = ( + - ); - }); + children = media.take(4).map((attachment, i) => ); } return (
    -
    +
    + {children}
    ); diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx index 110d26c6d..fb49db069 100644 --- a/app/assets/javascripts/components/components/status.jsx +++ b/app/assets/javascripts/components/components/status.jsx @@ -74,8 +74,8 @@ const Status = React.createClass({ } if (status.get('media_attachments').size > 0 && !this.props.muted) { - if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - media = ; + if (status.getIn(['media_attachments', 0, 'type']) === 'video' || (status.get('media_attachments').size === 1 && status.getIn(['media_attachments', 0, 'type']) === 'gifv')) { + media = ; } else { media = ; } diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx index 5c2cef21a..df09da912 100644 --- a/app/assets/javascripts/components/components/video_player.jsx +++ b/app/assets/javascripts/components/components/video_player.jsx @@ -175,7 +175,7 @@ const VideoPlayer = React.createClass({ ); } else { return ( -
    +
    {spoilerButton} @@ -197,7 +197,7 @@ const VideoPlayer = React.createClass({
    {spoilerButton} {muteButton} -
    ); } diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx index caa46ff3c..623872953 100644 --- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx +++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx @@ -38,7 +38,7 @@ const DetailedStatus = React.createClass({ let applicationLink = ''; if (status.get('media_attachments').size > 0) { - if (status.getIn(['media_attachments', 0, 'type']) === 'video') { + if (status.getIn(['media_attachments', 0, 'type']) === 'video' || (status.get('media_attachments').size === 1 && status.getIn(['media_attachments', 0, 'type']) === 'gifv')) { media = ; } else { media = ; diff --git a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx index d8301b20f..e3c4281b9 100644 --- a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx +++ b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx @@ -9,6 +9,7 @@ import ImageLoader from 'react-imageloader'; import LoadingIndicator from '../../../components/loading_indicator'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import ExtendedVideoPlayer from '../../../components/extended_video_player'; const mapStateToProps = state => ({ media: state.getIn(['modal', 'media']), @@ -131,27 +132,34 @@ const Modal = React.createClass({ return null; } - const url = media.get(index).get('url'); + const attachment = media.get(index); + const url = attachment.get('url'); - let leftNav, rightNav; + let leftNav, rightNav, content; - leftNav = rightNav = ''; + leftNav = rightNav = content = ''; if (media.size > 1) { leftNav =
    ; rightNav =
    ; } - return ( - - {leftNav} - + if (attachment.get('type') === 'image') { + content = ( + ); + } else if (attachment.get('type') === 'gifv') { + content = ; + } + return ( + + {leftNav} + {content} {rightNav} ); diff --git a/app/assets/stylesheets/stream_entries.scss b/app/assets/stylesheets/stream_entries.scss index 3b2e88f6d..b9a9a1da3 100644 --- a/app/assets/stylesheets/stream_entries.scss +++ b/app/assets/stylesheets/stream_entries.scss @@ -104,8 +104,12 @@ overflow: hidden; width: 100%; box-sizing: border-box; - height: 110px; - display: flex; + position: relative; + + .status__attachments__inner { + display: flex; + height: 214px; + } } } @@ -184,8 +188,12 @@ overflow: hidden; width: 100%; box-sizing: border-box; - height: 300px; - display: flex; + position: relative; + + .status__attachments__inner { + display: flex; + height: 360px; + } } .video-player { @@ -231,11 +239,19 @@ text-decoration: none; cursor: zoom-in; } + + video { + position: relative; + z-index: 1; + width: 100%; + height: 100%; + object-fit: cover; + top: 50%; + transform: translateY(-50%); + } } .video-item { - max-width: 196px; - a { cursor: pointer; } @@ -258,6 +274,9 @@ width: 100%; height: 100%; cursor: pointer; + position: absolute; + top: 0; + left: 0; display: flex; align-items: center; justify-content: center; diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 6925f9b0d..620a92dbc 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -1,15 +1,32 @@ # frozen_string_literal: true class MediaAttachment < ApplicationRecord + self.inheritance_column = nil + + enum type: [:image, :gifv, :video] + IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze + IMAGE_STYLES = { original: '1280x1280>', small: '400x400>' }.freeze + VIDEO_STYLES = { + small: { + convert_options: { + output: { + vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease', + }, + }, + format: 'png', + time: 0, + }, + }.freeze + belongs_to :account, inverse_of: :media_attachments belongs_to :status, inverse_of: :media_attachments has_attached_file :file, - styles: -> (f) { file_styles f }, - processors: -> (f) { f.video? ? [:transcoder] : [:thumbnail] }, + styles: ->(f) { file_styles f }, + processors: ->(f) { file_processors f }, convert_options: { all: '-quality 90 -strip' } validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES validates_attachment_size :file, less_than: 8.megabytes @@ -27,45 +44,45 @@ class MediaAttachment < ApplicationRecord self.file = URI.parse(url) end - def image? - IMAGE_MIME_TYPES.include? file_content_type - end - - def video? - VIDEO_MIME_TYPES.include? file_content_type - end - - def type - image? ? 'image' : 'video' - end - def to_param shortcode end before_create :set_shortcode + before_post_process :set_type class << self private def file_styles(f) - if f.instance.image? + if f.instance.file_content_type == 'image/gif' { - original: '1280x1280>', - small: '400x400>', - } - else - { - small: { + small: IMAGE_STYLES[:small], + original: { + format: 'webm', convert_options: { output: { - vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease', + 'c:v' => 'libvpx', + 'crf' => 6, + 'b:v' => '500K', }, }, - format: 'png', - time: 1, }, } + elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type + IMAGE_STYLES + else + VIDEO_STYLES + end + end + + def file_processors(f) + if f.file_content_type == 'image/gif' + [:gif_transcoder] + elsif VIDEO_MIME_TYPES.include? f.file_content_type + [:transcoder] + else + [:thumbnail] end end end @@ -80,4 +97,8 @@ class MediaAttachment < ApplicationRecord break if MediaAttachment.find_by(shortcode: shortcode).nil? end end + + def set_type + self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : :image + end end diff --git a/app/views/api/v1/media/create.rabl b/app/views/api/v1/media/create.rabl index 0b42e6e3d..916217cbd 100644 --- a/app/views/api/v1/media/create.rabl +++ b/app/views/api/v1/media/create.rabl @@ -1,5 +1,5 @@ object @media attribute :id, :type -node(:url) { |media| full_asset_url(media.file.url( :original)) } -node(:preview_url) { |media| full_asset_url(media.file.url( :small)) } +node(:url) { |media| full_asset_url(media.file.url(:original)) } +node(:preview_url) { |media| full_asset_url(media.file.url(:small)) } node(:text_url) { |media| medium_url(media) } diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index 6c1c1ce84..8c0456b1f 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -22,9 +22,9 @@ .detailed-status__attachments - if status.sensitive? = render partial: 'stream_entries/content_spoiler' - - status.media_attachments.each do |media| - .media-item - = link_to '', (media.remote_url.blank? ? media.file.url(:original) : media.remote_url), style: "background-image: url(#{media.file.url(:original)})", target: '_blank', rel: 'noopener', class: "u-#{media.video? ? 'video' : 'photo'}" + .status__attachments__inner + - status.media_attachments.each do |media| + = render partial: 'stream_entries/media', locals: { media: media } %div.detailed-status__meta %data.dt-published{ value: status.created_at.to_time.iso8601 } diff --git a/app/views/stream_entries/_media.html.haml b/app/views/stream_entries/_media.html.haml new file mode 100644 index 000000000..cd7faa700 --- /dev/null +++ b/app/views/stream_entries/_media.html.haml @@ -0,0 +1,4 @@ +.media-item + = link_to media.remote_url.blank? ? media.file.url(:original) : media.remote_url, style: media.image? ? "background-image: url(#{media.file.url(:original)})" : "", target: '_blank', rel: 'noopener', class: "u-#{media.video? || media.gifv? ? 'video' : 'photo'}" do + - unless media.image? + %video{ src: media.file.url(:original), autoplay: true, loop: true }/ diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index 52ad39220..cb2c976ce 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -22,11 +22,12 @@ - if status.sensitive? = render partial: 'stream_entries/content_spoiler' - if status.media_attachments.first.video? - .video-item - = link_to (status.media_attachments.first.remote_url.blank? ? status.media_attachments.first.file.url(:original) : status.media_attachments.first.remote_url), style: "background-image: url(#{status.media_attachments.first.file.url(:small)})", target: '_blank', rel: 'noopener', class: 'u-video' do - .video-item__play - = fa_icon('play') + .status__attachments__inner + .video-item + = link_to (status.media_attachments.first.remote_url.blank? ? status.media_attachments.first.file.url(:original) : status.media_attachments.first.remote_url), style: "background-image: url(#{status.media_attachments.first.file.url(:small)})", target: '_blank', rel: 'noopener', class: 'u-video' do + .video-item__play + = fa_icon('play') - else - - status.media_attachments.each do |media| - .media-item - = link_to '', (media.remote_url.blank? ? media.file.url(:original) : media.remote_url), style: "background-image: url(#{media.file.url(:original)})", target: '_blank', rel: 'noopener', class: "u-#{media.video? ? 'video' : 'photo'}" + .status__attachments__inner + - status.media_attachments.each do |media| + = render partial: 'stream_entries/media', locals: { media: media } diff --git a/config/application.rb b/config/application.rb index 1ea65619c..30ed608c5 100644 --- a/config/application.rb +++ b/config/application.rb @@ -2,12 +2,13 @@ require_relative 'boot' require 'rails/all' -require_relative '../app/lib/exceptions' - # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) +require_relative '../app/lib/exceptions' +require_relative '../lib/paperclip/gif_transcoder' + Dotenv::Railtie.load module Mastodon diff --git a/db/migrate/20170304202101_add_type_to_media_attachments.rb b/db/migrate/20170304202101_add_type_to_media_attachments.rb new file mode 100644 index 000000000..514079958 --- /dev/null +++ b/db/migrate/20170304202101_add_type_to_media_attachments.rb @@ -0,0 +1,12 @@ +class AddTypeToMediaAttachments < ActiveRecord::Migration[5.0] + def up + add_column :media_attachments, :type, :integer, default: 0, null: false + + MediaAttachment.where(file_content_type: MediaAttachment::IMAGE_MIME_TYPES).update_all(type: MediaAttachment.types[:image]) + MediaAttachment.where(file_content_type: MediaAttachment::VIDEO_MIME_TYPES).update_all(type: MediaAttachment.types[:video]) + end + + def down + remove_column :media_attachments, :type + end +end diff --git a/db/schema.rb b/db/schema.rb index 8cc3bd8e3..4ec85ef2b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170303212857) do +ActiveRecord::Schema.define(version: 20170304202101) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -98,6 +98,7 @@ ActiveRecord::Schema.define(version: 20170303212857) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "shortcode" + t.integer "type", default: 0, null: false t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true, using: :btree t.index ["status_id"], name: "index_media_attachments_on_status_id", using: :btree end diff --git a/lib/paperclip/gif_transcoder.rb b/lib/paperclip/gif_transcoder.rb new file mode 100644 index 000000000..33d2c4a01 --- /dev/null +++ b/lib/paperclip/gif_transcoder.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Paperclip + # This transcoder is only to be used for the MediaAttachment model + # to convert animated gifs to webm + class GifTranscoder < Paperclip::Processor + def make + num_frames = identify('-format %n :file', file: file.path).to_i + + return file unless options[:style] == :original && num_frames > 1 + + final_file = Paperclip::Transcoder.make(file, options, attachment) + + attachment.instance.file_file_name = 'media.webm' + attachment.instance.file_content_type = 'video/webm' + attachment.instance.type = MediaAttachment.types[:gifv] + + final_file + end + end +end -- cgit From 07b166af64ff3e201b944906835abac7100da706 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 5 Mar 2017 02:04:31 +0100 Subject: Make gifvs always use media gallery component --- app/assets/javascripts/components/components/status.jsx | 4 ++-- .../components/features/status/components/detailed_status.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'app/assets/javascripts/components/features') diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx index fb49db069..110d26c6d 100644 --- a/app/assets/javascripts/components/components/status.jsx +++ b/app/assets/javascripts/components/components/status.jsx @@ -74,8 +74,8 @@ const Status = React.createClass({ } if (status.get('media_attachments').size > 0 && !this.props.muted) { - if (status.getIn(['media_attachments', 0, 'type']) === 'video' || (status.get('media_attachments').size === 1 && status.getIn(['media_attachments', 0, 'type']) === 'gifv')) { - media = ; + if (status.getIn(['media_attachments', 0, 'type']) === 'video') { + media = ; } else { media = ; } diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx index 623872953..caa46ff3c 100644 --- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx +++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx @@ -38,7 +38,7 @@ const DetailedStatus = React.createClass({ let applicationLink = ''; if (status.get('media_attachments').size > 0) { - if (status.getIn(['media_attachments', 0, 'type']) === 'video' || (status.get('media_attachments').size === 1 && status.getIn(['media_attachments', 0, 'type']) === 'gifv')) { + if (status.getIn(['media_attachments', 0, 'type']) === 'video') { media = ; } else { media = ; -- cgit