diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb
index c4f600c54a..4578cf6ca6 100644
--- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb
@@ -19,6 +19,8 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
end
def load_accounts
+ return [] if @account.user_hides_network? && current_account.id != @account.id
+
default_accounts.merge(paginated_follows).to_a
end
diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb
index 90b1f7fc51..ce2bbda855 100644
--- a/app/controllers/api/v1/accounts/following_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb
@@ -19,6 +19,8 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
end
def load_accounts
+ return [] if @account.user_hides_network? && current_account.id != @account.id
+
default_accounts.merge(paginated_follows).to_a
end
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index 30bf733ba1..f5670c6bf8 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -8,11 +8,15 @@ class FollowerAccountsController < ApplicationController
format.html do
use_pack 'public'
+ next if @account.user_hides_network?
+
follows
@relationships = AccountRelationshipsPresenter.new(follows.map(&:account_id), current_user.account_id) if user_signed_in?
end
format.json do
+ raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
+
render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index e7cd587392..098b2a20cb 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -8,11 +8,15 @@ class FollowingAccountsController < ApplicationController
format.html do
use_pack 'public'
+ next if @account.user_hides_network?
+
follows
@relationships = AccountRelationshipsPresenter.new(follows.map(&:target_account_id), current_user.account_id) if user_signed_in?
end
format.json do
+ raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
+
render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index c853b5ab7a..425664d496 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -40,6 +40,7 @@ class Settings::PreferencesController < Settings::BaseController
:setting_reduce_motion,
:setting_system_font_ui,
:setting_noindex,
+ :setting_hide_network,
notification_emails: %i(follow follow_request reblog favourite mention digest),
interactions: %i(must_be_follower must_be_following)
)
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index 393268811e..641ad0e144 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -76,9 +76,14 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
-export function expandNotifications({ maxId } = {}) {
+const noOp = () => {};
+
+export function expandNotifications({ maxId } = {}, done = noOp) {
return (dispatch, getState) => {
- if (getState().getIn(['notifications', 'isLoading'])) {
+ const notifications = getState().get('notifications');
+
+ if (notifications.get('isLoading')) {
+ done();
return;
}
@@ -87,6 +92,10 @@ export function expandNotifications({ maxId } = {}) {
exclude_types: excludeTypesFromSettings(getState()),
};
+ if (!maxId && notifications.get('items').size > 0) {
+ params.since_id = notifications.getIn(['items', 0]);
+ }
+
dispatch(expandNotificationsRequest());
api(getState).get('/api/v1/notifications', { params }).then(response => {
@@ -97,8 +106,10 @@ export function expandNotifications({ maxId } = {}) {
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));
fetchRelatedRelationships(dispatch, response.data);
+ done();
}).catch(error => {
dispatch(expandNotificationsFail(error));
+ done();
});
};
};
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index 14215ab6d5..10e68bf3aa 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -36,10 +36,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null)
});
}
-function refreshHomeTimelineAndNotification (dispatch) {
- dispatch(expandHomeTimeline());
- dispatch(expandNotifications());
-}
+const refreshHomeTimelineAndNotification = (dispatch, done) => {
+ dispatch(expandHomeTimeline({}, () => dispatch(expandNotifications({}, done))));
+};
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
export const connectCommunityStream = () => connectTimelineStream('community', 'public:local');
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index eca847ee73..8bcfe4db9c 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -1,6 +1,6 @@
import { importFetchedStatus, importFetchedStatuses } from './importer';
import api, { getLinks } from '../api';
-import { Map as ImmutableMap } from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
@@ -64,35 +64,44 @@ export function deleteFromTimelines(id) {
};
};
-export function expandTimeline(timelineId, path, params = {}) {
+const noOp = () => {};
+
+export function expandTimeline(timelineId, path, params = {}, done = noOp) {
return (dispatch, getState) => {
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
if (timeline.get('isLoading')) {
+ done();
return;
}
+ if (!params.max_id && timeline.get('items', ImmutableList()).size > 0) {
+ params.since_id = timeline.getIn(['items', 0]);
+ }
+
dispatch(expandTimelineRequest(timelineId));
api(getState).get(path, { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206));
+ done();
}).catch(error => {
dispatch(expandTimelineFail(timelineId, error));
+ done();
});
};
};
-export const expandHomeTimeline = ({ maxId } = {}) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId });
-export const expandPublicTimeline = ({ maxId } = {}) => expandTimeline('public', '/api/v1/timelines/public', { max_id: maxId });
-export const expandCommunityTimeline = ({ maxId } = {}) => expandTimeline('community', '/api/v1/timelines/public', { local: true, max_id: maxId });
-export const expandDirectTimeline = ({ maxId } = {}) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId });
+export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
+export const expandPublicTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('public', '/api/v1/timelines/public', { max_id: maxId }, done);
+export const expandCommunityTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('community', '/api/v1/timelines/public', { local: true, max_id: maxId }, done);
+export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, 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 });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true });
-export const expandHashtagTimeline = (hashtag, { maxId } = {}) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId });
-export const expandListTimeline = (id, { maxId } = {}) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId });
+export const expandHashtagTimeline = (hashtag, { maxId } = {}, done = noOp) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }, done);
+export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
export function expandTimelineRequest(timeline) {
return {
diff --git a/app/javascript/mastodon/stream.js b/app/javascript/mastodon/stream.js
index 6c67ba2755..9928d0dd76 100644
--- a/app/javascript/mastodon/stream.js
+++ b/app/javascript/mastodon/stream.js
@@ -1,21 +1,24 @@
import WebSocketClient from 'websocket.js';
+const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max));
+
export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onDisconnect() {}, onReceive() {} })) {
return (dispatch, getState) => {
const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
const accessToken = getState().getIn(['meta', 'access_token']);
const { onDisconnect, onReceive } = callbacks(dispatch, getState);
+
let polling = null;
const setupPolling = () => {
- polling = setInterval(() => {
- pollingRefresh(dispatch);
- }, 20000);
+ pollingRefresh(dispatch, () => {
+ polling = setTimeout(() => setupPolling(), 20000 + randomIntUpTo(20000));
+ });
};
const clearPolling = () => {
if (polling) {
- clearInterval(polling);
+ clearTimeout(polling);
polling = null;
}
};
@@ -29,8 +32,9 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
disconnected () {
if (pollingRefresh) {
- setupPolling();
+ polling = setTimeout(() => setupPolling(), randomIntUpTo(40000));
}
+
onDisconnect();
},
@@ -51,6 +55,7 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
if (subscription) {
subscription.close();
}
+
clearPolling();
};
diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss
index b063ca52dd..93aa134cf4 100644
--- a/app/javascript/styles/mastodon/accounts.scss
+++ b/app/javascript/styles/mastodon/accounts.scss
@@ -322,6 +322,15 @@
z-index: 2;
position: relative;
+ &.empty img {
+ position: absolute;
+ opacity: 0.2;
+ height: 200px;
+ left: 0;
+ bottom: 0;
+ pointer-events: none;
+ }
+
@media screen and (max-width: 740px) {
border-radius: 0;
box-shadow: none;
@@ -438,8 +447,8 @@
font-size: 14px;
font-weight: 500;
text-align: center;
- padding: 60px 0;
- padding-top: 55px;
+ padding: 130px 0;
+ padding-top: 125px;
margin: 0 auto;
cursor: default;
}
diff --git a/app/javascript/styles/mastodon/footer.scss b/app/javascript/styles/mastodon/footer.scss
index ba2a06954e..dd3c1b6884 100644
--- a/app/javascript/styles/mastodon/footer.scss
+++ b/app/javascript/styles/mastodon/footer.scss
@@ -4,7 +4,7 @@
font-size: 12px;
color: $darker-text-color;
- .domain {
+ .footer__domain {
font-weight: 500;
a {
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 84d4b17520..03476920b2 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -118,4 +118,13 @@ class ActivityPub::Activity
def delete_later!(uri)
redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri)
end
+
+ def fetch_remote_original_status
+ if object_uri.start_with?('http')
+ return if ActivityPub::TagManager.instance.local_uri?(object_uri)
+ ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true, on_behalf_of: @account.followers.local.first)
+ elsif @object['url'].present?
+ ::FetchRemoteStatusService.new.call(@object['url'])
+ end
+ end
end
diff --git a/app/lib/activitypub/activity/add.rb b/app/lib/activitypub/activity/add.rb
index ea94d2f983..688ab00b33 100644
--- a/app/lib/activitypub/activity/add.rb
+++ b/app/lib/activitypub/activity/add.rb
@@ -4,9 +4,10 @@ class ActivityPub::Activity::Add < ActivityPub::Activity
def perform
return unless @json['target'].present? && value_or_id(@json['target']) == @account.featured_collection_url
- status = status_from_uri(object_uri)
+ status = status_from_uri(object_uri)
+ status ||= fetch_remote_original_status
- return unless status.account_id == @account.id && !@account.pinned?(status)
+ return unless !status.nil? && status.account_id == @account.id && !@account.pinned?(status)
StatusPin.create!(account: @account, status: status)
end
diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb
index f810c88a27..1147a4481f 100644
--- a/app/lib/activitypub/activity/announce.rb
+++ b/app/lib/activitypub/activity/announce.rb
@@ -26,16 +26,6 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
private
- def fetch_remote_original_status
- if object_uri.start_with?('http')
- return if ActivityPub::TagManager.instance.local_uri?(object_uri)
-
- ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true, on_behalf_of: @account.followers.local.first)
- elsif @object['url'].present?
- ::FetchRemoteStatusService.new.call(@object['url'])
- end
- end
-
def announceable?(status)
status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility?
end
diff --git a/app/lib/activitypub/activity/remove.rb b/app/lib/activitypub/activity/remove.rb
index 62a1e3196e..f523ead9f6 100644
--- a/app/lib/activitypub/activity/remove.rb
+++ b/app/lib/activitypub/activity/remove.rb
@@ -6,7 +6,7 @@ class ActivityPub::Activity::Remove < ActivityPub::Activity
status = status_from_uri(object_uri)
- return unless status.account_id == @account.id
+ return unless !status.nil? && status.account_id == @account.id
pin = StatusPin.find_by(account: @account, status: status)
pin&.destroy!
diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb
index 78b3aa77cc..f8bacb036c 100644
--- a/app/lib/user_settings_decorator.rb
+++ b/app/lib/user_settings_decorator.rb
@@ -30,6 +30,7 @@ class UserSettingsDecorator
user.settings['noindex'] = noindex_preference if change?('setting_noindex')
user.settings['flavour'] = flavour_preference if change?('setting_flavour')
user.settings['skin'] = skin_preference if change?('setting_skin')
+ user.settings['hide_network'] = hide_network_preference if change?('setting_hide_network')
end
def merged_notification_emails
@@ -92,6 +93,10 @@ class UserSettingsDecorator
settings['setting_skin']
end
+ def hide_network_preference
+ boolean_cast_setting 'setting_hide_network'
+ end
+
def boolean_cast_setting(key)
ActiveModel::Type::Boolean.new.cast(settings[key])
end
diff --git a/app/models/account.rb b/app/models/account.rb
index 06c1904465..48f2847858 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -139,6 +139,7 @@ class Account < ApplicationRecord
:moderator?,
:staff?,
:locale,
+ :hides_network?,
to: :user,
prefix: true,
allow_nil: true
diff --git a/app/models/user.rb b/app/models/user.rb
index d5ca9be36a..9bdf8807f5 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -86,7 +86,7 @@ class User < ApplicationRecord
has_many :session_activations, dependent: :destroy
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal,
- :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_sensitive_media,
+ :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_sensitive_media, :hide_network,
to: :settings, prefix: :setting, allow_nil: false
attr_accessor :invite_code
@@ -219,6 +219,10 @@ class User < ApplicationRecord
settings.notification_emails['digest']
end
+ def hides_network?
+ @hides_network ||= settings.hide_network
+ end
+
def token_for_app(a)
return nil if a.nil? || a.owner != self
Doorkeeper::AccessToken
diff --git a/app/views/accounts/_follow_grid.html.haml b/app/views/accounts/_follow_grid.html.haml
index a6d0ee8176..fdcef84be2 100644
--- a/app/views/accounts/_follow_grid.html.haml
+++ b/app/views/accounts/_follow_grid.html.haml
@@ -1,5 +1,6 @@
-.accounts-grid
+.accounts-grid{ class: accounts.empty? ? 'empty' : '' }
- if accounts.empty?
+ = image_tag asset_pack_path('elephant_ui_greeting.svg'), alt: '', role: 'presentational'
= render partial: 'accounts/nothing_here'
- else
= render partial: 'accounts/grid_card', collection: accounts, as: :account, cached: !user_signed_in?
diff --git a/app/views/accounts/_follow_grid_hidden.html.haml b/app/views/accounts/_follow_grid_hidden.html.haml
new file mode 100644
index 0000000000..e970350e63
--- /dev/null
+++ b/app/views/accounts/_follow_grid_hidden.html.haml
@@ -0,0 +1,3 @@
+.accounts-grid.empty
+ = image_tag asset_pack_path('elephant_ui_greeting.svg'), alt: '', role: 'presentational'
+ %p.nothing-here= t('accounts.network_hidden')
diff --git a/app/views/follower_accounts/index.html.haml b/app/views/follower_accounts/index.html.haml
index a24e4ea20f..65af81a5b7 100644
--- a/app/views/follower_accounts/index.html.haml
+++ b/app/views/follower_accounts/index.html.haml
@@ -7,4 +7,7 @@
= render 'accounts/header', account: @account
-= render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:account)
+- if @account.user_hides_network?
+ = render 'accounts/follow_grid_hidden'
+- else
+ = render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:account)
diff --git a/app/views/following_accounts/index.html.haml b/app/views/following_accounts/index.html.haml
index 67f6cfede4..8fd95a0b4d 100644
--- a/app/views/following_accounts/index.html.haml
+++ b/app/views/following_accounts/index.html.haml
@@ -7,4 +7,7 @@
= render 'accounts/header', account: @account
-= render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:target_account)
+- if @account.user_hides_network?
+ = render 'accounts/follow_grid_hidden'
+- else
+ = render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:target_account)
diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml
index 07441a77d4..858d354fa3 100644
--- a/app/views/layouts/public.html.haml
+++ b/app/views/layouts/public.html.haml
@@ -5,9 +5,9 @@
%span.single-user-login
= link_to t('auth.login'), new_user_session_path
—
- %span.domain= link_to site_hostname, about_path
+ %span.footer__domain= link_to site_hostname, about_path
- else
- %span.domain= link_to site_hostname, root_path
+ %span.footer__domain= link_to site_hostname, root_path
%span.powered-by
!= t('generic.powered_by', link: link_to('Mastodon', 'https://joinmastodon.org'))
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index 102e4d2007..4632034d71 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -26,6 +26,9 @@
.fields-group
= f.input :setting_noindex, as: :boolean, wrapper: :with_label
+ .fields-group
+ = f.input :setting_hide_network, as: :boolean, wrapper: :with_label
+
%h4= t 'preferences.web'
.fields-group
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 0117a96808..2282823d04 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -40,6 +40,7 @@ en:
following: Following
media: Media
moved_html: "%{name} has moved to %{new_profile_link}:"
+ network_hidden: This information is not available
nothing_here: There is nothing here!
people_followed_by: People whom %{name} follows
people_who_follow: People who follow %{name}
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index c0c3b4b858..851b678e1a 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -15,6 +15,7 @@ en:
note:
one: 1 character left
other: %{count} characters left
+ setting_hide_network: Who you follow and who follows you will not be shown on your profile
setting_noindex: Affects your public profile and status pages
setting_skin: Reskins the selected Mastodon flavour
imports:
@@ -55,6 +56,7 @@ en:
setting_delete_modal: Show confirmation dialog before deleting a toot
setting_display_sensitive_media: Always show media marked as sensitive
setting_favourite_modal: Show confirmation dialog before favouriting
+ setting_hide_network: Hide your network
setting_noindex: Opt-out of search engine indexing
setting_reduce_motion: Reduce motion in animations
setting_skin: Skin
diff --git a/config/settings.yml b/config/settings.yml
index a92a0bfd0e..4a3720c2de 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -20,6 +20,7 @@ defaults: &defaults
min_invite_role: 'admin'
show_staff_badge: true
default_sensitive: false
+ hide_network: false
unfollow_modal: false
boost_modal: false
favourite_modal: false
diff --git a/spec/lib/activitypub/activity/add_spec.rb b/spec/lib/activitypub/activity/add_spec.rb
index 3ebab4e373..16db71c880 100644
--- a/spec/lib/activitypub/activity/add_spec.rb
+++ b/spec/lib/activitypub/activity/add_spec.rb
@@ -18,12 +18,31 @@ RSpec.describe ActivityPub::Activity::Add do
describe '#perform' do
subject { described_class.new(json, sender) }
- before do
+ it 'creates a pin' do
subject.perform
+ expect(sender.pinned?(status)).to be true
end
- it 'creates a pin' do
- expect(sender.pinned?(status)).to be true
+ context 'when status was not known before' do
+ let(:json) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'foo',
+ type: 'Add',
+ actor: ActivityPub::TagManager.instance.uri_for(sender),
+ object: 'https://example.com/unknown',
+ target: sender.featured_collection_url,
+ }.with_indifferent_access
+ end
+
+ before do
+ stub_request(:get, 'https://example.com/unknown').to_return(status: 410)
+ end
+
+ it 'fetches the status' do
+ subject.perform
+ expect(a_request(:get, 'https://example.com/unknown')).to have_been_made.at_least_once
+ end
end
end
end