+
+ {multiColumn && - ·
}
+ - ·
+ - ·
+ - ·
+
+
+
: ;
return (
-
+ {redirect}
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 690904236..d44d959f3 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -168,7 +168,7 @@
"navigation_bar.favourites": "Favourites",
"navigation_bar.follow_requests": "Follow requests",
"navigation_bar.info": "About this instance",
- "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.keyboard_shortcuts": "Hotkeys",
"navigation_bar.lists": "Lists",
"navigation_bar.misc": "Misc",
"navigation_bar.logout": "Logout",
diff --git a/app/javascript/mastodon/reducers/contexts.js b/app/javascript/mastodon/reducers/contexts.js
index 53e70b58e..4c2d6cc8a 100644
--- a/app/javascript/mastodon/reducers/contexts.js
+++ b/app/javascript/mastodon/reducers/contexts.js
@@ -5,6 +5,7 @@ import {
import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses';
import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+import compareId from '../compare_id';
const initialState = ImmutableMap({
inReplyTos: ImmutableMap(),
@@ -15,27 +16,27 @@ const normalizeContext = (immutableState, id, ancestors, descendants) => immutab
state.update('inReplyTos', immutableAncestors => immutableAncestors.withMutations(inReplyTos => {
state.update('replies', immutableDescendants => immutableDescendants.withMutations(replies => {
function addReply({ id, in_reply_to_id }) {
- if (in_reply_to_id) {
- const siblings = replies.get(in_reply_to_id, ImmutableList());
+ if (in_reply_to_id && !inReplyTos.has(id)) {
- if (!siblings.includes(id)) {
- const index = siblings.findLastIndex(sibling => sibling.id < id);
- replies.set(in_reply_to_id, siblings.insert(index + 1, id));
- }
+ replies.update(in_reply_to_id, ImmutableList(), siblings => {
+ const index = siblings.findLastIndex(sibling => compareId(sibling, id) < 0);
+ return siblings.insert(index + 1, id);
+ });
inReplyTos.set(id, in_reply_to_id);
}
}
- if (ancestors[0]) {
- addReply({ id, in_reply_to_id: ancestors[0].id });
- }
+ // We know in_reply_to_id of statuses but `id` itself.
+ // So we assume that the status of the id replies to last ancestors.
- if (descendants[0]) {
- addReply({ id: descendants[0].id, in_reply_to_id: id });
+ ancestors.forEach(addReply);
+
+ if (ancestors[0]) {
+ addReply({ id, in_reply_to_id: ancestors[ancestors.length - 1].id });
}
- [ancestors, descendants].forEach(statuses => statuses.forEach(addReply));
+ descendants.forEach(addReply);
}));
}));
});
@@ -80,7 +81,7 @@ const updateContext = (state, status) => {
mutable.setIn(['inReplyTos', status.id], status.in_reply_to_id);
if (!replies.includes(status.id)) {
- mutable.setIn(['replies', status.id], replies.push(status.id));
+ mutable.setIn(['replies', status.in_reply_to_id], replies.push(status.id));
}
});
}
diff --git a/app/javascript/mastodon/reducers/domain_lists.js b/app/javascript/mastodon/reducers/domain_lists.js
index a9e3519f3..eff97fbd6 100644
--- a/app/javascript/mastodon/reducers/domain_lists.js
+++ b/app/javascript/mastodon/reducers/domain_lists.js
@@ -6,7 +6,9 @@ import {
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
const initialState = ImmutableMap({
- blocks: ImmutableMap(),
+ blocks: ImmutableMap({
+ items: ImmutableOrderedSet(),
+ }),
});
export default function domainLists(state = initialState, action) {
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 712b6f813..04662900a 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1663,24 +1663,6 @@ a.account__display-name {
vertical-align: middle;
}
-.static-content {
- padding: 10px;
- padding-top: 20px;
- color: $dark-text-color;
-
- h1 {
- font-size: 16px;
- font-weight: 500;
- margin-bottom: 40px;
- text-align: center;
- }
-
- p {
- font-size: 13px;
- margin-bottom: 20px;
- }
-}
-
.columns-area {
display: flex;
flex: 1 1 auto;
@@ -1772,6 +1754,8 @@ a.account__display-name {
margin-bottom: 0;
}
+ .getting-started__wrapper,
+ .getting-started__trends,
.search {
margin-bottom: 10px;
}
@@ -2175,7 +2159,8 @@ a.account__display-name {
}
.getting-started__wrapper,
-.getting_started {
+.getting-started,
+.flex-spacer {
background: $ui-base-color;
}
@@ -2184,16 +2169,58 @@ a.account__display-name {
overflow-y: auto;
}
+.flex-spacer {
+ flex: 1 1 auto;
+}
+
.getting-started {
- background: $ui-base-color;
flex: 1 0 auto;
+ color: $dark-text-color;
p {
- color: $secondary-text-color;
+ color: $dark-text-color;
+ font-size: 13px;
+ margin-bottom: 20px;
+
+ a {
+ color: $dark-text-color;
+ text-decoration: underline;
+ }
}
a {
- color: $dark-text-color;
+ text-decoration: none;
+ color: $darker-text-color;
+
+ &:hover,
+ &:focus,
+ &:active {
+ text-decoration: underline;
+ }
+ }
+
+ &__footer {
+ flex: 0 0 auto;
+ padding: 10px;
+ padding-top: 20px;
+
+ ul {
+ margin-bottom: 10px;
+ }
+
+ ul li {
+ display: inline;
+ }
+ }
+
+ &__trends {
+ background: $ui-base-color;
+ flex: 1 1 auto;
+ }
+
+ &__scrollable {
+ max-height: 100%;
+ overflow-y: auto;
}
}
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index a064248d9..d067415fd 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -187,4 +187,15 @@ module AccountInteractions
def pinned?(status)
status_pins.where(status: status).exists?
end
+
+ def followers_for_local_distribution
+ followers.local
+ .joins(:user)
+ .where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
+ end
+
+ def lists_for_local_distribution
+ lists.joins(account: :user)
+ .where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
+ end
end
diff --git a/app/models/favourite.rb b/app/models/favourite.rb
index c998a67eb..0fce82f6f 100644
--- a/app/models/favourite.rb
+++ b/app/models/favourite.rb
@@ -16,7 +16,7 @@ class Favourite < ApplicationRecord
update_index('statuses#status', :status) if Chewy.enabled?
belongs_to :account, inverse_of: :favourites
- belongs_to :status, inverse_of: :favourites, counter_cache: true
+ belongs_to :status, inverse_of: :favourites
has_one :notification, as: :activity, dependent: :destroy
@@ -25,4 +25,27 @@ class Favourite < ApplicationRecord
before_validation do
self.status = status.reblog if status&.reblog?
end
+
+ after_create :increment_cache_counters
+ after_destroy :decrement_cache_counters
+
+ private
+
+ def increment_cache_counters
+ if association(:status).loaded?
+ status.update_attribute(:favourites_count, status.favourites_count + 1)
+ else
+ Status.where(id: status_id).update_all('favourites_count = COALESCE(favourites_count, 0) + 1')
+ end
+ end
+
+ def decrement_cache_counters
+ return if association(:status).loaded? && (status.marked_for_destruction? || status.marked_for_mass_destruction?)
+
+ if association(:status).loaded?
+ status.update_attribute(:favourites_count, [status.favourites_count - 1, 0].max)
+ else
+ Status.where(id: status_id).update_all('favourites_count = GREATEST(COALESCE(favourites_count, 0) - 1, 0)')
+ end
+ end
end
diff --git a/app/models/status.rb b/app/models/status.rb
index c6d6453df..69fae2eb6 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -43,12 +43,12 @@ class Status < ApplicationRecord
belongs_to :application, class_name: 'Doorkeeper::Application', optional: true
- belongs_to :account, inverse_of: :statuses, counter_cache: true
+ belongs_to :account, inverse_of: :statuses
belongs_to :in_reply_to_account, foreign_key: 'in_reply_to_account_id', class_name: 'Account', optional: true
belongs_to :conversation, optional: true
belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true
- belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, counter_cache: :reblogs_count, optional: true
+ belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true
has_many :favourites, inverse_of: :status, dependent: :destroy
has_many :bookmarks, inverse_of: :status, dependent: :destroy
@@ -172,6 +172,17 @@ class Status < ApplicationRecord
@emojis ||= CustomEmoji.from_text([spoiler_text, text].join(' '), account.domain)
end
+ def mark_for_mass_destruction!
+ @marked_for_mass_destruction = true
+ end
+
+ def marked_for_mass_destruction?
+ @marked_for_mass_destruction
+ end
+
+ after_create :increment_counter_caches
+ after_destroy :decrement_counter_caches
+
after_create_commit :store_uri, if: :local?
after_create_commit :update_statistics, if: :local?
@@ -414,4 +425,40 @@ class Status < ApplicationRecord
return unless public_visibility? || unlisted_visibility?
ActivityTracker.increment('activity:statuses:local')
end
+
+ def increment_counter_caches
+ return if direct_visibility?
+
+ if association(:account).loaded?
+ account.update_attribute(:statuses_count, account.statuses_count + 1)
+ else
+ Account.where(id: account_id).update_all('statuses_count = COALESCE(statuses_count, 0) + 1')
+ end
+
+ return unless reblog?
+
+ if association(:reblog).loaded?
+ reblog.update_attribute(:reblogs_count, reblog.reblogs_count + 1)
+ else
+ Status.where(id: reblog_of_id).update_all('reblogs_count = COALESCE(reblogs_count, 0) + 1')
+ end
+ end
+
+ def decrement_counter_caches
+ return if direct_visibility? || marked_for_mass_destruction?
+
+ if association(:account).loaded?
+ account.update_attribute(:statuses_count, [account.statuses_count - 1, 0].max)
+ else
+ Account.where(id: account_id).update_all('statuses_count = GREATEST(COALESCE(statuses_count, 0) - 1, 0)')
+ end
+
+ return unless reblog?
+
+ if association(:reblog).loaded?
+ reblog.update_attribute(:reblogs_count, [reblog.reblogs_count - 1, 0].max)
+ else
+ Status.where(id: reblog_of_id).update_all('reblogs_count = GREATEST(COALESCE(reblogs_count, 0) - 1, 0)')
+ end
+ end
end
diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb
index ace51a1fc..ebb4034aa 100644
--- a/app/services/batched_remove_status_service.rb
+++ b/app/services/batched_remove_status_service.rb
@@ -21,7 +21,10 @@ class BatchedRemoveStatusService < BaseService
@activity_xml = {}
# Ensure that rendered XML reflects destroyed state
- statuses.each(&:destroy)
+ statuses.each do |status|
+ status.mark_for_mass_destruction!
+ status.destroy
+ end
# Batch by source account
statuses.group_by(&:account_id).each_value do |account_statuses|
@@ -53,7 +56,7 @@ class BatchedRemoveStatusService < BaseService
end
def unpush_from_home_timelines(account, statuses)
- recipients = account.followers.local.to_a
+ recipients = account.followers_for_local_distribution.to_a
recipients << account if account.local?
@@ -65,7 +68,7 @@ class BatchedRemoveStatusService < BaseService
end
def unpush_from_list_timelines(account, statuses)
- account.lists.select(:id, :account_id).each do |list|
+ account.lists_for_local_distribution.select(:id, :account_id).each do |list|
statuses.each do |status|
FeedManager.instance.unpush_from_list(list, status)
end
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index 8b3630229..5efd3edb2 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -38,7 +38,7 @@ class FanOutOnWriteService < BaseService
def deliver_to_followers(status)
Rails.logger.debug "Delivering status #{status.id} to followers"
- status.account.followers.where(domain: nil).joins(:user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).select(:id).reorder(nil).find_in_batches do |followers|
+ status.account.followers_for_local_distribution.select(:id).reorder(nil).find_in_batches do |followers|
FeedInsertWorker.push_bulk(followers) do |follower|
[status.id, follower.id, :home]
end
@@ -48,7 +48,7 @@ class FanOutOnWriteService < BaseService
def deliver_to_lists(status)
Rails.logger.debug "Delivering status #{status.id} to lists"
- status.account.lists.joins(account: :user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).select(:id).reorder(nil).find_in_batches do |lists|
+ status.account.lists_for_local_distribution.select(:id).reorder(nil).find_in_batches do |lists|
FeedInsertWorker.push_bulk(lists) do |list|
[status.id, list.id, :list]
end
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index 8c3e18444..b9631077c 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -43,13 +43,13 @@ class RemoveStatusService < BaseService
end
def remove_from_followers
- @account.followers.local.find_each do |follower|
+ @account.followers_for_local_distribution.find_each do |follower|
FeedManager.instance.unpush_from_home(follower, @status)
end
end
def remove_from_lists
- @account.lists.select(:id, :account_id).find_each do |list|
+ @account.lists_for_local_distribution.select(:id, :account_id).find_each do |list|
FeedManager.instance.unpush_from_list(list, @status)
end
end
diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb
index 56fa2d8dd..708d15e37 100644
--- a/app/services/suspend_account_service.rb
+++ b/app/services/suspend_account_service.rb
@@ -41,9 +41,10 @@ class SuspendAccountService < BaseService
end
def purge_profile!
- @account.suspended = true
- @account.display_name = ''
- @account.note = ''
+ @account.suspended = true
+ @account.display_name = ''
+ @account.note = ''
+ @account.statuses_count = 0
@account.avatar.destroy
@account.header.destroy
@account.save!
diff --git a/app/workers/maintenance/destroy_media_worker.rb b/app/workers/maintenance/destroy_media_worker.rb
new file mode 100644
index 000000000..5f052983b
--- /dev/null
+++ b/app/workers/maintenance/destroy_media_worker.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class Maintenance::DestroyMediaWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: 'pull'
+
+ def perform(media_attachment_id)
+ media = MediaAttachment.find(media_attachment_id)
+ media.destroy
+ rescue ActiveRecord::RecordNotFound
+ true
+ end
+end
diff --git a/app/workers/maintenance/redownload_account_media_worker.rb b/app/workers/maintenance/redownload_account_media_worker.rb
new file mode 100644
index 000000000..fc26815f2
--- /dev/null
+++ b/app/workers/maintenance/redownload_account_media_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class Maintenance::RedownloadAccountMediaWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: 'pull', retry: false
+
+ def perform(account_id)
+ account = Account.find(account_id)
+ account.reset_avatar!
+ account.reset_header!
+ account.save
+ rescue ActiveRecord::RecordNotFound
+ true
+ end
+end
diff --git a/app/workers/maintenance/uncache_media_worker.rb b/app/workers/maintenance/uncache_media_worker.rb
new file mode 100644
index 000000000..f6a51a1b8
--- /dev/null
+++ b/app/workers/maintenance/uncache_media_worker.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class Maintenance::UncacheMediaWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: 'pull'
+
+ def perform(media_attachment_id)
+ media = MediaAttachment.find(media_attachment_id)
+
+ return unless media.file.exists?
+
+ media.file.destroy
+ media.save
+ rescue ActiveRecord::RecordNotFound
+ true
+ end
+end
diff --git a/db/migrate/20180528141303_fix_accounts_unique_index.rb b/db/migrate/20180528141303_fix_accounts_unique_index.rb
new file mode 100644
index 000000000..aadb5b7db
--- /dev/null
+++ b/db/migrate/20180528141303_fix_accounts_unique_index.rb
@@ -0,0 +1,88 @@
+class FixAccountsUniqueIndex < ActiveRecord::Migration[5.2]
+ disable_ddl_transaction!
+
+ def up
+ say ''
+ say 'WARNING: This migration may take a *long* time for large instances'
+ say 'It will *not* lock tables for any significant time, but it may run'
+ say 'for a very long time. We will pause for 10 seconds to allow you to'
+ say 'interrupt this migration if you are not ready.'
+ say ''
+ say 'This migration will irreversibly delete user accounts with duplicate'
+ say 'usernames. You may use the `rake mastodon:maintenance:find_duplicate_usernames`'
+ say 'task to manually deal with such accounts before running this migration.'
+
+ 10.downto(1) do |i|
+ say "Continuing in #{i} second#{i == 1 ? '' : 's'}...", true
+ sleep 1
+ end
+
+ duplicates = Account.connection.select_all('SELECT string_agg(id::text, \',\') AS ids FROM accounts GROUP BY lower(username), lower(domain) HAVING count(*) > 1').to_hash
+
+ duplicates.each do |row|
+ deduplicate_account!(row['ids'].split(','))
+ end
+
+ remove_index :accounts, name: 'index_accounts_on_username_and_domain_lower' if index_name_exists?(:accounts, 'index_accounts_on_username_and_domain_lower')
+ safety_assured { execute 'CREATE UNIQUE INDEX CONCURRENTLY index_accounts_on_username_and_domain_lower ON accounts (lower(username), lower(domain))' }
+ remove_index :accounts, name: 'index_accounts_on_username_and_domain' if index_name_exists?(:accounts, 'index_accounts_on_username_and_domain')
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+
+ private
+
+ def deduplicate_account!(account_ids)
+ accounts = Account.where(id: account_ids).to_a
+ accounts = accounts.first.local? ? accounts.sort_by(&:created_at) : accounts.sort_by(&:updated_at).reverse
+ reference_account = accounts.shift
+
+ accounts.each do |other_account|
+ if other_account.public_key == reference_account.public_key
+ # The accounts definitely point to the same resource, so
+ # it's safe to re-attribute content and relationships
+ merge_accounts!(reference_account, other_account)
+ elsif other_account.local?
+ # Since domain is in the GROUP BY clause, both accounts
+ # are always either going to be local or not local, so only
+ # one check is needed. Since we cannot support two users with
+ # the same username locally, one has to go. 😢
+ other_account.user&.destroy
+ end
+
+ other_account.destroy
+ end
+ end
+
+ def merge_accounts!(main_account, duplicate_account)
+ [Status, Favourite, Mention, StatusPin, StreamEntry].each do |klass|
+ klass.where(account_id: duplicate_account.id).update_all(account_id: main_account.id)
+ end
+
+ # Since it's the same remote resource, the remote resource likely
+ # already believes we are following/blocking, so it's safe to
+ # re-attribute the relationships too. However, during the presence
+ # of the index bug users could have *also* followed the reference
+ # account already, therefore mass update will not work and we need
+ # to check for (and skip past) uniqueness errors
+ [Follow, FollowRequest, Block, Mute].each do |klass|
+ klass.where(account_id: duplicate_account.id).find_each do |record|
+ begin
+ record.update(account_id: main_account.id)
+ rescue ActiveRecord::RecordNotUnique
+ next
+ end
+ end
+
+ klass.where(target_account_id: duplicate_account.id).find_each do |record|
+ begin
+ record.update(target_account_id: main_account.id)
+ rescue ActiveRecord::RecordNotUnique
+ next
+ end
+ end
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index def0505a6..796924123 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: 2018_05_14_140000) do
+ActiveRecord::Schema.define(version: 2018_05_28_141303) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -77,10 +77,9 @@ ActiveRecord::Schema.define(version: 2018_05_14_140000) do
t.jsonb "fields"
t.string "actor_type"
t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
- t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower"
+ t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower", unique: true
t.index ["uri"], name: "index_accounts_on_uri"
t.index ["url"], name: "index_accounts_on_url"
- t.index ["username", "domain"], name: "index_accounts_on_username_and_domain", unique: true
end
create_table "admin_action_logs", force: :cascade do |t|
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index 9a7d49674..622103ea4 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -13,7 +13,7 @@ module Mastodon
end
def patch
- 0
+ 1
end
def pre
@@ -21,7 +21,7 @@ module Mastodon
end
def flags
- ''
+ 'rc1'
end
def to_a
diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake
index 00a85fa5e..8ff29ea9e 100644
--- a/lib/tasks/mastodon.rake
+++ b/lib/tasks/mastodon.rake
@@ -502,18 +502,17 @@ namespace :mastodon do
desc 'Remove media attachments attributed to silenced accounts'
task remove_silenced: :environment do
- MediaAttachment.where(account: Account.silenced).find_each(&:destroy)
+ MediaAttachment.where(account: Account.silenced).select(:id).find_in_batches do |media_attachments|
+ Maintenance::DestroyMediaWorker.push_bulk(media_attachments.map(&:id))
+ end
end
desc 'Remove cached remote media attachments that are older than NUM_DAYS. By default 7 (week)'
task remove_remote: :environment do
time_ago = ENV.fetch('NUM_DAYS') { 7 }.to_i.days.ago
- MediaAttachment.where.not(remote_url: '').where.not(file_file_name: nil).where('created_at < ?', time_ago).find_each do |media|
- next unless media.file.exists?
-
- media.file.destroy
- media.save
+ MediaAttachment.where.not(remote_url: '').where.not(file_file_name: nil).where('created_at < ?', time_ago).select(:id).find_in_batches do |media_attachments|
+ Maintenance::UncacheMediaWorker.push_bulk(media_attachments.map(&:id))
end
end
@@ -529,14 +528,8 @@ namespace :mastodon do
accounts = Account.remote
accounts = accounts.where(domain: ENV['DOMAIN']) if ENV['DOMAIN'].present?
- accounts.find_each do |account|
- begin
- account.reset_avatar!
- account.reset_header!
- account.save
- rescue Paperclip::Error
- puts "Error resetting avatar and header for account #{username}@#{domain}"
- end
+ accounts.select(:id).find_in_batches do |accounts_batch|
+ Maintenance::RedownloadAccountMediaWorker.push_bulk(accounts_batch.map(&:id))
end
end
end
@@ -568,8 +561,8 @@ namespace :mastodon do
desc 'Generates home timelines for users who logged in in the past two weeks'
task build: :environment do
- User.active.includes(:account).find_each do |u|
- PrecomputeFeedService.new.call(u.account)
+ User.active.select(:account_id).find_in_batches do |users|
+ RegenerationWorker.push_bulk(users.map(&:account_id))
end
end
end
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index a88b11482..512b6e661 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -525,6 +525,37 @@ RSpec.describe Account, type: :model do
end
end
+ describe '#statuses_count' do
+ subject { Fabricate(:account) }
+
+ it 'counts statuses' do
+ Fabricate(:status, account: subject)
+ Fabricate(:status, account: subject)
+ expect(subject.statuses_count).to eq 2
+ end
+
+ it 'does not count direct statuses' do
+ Fabricate(:status, account: subject, visibility: :direct)
+ expect(subject.statuses_count).to eq 0
+ end
+
+ it 'is decremented when status is removed' do
+ status = Fabricate(:status, account: subject)
+ expect(subject.statuses_count).to eq 1
+ status.destroy
+ expect(subject.statuses_count).to eq 0
+ end
+
+ it 'is decremented when status is removed when account is not preloaded' do
+ status = Fabricate(:status, account: subject)
+ expect(subject.reload.statuses_count).to eq 1
+ clean_status = Status.find(status.id)
+ expect(clean_status.association(:account).loaded?).to be false
+ clean_status.destroy
+ expect(subject.reload.statuses_count).to eq 0
+ end
+ end
+
describe '.following_map' do
it 'returns an hash' do
expect(Account.following_map([], 1)).to be_a Hash
diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb
index 03d1a94de..14233e824 100644
--- a/spec/models/status_spec.rb
+++ b/spec/models/status_spec.rb
@@ -175,6 +175,13 @@ RSpec.describe Status, type: :model do
expect(subject.reblogs_count).to eq 2
end
+
+ it 'is decremented when reblog is removed' do
+ reblog = Fabricate(:status, account: bob, reblog: subject)
+ expect(subject.reblogs_count).to eq 1
+ reblog.destroy
+ expect(subject.reblogs_count).to eq 0
+ end
end
describe '#favourites_count' do
@@ -184,6 +191,13 @@ RSpec.describe Status, type: :model do
expect(subject.favourites_count).to eq 2
end
+
+ it 'is decremented when favourite is removed' do
+ favourite = Fabricate(:favourite, account: bob, status: subject)
+ expect(subject.favourites_count).to eq 1
+ favourite.destroy
+ expect(subject.favourites_count).to eq 0
+ end
end
describe '#proper' do