From 6748a5acb1fc0e4652dde1ee6544d4ae2d6d4f26 Mon Sep 17 00:00:00 2001 From: Taras Gogol Date: Fri, 8 May 2020 21:17:16 +0300 Subject: Fix followings list order | Issue #13538 (#13676) --- spec/models/relationship_filter_spec.rb | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 spec/models/relationship_filter_spec.rb (limited to 'spec/models') diff --git a/spec/models/relationship_filter_spec.rb b/spec/models/relationship_filter_spec.rb new file mode 100644 index 000000000..7c0f37a06 --- /dev/null +++ b/spec/models/relationship_filter_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe RelationshipFilter do + let(:account) { Fabricate(:account) } + + describe '#results' do + context 'when default params are used' do + let(:subject) do + RelationshipFilter.new(account, 'order' => 'active').results + end + + before do + add_following_account_with(last_status_at: 7.days.ago) + add_following_account_with(last_status_at: 1.day.ago) + add_following_account_with(last_status_at: 3.days.ago) + end + + it 'returns followings ordered by last activity' do + expected_result = account.following.eager_load(:account_stat).reorder(nil).by_recent_status + + expect(subject).to eq expected_result + end + end + end + + def add_following_account_with(last_status_at:) + following_account = Fabricate(:account) + Fabricate(:account_stat, account: following_account, + last_status_at: last_status_at, + statuses_count: 1, + following_count: 0, + followers_count: 0) + Fabricate(:follow, account: account, target_account: following_account).account + end +end -- cgit From a4240fd0272eb79b7d99cccfa7d14e8a1e12921d Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 10 May 2020 09:50:54 +0200 Subject: Improve RSS entries for statuses (#13592) * Improve RSS entries for statuses - Render polls in both accounts and tags serializers - Refactor RSS serializers - Change title preview to include ellipsis when truncated - Change title preview to show CW instead of toot text - Add tests * Remove title from OEmbed serialization Twitter doesn't serialize title either, and tihs allows us to move the title formatting code to the RSS serializers. --- app/lib/rss/serializer.rb | 38 +++++++++++++++++++ app/models/status.rb | 8 ---- app/serializers/oembed_serializer.rb | 2 +- app/serializers/rss/account_serializer.rb | 15 +------- app/serializers/rss/tag_serializer.rb | 15 +------- spec/lib/rss/serializer_spec.rb | 63 +++++++++++++++++++++++++++++++ spec/models/status_spec.rb | 29 -------------- 7 files changed, 106 insertions(+), 64 deletions(-) create mode 100644 app/lib/rss/serializer.rb create mode 100644 spec/lib/rss/serializer_spec.rb (limited to 'spec/models') diff --git a/app/lib/rss/serializer.rb b/app/lib/rss/serializer.rb new file mode 100644 index 000000000..fd56c568c --- /dev/null +++ b/app/lib/rss/serializer.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class RSS::Serializer + private + + def render_statuses(builder, statuses) + statuses.each do |status| + builder.item do |item| + item.title(status_title(status)) + .link(ActivityPub::TagManager.instance.url_for(status)) + .pub_date(status.created_at) + .description(status.spoiler_text.presence || Formatter.instance.format(status, inline_poll_options: true).to_str) + + status.media_attachments.each do |media| + item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size) + end + end + end + end + + def status_title(status) + return "#{status.account.acct} deleted status" if status.destroyed? + + preview = status.proper.spoiler_text.presence || status.proper.text + if preview.length > 30 || preview[0, 30].include?("\n") + preview = preview[0, 30] + preview = preview[0, preview.index("\n").presence || 30] + '…' + end + + preview = "#{status.proper.spoiler_text.present? ? 'CW ' : ''}“#{preview}”#{status.proper.sensitive? ? ' (sensitive)' : ''}" + + if status.reblog? + "#{status.account.acct} boosted #{status.reblog.account.acct}: #{preview}" + else + "#{status.account.acct}: #{preview}" + end + end +end diff --git a/app/models/status.rb b/app/models/status.rb index a938ff032..8c7fe8dfa 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -197,14 +197,6 @@ class Status < ApplicationRecord preview_cards.first end - def title - if destroyed? - "#{account.acct} deleted status" - else - reblog? ? "#{account.acct} shared a status by #{reblog.account.acct}" : "New status by #{account.acct}" - end - end - def hidden? !distributable? end diff --git a/app/serializers/oembed_serializer.rb b/app/serializers/oembed_serializer.rb index 01689633b..d6261d724 100644 --- a/app/serializers/oembed_serializer.rb +++ b/app/serializers/oembed_serializer.rb @@ -4,7 +4,7 @@ class OEmbedSerializer < ActiveModel::Serializer include RoutingHelper include ActionView::Helpers::TagHelper - attributes :type, :version, :title, :author_name, + attributes :type, :version, :author_name, :author_url, :provider_name, :provider_url, :cache_age, :html, :width, :height diff --git a/app/serializers/rss/account_serializer.rb b/app/serializers/rss/account_serializer.rb index ee972ff96..81e24af0d 100644 --- a/app/serializers/rss/account_serializer.rb +++ b/app/serializers/rss/account_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class RSS::AccountSerializer +class RSS::AccountSerializer < RSS::Serializer include ActionView::Helpers::NumberHelper include AccountsHelper include RoutingHelper @@ -17,18 +17,7 @@ class RSS::AccountSerializer builder.image(full_asset_url(account.avatar.url(:original))) if account.avatar? builder.cover(full_asset_url(account.header.url(:original))) if account.header? - statuses.each do |status| - builder.item do |item| - item.title(status.title) - .link(ActivityPub::TagManager.instance.url_for(status)) - .pub_date(status.created_at) - .description(status.spoiler_text.presence || Formatter.instance.format(status, inline_poll_options: true).to_str) - - status.media_attachments.each do |media| - item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size) - end - end - end + render_statuses(builder, statuses) builder.to_xml end diff --git a/app/serializers/rss/tag_serializer.rb b/app/serializers/rss/tag_serializer.rb index ea26189a2..e549ac367 100644 --- a/app/serializers/rss/tag_serializer.rb +++ b/app/serializers/rss/tag_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class RSS::TagSerializer +class RSS::TagSerializer < RSS::Serializer include ActionView::Helpers::NumberHelper include ActionView::Helpers::SanitizeHelper include RoutingHelper @@ -14,18 +14,7 @@ class RSS::TagSerializer .logo(full_pack_url('media/images/logo.svg')) .accent_color('2b90d9') - statuses.each do |status| - builder.item do |item| - item.title(status.title) - .link(ActivityPub::TagManager.instance.url_for(status)) - .pub_date(status.created_at) - .description(status.spoiler_text.presence || Formatter.instance.format(status).to_str) - - status.media_attachments.each do |media| - item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size) - end - end - end + render_statuses(builder, statuses) builder.to_xml end diff --git a/spec/lib/rss/serializer_spec.rb b/spec/lib/rss/serializer_spec.rb new file mode 100644 index 000000000..0364d13de --- /dev/null +++ b/spec/lib/rss/serializer_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe RSS::Serializer do + describe '#status_title' do + let(:text) { 'This is a toot' } + let(:spoiler) { '' } + let(:sensitive) { false } + let(:reblog) { nil } + let(:account) { Fabricate(:account) } + let(:status) { Fabricate(:status, account: account, text: text, spoiler_text: spoiler, sensitive: sensitive, reblog: reblog) } + + subject { RSS::Serializer.new.send(:status_title, status) } + + context 'if destroyed?' do + it 'returns "#{account.acct} deleted status"' do + status.destroy! + expect(subject).to eq "#{account.acct} deleted status" + end + end + + context 'on a toot with long text' do + let(:text) { "This toot's text is longer than the allowed number of characters" } + + it 'truncates toot text appropriately' do + expect(subject).to eq "#{account.acct}: “This toot's text is longer tha…”" + end + end + + context 'on a toot with long text with a newline' do + let(:text) { "This toot's text is longer\nthan the allowed number of characters" } + + it 'truncates toot text appropriately' do + expect(subject).to eq "#{account.acct}: “This toot's text is longer…”" + end + end + + context 'on a toot with a content warning' do + let(:spoiler) { 'long toot' } + + it 'displays spoiler text instead of toot content' do + expect(subject).to eq "#{account.acct}: CW “long toot”" + end + end + + context 'on a toot with sensitive media' do + let(:sensitive) { true } + + it 'displays that the media is sensitive' do + expect(subject).to eq "#{account.acct}: “This is a toot” (sensitive)" + end + end + + context 'on a reblog' do + let(:reblog) { Fabricate(:status, text: 'This is a toot') } + + it 'display that the toot is a reblog' do + expect(subject).to eq "#{account.acct} boosted #{reblog.account.acct}: “This is a toot”" + end + end + end +end diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index 51a10cd17..9c1eca1b5 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -82,35 +82,6 @@ RSpec.describe Status, type: :model do end end - describe '#title' do - # rubocop:disable Style/InterpolationCheck - - let(:account) { subject.account } - - context 'if destroyed?' do - it 'returns "#{account.acct} deleted status"' do - subject.destroy! - expect(subject.title).to eq "#{account.acct} deleted status" - end - end - - context 'unless destroyed?' do - context 'if reblog?' do - it 'returns "#{account.acct} shared a status by #{reblog.account.acct}"' do - reblog = subject.reblog = other - expect(subject.title).to eq "#{account.acct} shared a status by #{reblog.account.acct}" - end - end - - context 'unless reblog?' do - it 'returns "New status by #{account.acct}"' do - subject.reblog = nil - expect(subject.title).to eq "New status by #{account.acct}" - end - end - end - end - describe '#hidden?' do context 'if private_visibility?' do it 'returns true' do -- cgit From 26b08a3c54847f2816f78c3ac67ace001d3fea1f Mon Sep 17 00:00:00 2001 From: Takeshi Umeda Date: Sun, 10 May 2020 17:36:18 +0900 Subject: Add remote only to public timeline (#13504) * Add remote only to public timeline * Fix code style --- .../api/v1/timelines/public_controller.rb | 4 +-- app/javascript/mastodon/actions/streaming.js | 2 +- app/javascript/mastodon/actions/timelines.js | 2 +- .../public_timeline/components/column_settings.js | 30 ++++++++++++++++++++++ .../containers/column_settings_container.js | 2 +- .../mastodon/features/public_timeline/index.js | 29 +++++++++++---------- .../features/ui/components/columns_area.js | 1 + app/models/status.rb | 14 +++++++--- spec/models/status_spec.rb | 27 +++++++++++++++++++ streaming/index.js | 16 ++++++++++++ 10 files changed, 106 insertions(+), 21 deletions(-) create mode 100644 app/javascript/mastodon/features/public_timeline/components/column_settings.js (limited to 'spec/models') diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index 581befef1..c6e7854d9 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -39,7 +39,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController end def public_timeline_statuses - Status.as_public_timeline(current_account, truthy_param?(:local)) + Status.as_public_timeline(current_account, truthy_param?(:remote) ? :remote : truthy_param?(:local)) end def insert_pagination_headers @@ -47,7 +47,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController end def pagination_params(core_params) - params.slice(:local, :limit, :only_media).permit(:local, :limit, :only_media).merge(core_params) + params.slice(:local, :remote, :limit, :only_media).permit(:local, :remote, :limit, :only_media).merge(core_params) end def next_path diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 79b08bdda..080d665f4 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -73,7 +73,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => { export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`); -export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`); +export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`); export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept); export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`); diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 861827d33..01f0fb015 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -107,7 +107,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { }; export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); -export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done); +export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); diff --git a/app/javascript/mastodon/features/public_timeline/components/column_settings.js b/app/javascript/mastodon/features/public_timeline/components/column_settings.js new file mode 100644 index 000000000..756b6fe06 --- /dev/null +++ b/app/javascript/mastodon/features/public_timeline/components/column_settings.js @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl, FormattedMessage } from 'react-intl'; +import SettingToggle from '../../notifications/components/setting_toggle'; + +export default @injectIntl +class ColumnSettings extends React.PureComponent { + + static propTypes = { + settings: ImmutablePropTypes.map.isRequired, + onChange: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + columnId: PropTypes.string, + }; + + render () { + const { settings, onChange } = this.props; + + return ( +
+
+ } /> + } /> +
+
+ ); + } + +} diff --git a/app/javascript/mastodon/features/public_timeline/containers/column_settings_container.js b/app/javascript/mastodon/features/public_timeline/containers/column_settings_container.js index c56caa59e..8c9e8aef4 100644 --- a/app/javascript/mastodon/features/public_timeline/containers/column_settings_container.js +++ b/app/javascript/mastodon/features/public_timeline/containers/column_settings_container.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import ColumnSettings from '../../community_timeline/components/column_settings'; +import ColumnSettings from '../components/column_settings'; import { changeSetting } from '../../../actions/settings'; import { changeColumnParams } from '../../../actions/columns'; diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js index 7aabd7f6e..988b1b070 100644 --- a/app/javascript/mastodon/features/public_timeline/index.js +++ b/app/javascript/mastodon/features/public_timeline/index.js @@ -19,11 +19,13 @@ const mapStateToProps = (state, { columnId }) => { const columns = state.getIn(['settings', 'columns']); const index = columns.findIndex(c => c.get('uuid') === uuid); const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']); + const onlyRemote = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyRemote']) : state.getIn(['settings', 'public', 'other', 'onlyRemote']); const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]); return { hasUnread: !!timelineState && timelineState.get('unread') > 0, onlyMedia, + onlyRemote, }; }; @@ -47,15 +49,16 @@ class PublicTimeline extends React.PureComponent { multiColumn: PropTypes.bool, hasUnread: PropTypes.bool, onlyMedia: PropTypes.bool, + onlyRemote: PropTypes.bool, }; handlePin = () => { - const { columnId, dispatch, onlyMedia } = this.props; + const { columnId, dispatch, onlyMedia, onlyRemote } = this.props; if (columnId) { dispatch(removeColumn(columnId)); } else { - dispatch(addColumn('PUBLIC', { other: { onlyMedia } })); + dispatch(addColumn(onlyRemote ? 'REMOTE' : 'PUBLIC', { other: { onlyMedia, onlyRemote } })); } } @@ -69,19 +72,19 @@ class PublicTimeline extends React.PureComponent { } componentDidMount () { - const { dispatch, onlyMedia } = this.props; + const { dispatch, onlyMedia, onlyRemote } = this.props; - dispatch(expandPublicTimeline({ onlyMedia })); - this.disconnect = dispatch(connectPublicStream({ onlyMedia })); + dispatch(expandPublicTimeline({ onlyMedia, onlyRemote })); + this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote })); } componentDidUpdate (prevProps) { - if (prevProps.onlyMedia !== this.props.onlyMedia) { - const { dispatch, onlyMedia } = this.props; + if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote) { + const { dispatch, onlyMedia, onlyRemote } = this.props; this.disconnect(); - dispatch(expandPublicTimeline({ onlyMedia })); - this.disconnect = dispatch(connectPublicStream({ onlyMedia })); + dispatch(expandPublicTimeline({ onlyMedia, onlyRemote })); + this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote })); } } @@ -97,13 +100,13 @@ class PublicTimeline extends React.PureComponent { } handleLoadMore = maxId => { - const { dispatch, onlyMedia } = this.props; + const { dispatch, onlyMedia, onlyRemote } = this.props; - dispatch(expandPublicTimeline({ maxId, onlyMedia })); + dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote })); } render () { - const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia } = this.props; + const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props; const pinned = !!columnId; return ( @@ -122,7 +125,7 @@ class PublicTimeline extends React.PureComponent { { 'public:media', 'public:local', 'public:local:media', + 'public:remote', + 'public:remote:media', 'hashtag', 'hashtag:local', ]; @@ -297,6 +299,7 @@ const startWorker = (workerId) => { const PUBLIC_ENDPOINTS = [ '/api/v1/streaming/public', '/api/v1/streaming/public/local', + '/api/v1/streaming/public/remote', '/api/v1/streaming/hashtag', '/api/v1/streaming/hashtag/local', ]; @@ -535,6 +538,13 @@ const startWorker = (workerId) => { streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true); }); + app.get('/api/v1/streaming/public/remote', (req, res) => { + const onlyMedia = req.query.only_media === '1' || req.query.only_media === 'true'; + const channel = onlyMedia ? 'timeline:public:remote:media' : 'timeline:public:remote'; + + streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true); + }); + app.get('/api/v1/streaming/direct', (req, res) => { const channel = `timeline:direct:${req.accountId}`; streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel)), true); @@ -599,12 +609,18 @@ const startWorker = (workerId) => { case 'public:local': streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; + case 'public:remote': + streamFrom('timeline:public:remote', req, streamToWs(req, ws), streamWsEnd(req, ws), true); + break; case 'public:media': streamFrom('timeline:public:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; case 'public:local:media': streamFrom('timeline:public:local:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; + case 'public:remote:media': + streamFrom('timeline:public:remote:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); + break; case 'direct': channel = `timeline:direct:${req.accountId}`; streamFrom(channel, req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel)), true); -- cgit