about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.circleci/config.yml191
-rw-r--r--.env.production.sample7
-rw-r--r--.env.test2
-rw-r--r--Gemfile38
-rw-r--r--Gemfile.lock166
-rw-r--r--app/controllers/accounts_controller.rb10
-rw-r--r--app/controllers/admin/reported_statuses_controller.rb14
-rw-r--r--app/controllers/admin/reports_controller.rb8
-rw-r--r--app/controllers/api/base_controller.rb4
-rw-r--r--app/controllers/api/v1/accounts/credentials_controller.rb2
-rw-r--r--app/controllers/api/v1/accounts_controller.rb5
-rw-r--r--app/controllers/api/v1/statuses_controller.rb2
-rw-r--r--app/controllers/api/web/embeds_controller.rb11
-rw-r--r--app/controllers/concerns/localized.rb8
-rw-r--r--app/controllers/statuses_controller.rb76
-rw-r--r--app/controllers/stream_entries_controller.rb1
-rw-r--r--app/controllers/tags_controller.rb11
-rw-r--r--app/helpers/admin/account_moderation_notes_helper.rb16
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/jsonld_helper.rb21
-rw-r--r--app/helpers/settings_helper.rb5
-rw-r--r--app/helpers/stream_entries_helper.rb25
-rw-r--r--app/javascript/core/admin.js1
-rw-r--r--app/javascript/mastodon/actions/compose.js19
-rw-r--r--app/javascript/mastodon/actions/push_notifications/registerer.js9
-rw-r--r--app/javascript/mastodon/base_polyfills.js21
-rw-r--r--app/javascript/mastodon/components/autosuggest_textarea.js24
-rw-r--r--app/javascript/mastodon/components/dropdown_menu.js2
-rw-r--r--app/javascript/mastodon/components/relative_timestamp.js15
-rw-r--r--app/javascript/mastodon/components/scrollable_list.js13
-rw-r--r--app/javascript/mastodon/components/status.js10
-rw-r--r--app/javascript/mastodon/components/status_action_bar.js4
-rw-r--r--app/javascript/mastodon/components/status_list.js20
-rw-r--r--app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js13
-rw-r--r--app/javascript/mastodon/features/compose/components/reply_indicator.js2
-rw-r--r--app/javascript/mastodon/features/status/components/action_bar.js4
-rw-r--r--app/javascript/mastodon/load_polyfills.js7
-rw-r--r--app/javascript/mastodon/locales/ar.json27
-rw-r--r--app/javascript/mastodon/locales/bg.json1
-rw-r--r--app/javascript/mastodon/locales/ca.json31
-rw-r--r--app/javascript/mastodon/locales/de.json79
-rw-r--r--app/javascript/mastodon/locales/el.json296
-rw-r--r--app/javascript/mastodon/locales/en.json1
-rw-r--r--app/javascript/mastodon/locales/eo.json1
-rw-r--r--app/javascript/mastodon/locales/es.json1
-rw-r--r--app/javascript/mastodon/locales/eu.json296
-rw-r--r--app/javascript/mastodon/locales/fa.json1
-rw-r--r--app/javascript/mastodon/locales/fi.json1
-rw-r--r--app/javascript/mastodon/locales/fr.json29
-rw-r--r--app/javascript/mastodon/locales/gl.json5
-rw-r--r--app/javascript/mastodon/locales/he.json1
-rw-r--r--app/javascript/mastodon/locales/hr.json1
-rw-r--r--app/javascript/mastodon/locales/hu.json1
-rw-r--r--app/javascript/mastodon/locales/hy.json1
-rw-r--r--app/javascript/mastodon/locales/id.json1
-rw-r--r--app/javascript/mastodon/locales/io.json1
-rw-r--r--app/javascript/mastodon/locales/it.json9
-rw-r--r--app/javascript/mastodon/locales/ja.json13
-rw-r--r--app/javascript/mastodon/locales/ko.json5
-rw-r--r--app/javascript/mastodon/locales/nl.json29
-rw-r--r--app/javascript/mastodon/locales/no.json1
-rw-r--r--app/javascript/mastodon/locales/oc.json37
-rw-r--r--app/javascript/mastodon/locales/pl.json10
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json23
-rw-r--r--app/javascript/mastodon/locales/pt.json1
-rw-r--r--app/javascript/mastodon/locales/ru.json1
-rw-r--r--app/javascript/mastodon/locales/sk.json21
-rw-r--r--app/javascript/mastodon/locales/sr-Latn.json1
-rw-r--r--app/javascript/mastodon/locales/sr.json1
-rw-r--r--app/javascript/mastodon/locales/sv.json29
-rw-r--r--app/javascript/mastodon/locales/te.json296
-rw-r--r--app/javascript/mastodon/locales/th.json1
-rw-r--r--app/javascript/mastodon/locales/tr.json1
-rw-r--r--app/javascript/mastodon/locales/uk.json1
-rw-r--r--app/javascript/mastodon/locales/whitelist_el.json2
-rw-r--r--app/javascript/mastodon/locales/whitelist_eu.json2
-rw-r--r--app/javascript/mastodon/locales/whitelist_te.json2
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json1
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json1
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json1
-rw-r--r--app/javascript/mastodon/reducers/notifications.js2
-rw-r--r--app/javascript/mastodon/reducers/timelines.js2
-rw-r--r--app/javascript/mastodon/utils/__tests__/base64-test.js10
-rw-r--r--app/javascript/mastodon/utils/base64.js10
-rw-r--r--app/javascript/mastodon/utils/resize_image.js66
-rw-r--r--app/javascript/styles/contrast.scss3
-rw-r--r--app/javascript/styles/contrast/diff.scss14
-rw-r--r--app/javascript/styles/contrast/variables.scss24
-rw-r--r--app/javascript/styles/mastodon/about.scss32
-rw-r--r--app/javascript/styles/mastodon/accounts.scss22
-rw-r--r--app/javascript/styles/mastodon/admin.scss123
-rw-r--r--app/javascript/styles/mastodon/compact_header.scss4
-rw-r--r--app/javascript/styles/mastodon/components.scss175
-rw-r--r--app/javascript/styles/mastodon/containers.scss2
-rw-r--r--app/javascript/styles/mastodon/emoji_picker.scss4
-rw-r--r--app/javascript/styles/mastodon/forms.scss10
-rw-r--r--app/javascript/styles/mastodon/landing_strip.scss2
-rw-r--r--app/javascript/styles/mastodon/stream_entries.scss14
-rw-r--r--app/javascript/styles/mastodon/tables.scss116
-rw-r--r--app/javascript/styles/mastodon/variables.scss20
-rw-r--r--app/lib/activitypub/activity.rb2
-rw-r--r--app/lib/activitypub/activity/announce.rb2
-rw-r--r--app/lib/activitypub/activity/create.rb13
-rw-r--r--app/lib/activitypub/activity/update.rb7
-rw-r--r--app/lib/entity_cache.rb34
-rw-r--r--app/lib/exceptions.rb1
-rw-r--r--app/lib/feed_manager.rb17
-rw-r--r--app/lib/formatter.rb10
-rw-r--r--app/lib/ostatus/activity/creation.rb4
-rw-r--r--app/lib/ostatus/atom_serializer.rb2
-rw-r--r--app/lib/provider_discovery.rb47
-rw-r--r--app/lib/request.rb19
-rw-r--r--app/lib/rss_builder.rb130
-rw-r--r--app/lib/status_filter.rb17
-rw-r--r--app/models/account.rb15
-rw-r--r--app/models/account_domain_block.rb4
-rw-r--r--app/models/account_moderation_note.rb6
-rw-r--r--app/models/admin/action_log.rb6
-rw-r--r--app/models/backup.rb4
-rw-r--r--app/models/block.rb6
-rw-r--r--app/models/concerns/account_interactions.rb13
-rw-r--r--app/models/concerns/attachmentable.rb32
-rw-r--r--app/models/concerns/cacheable.rb15
-rw-r--r--app/models/concerns/remotable.rb2
-rw-r--r--app/models/concerns/status_threading_concern.rb38
-rw-r--r--app/models/conversation.rb2
-rw-r--r--app/models/conversation_mute.rb6
-rw-r--r--app/models/custom_emoji.rb14
-rw-r--r--app/models/domain_block.rb2
-rw-r--r--app/models/email_domain_block.rb2
-rw-r--r--app/models/favourite.rb6
-rw-r--r--app/models/follow.rb6
-rw-r--r--app/models/follow_request.rb6
-rw-r--r--app/models/import.rb4
-rw-r--r--app/models/invite.rb4
-rw-r--r--app/models/list.rb4
-rw-r--r--app/models/list_account.rb8
-rw-r--r--app/models/media_attachment.rb19
-rw-r--r--app/models/mention.rb6
-rw-r--r--app/models/mute.rb6
-rw-r--r--app/models/notification.rb8
-rw-r--r--app/models/preview_card.rb21
-rw-r--r--app/models/report.rb12
-rw-r--r--app/models/report_note.rb6
-rw-r--r--app/models/session_activation.rb8
-rw-r--r--app/models/setting.rb4
-rw-r--r--app/models/site_upload.rb2
-rw-r--r--app/models/status.rb17
-rw-r--r--app/models/status_pin.rb6
-rw-r--r--app/models/stream_entry.rb6
-rw-r--r--app/models/subscription.rb4
-rw-r--r--app/models/tag.rb2
-rw-r--r--app/models/user.rb6
-rw-r--r--app/models/web/push_subscription.rb2
-rw-r--r--app/models/web/setting.rb4
-rw-r--r--app/policies/status_policy.rb46
-rw-r--r--app/serializers/rest/credential_account_serializer.rb2
-rw-r--r--app/serializers/rss/account_serializer.rb39
-rw-r--r--app/serializers/rss/tag_serializer.rb37
-rw-r--r--app/services/account_search_service.rb4
-rw-r--r--app/services/activitypub/fetch_featured_collection_service.rb2
-rw-r--r--app/services/activitypub/fetch_remote_account_service.rb2
-rw-r--r--app/services/activitypub/fetch_remote_key_service.rb4
-rw-r--r--app/services/activitypub/fetch_remote_status_service.rb2
-rw-r--r--app/services/activitypub/process_account_service.rb5
-rw-r--r--app/services/fan_out_on_write_service.rb2
-rw-r--r--app/services/favourite_service.rb2
-rw-r--r--app/services/fetch_atom_service.rb4
-rw-r--r--app/services/fetch_link_card_service.rb38
-rw-r--r--app/services/fetch_oembed_service.rb71
-rw-r--r--app/services/mute_service.rb7
-rw-r--r--app/services/process_hashtags_service.rb2
-rw-r--r--app/services/process_mentions_service.rb44
-rw-r--r--app/services/resolve_account_service.rb2
-rw-r--r--app/services/resolve_url_service.rb5
-rw-r--r--app/validators/disallowed_hashtags_validator.rb22
-rw-r--r--app/views/admin/action_logs/_action_log.html.haml2
-rw-r--r--app/views/admin/action_logs/index.html.haml3
-rw-r--r--app/views/admin/report_notes/_report_note.html.haml14
-rw-r--r--app/views/admin/reports/_account.html.haml19
-rw-r--r--app/views/admin/reports/_account_details.html.haml20
-rw-r--r--app/views/admin/reports/_action_log.html.haml6
-rw-r--r--app/views/admin/reports/_report.html.haml6
-rw-r--r--app/views/admin/reports/_status.html.haml28
-rw-r--r--app/views/admin/reports/index.html.haml27
-rw-r--r--app/views/admin/reports/show.html.haml117
-rw-r--r--app/views/home/index.html.haml5
-rw-r--r--app/views/stream_entries/_detailed_status.html.haml6
-rw-r--r--app/views/stream_entries/_more.html.haml2
-rw-r--r--app/views/stream_entries/_simple_status.html.haml13
-rw-r--r--app/views/stream_entries/_status.html.haml31
-rw-r--r--app/views/well_known/host_meta/show.xml.ruby16
-rw-r--r--app/views/well_known/webfinger/show.xml.ruby57
-rw-r--r--app/workers/activitypub/processing_worker.rb2
-rw-r--r--app/workers/local_notification_worker.rb12
-rw-r--r--app/workers/processing_worker.rb2
-rw-r--r--app/workers/scheduler/backup_cleanup_scheduler.rb1
-rw-r--r--app/workers/scheduler/doorkeeper_cleanup_scheduler.rb1
-rw-r--r--app/workers/scheduler/email_scheduler.rb1
-rw-r--r--app/workers/scheduler/feed_cleanup_scheduler.rb1
-rw-r--r--app/workers/scheduler/ip_cleanup_scheduler.rb1
-rw-r--r--app/workers/scheduler/media_cleanup_scheduler.rb1
-rw-r--r--app/workers/scheduler/subscriptions_cleanup_scheduler.rb2
-rw-r--r--app/workers/scheduler/subscriptions_scheduler.rb3
-rw-r--r--app/workers/scheduler/user_cleanup_scheduler.rb1
-rw-r--r--app/workers/soft_block_domain_followers_worker.rb2
-rw-r--r--config/application.rb3
-rw-r--r--config/deploy.rb2
-rw-r--r--config/environments/production.rb2
-rw-r--r--config/initializers/http_client_proxy.rb24
-rw-r--r--config/initializers/json_ld.rb5
-rw-r--r--config/initializers/oembed.rb4
-rw-r--r--config/initializers/rack_attack.rb4
-rw-r--r--config/locales/activerecord.eu.yml9
-rw-r--r--config/locales/ar.yml3
-rw-r--r--config/locales/ca.yml129
-rw-r--r--config/locales/de.yml76
-rw-r--r--config/locales/devise.de.yml14
-rw-r--r--config/locales/devise.eu.yml5
-rw-r--r--config/locales/devise.it.yml21
-rw-r--r--config/locales/doorkeeper.de.yml6
-rw-r--r--config/locales/doorkeeper.eu.yml6
-rw-r--r--config/locales/doorkeeper.it.yml16
-rw-r--r--config/locales/doorkeeper.zh-HK.yml14
-rw-r--r--config/locales/el.yml40
-rw-r--r--config/locales/en.yml29
-rw-r--r--config/locales/eo.yml1
-rw-r--r--config/locales/es.yml56
-rw-r--r--config/locales/eu.yml1
-rw-r--r--config/locales/fa.yml1
-rw-r--r--config/locales/fi.yml1
-rw-r--r--config/locales/fr.yml53
-rw-r--r--config/locales/gl.yml132
-rw-r--r--config/locales/he.yml1
-rw-r--r--config/locales/hu.yml1
-rw-r--r--config/locales/id.yml1
-rw-r--r--config/locales/io.yml1
-rw-r--r--config/locales/it.yml281
-rw-r--r--config/locales/ja.yml20
-rw-r--r--config/locales/ko.yml45
-rw-r--r--config/locales/ms.yml1
-rw-r--r--config/locales/nl.yml129
-rw-r--r--config/locales/no.yml1
-rw-r--r--config/locales/oc.yml44
-rw-r--r--config/locales/pl.yml27
-rw-r--r--config/locales/pt-BR.yml128
-rw-r--r--config/locales/pt.yml1
-rw-r--r--config/locales/ru.yml1
-rw-r--r--config/locales/simple_form.ar.yml12
-rw-r--r--config/locales/simple_form.ca.yml6
-rw-r--r--config/locales/simple_form.de.yml6
-rw-r--r--config/locales/simple_form.eu.yml32
-rw-r--r--config/locales/simple_form.fr.yml6
-rw-r--r--config/locales/simple_form.gl.yml8
-rw-r--r--config/locales/simple_form.it.yml84
-rw-r--r--config/locales/simple_form.ja.yml8
-rw-r--r--config/locales/simple_form.ko.yml6
-rw-r--r--config/locales/simple_form.nl.yml8
-rw-r--r--config/locales/simple_form.oc.yml6
-rw-r--r--config/locales/simple_form.pl.yml2
-rw-r--r--config/locales/simple_form.pt-BR.yml6
-rw-r--r--config/locales/simple_form.sk.yml16
-rw-r--r--config/locales/simple_form.sv.yml6
-rw-r--r--config/locales/simple_form.zh-HK.yml12
-rw-r--r--config/locales/sk.yml51
-rw-r--r--config/locales/sr-Latn.yml1
-rw-r--r--config/locales/sr.yml1
-rw-r--r--config/locales/sv.yml69
-rw-r--r--config/locales/te.yml5
-rw-r--r--config/locales/th.yml1
-rw-r--r--config/locales/tr.yml1
-rw-r--r--config/locales/uk.yml1
-rw-r--r--config/locales/zh-CN.yml1
-rw-r--r--config/locales/zh-HK.yml12
-rw-r--r--config/locales/zh-TW.yml1
-rw-r--r--config/settings.yml1
-rw-r--r--lib/json_ld/activitystreams.rb153
-rw-r--r--lib/json_ld/identity.rb86
-rw-r--r--lib/json_ld/security.rb50
-rw-r--r--lib/mastodon/migration_helpers.rb11
-rw-r--r--lib/mastodon/redis_config.rb27
-rw-r--r--lib/tasks/mastodon.rake9
-rw-r--r--spec/controllers/about_controller_spec.rb6
-rw-r--r--spec/controllers/accounts_controller_spec.rb2
-rw-r--r--spec/controllers/activitypub/outboxes_controller_spec.rb2
-rw-r--r--spec/controllers/admin/accounts_controller_spec.rb4
-rw-r--r--spec/controllers/admin/change_email_controller_spec.rb2
-rw-r--r--spec/controllers/admin/confirmations_controller_spec.rb4
-rw-r--r--spec/controllers/admin/domain_blocks_controller_spec.rb6
-rw-r--r--spec/controllers/admin/email_domain_blocks_controller_spec.rb4
-rw-r--r--spec/controllers/admin/instances_controller_spec.rb2
-rw-r--r--spec/controllers/admin/reported_statuses_controller_spec.rb4
-rw-r--r--spec/controllers/admin/reports_controller_spec.rb8
-rw-r--r--spec/controllers/admin/settings_controller_spec.rb2
-rw-r--r--spec/controllers/admin/statuses_controller_spec.rb6
-rw-r--r--spec/controllers/admin/subscriptions_controller_spec.rb2
-rw-r--r--spec/controllers/api/base_controller_spec.rb2
-rw-r--r--spec/controllers/api/oembed_controller_spec.rb2
-rw-r--r--spec/controllers/api/push_controller_spec.rb4
-rw-r--r--spec/controllers/api/salmon_controller_spec.rb2
-rw-r--r--spec/controllers/api/subscriptions_controller_spec.rb6
-rw-r--r--spec/controllers/api/v1/accounts/credentials_controller_spec.rb4
-rw-r--r--spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb2
-rw-r--r--spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb2
-rw-r--r--spec/controllers/api/v1/accounts/lists_controller_spec.rb2
-rw-r--r--spec/controllers/api/v1/accounts/relationships_controller_spec.rb4
-rw-r--r--spec/controllers/api/v1/accounts/search_controller_spec.rb2
-rw-r--r--spec/controllers/api/v1/accounts/statuses_controller_spec.rb8
-rw-r--r--spec/controllers/api/v1/accounts_controller_spec.rb18
-rw-r--r--spec/controllers/api/v1/apps/credentials_controller_spec.rb2
-rw-r--r--spec/controllers/api/v1/apps_controller_spec.rb2
-rw-r--r--spec/controllers/api/v1/blocks_controller_spec.rb2
-rw-r--r--spec/controllers/api/v1/custom_emojis_controller_spec.rb2
-rw-r--r--spec/controllers/api/v1/domain_blocks_controller_spec.rb6
-rw-r--r--spec/controllers/api/v1/follow_requests_controller_spec.rb6
-rw-r--r--spec/controllers/api/v1/follows_controller_spec.rb4
-rw-r--r--spec/controllers/api/v1/instances_controller_spec.rb2
-rw-r--r--spec/controllers/api/v1/lists/accounts_controller_spec.rb6
-rw-r--r--spec/controllers/api/v1/lists_controller_spec.rb10
-rw-r--r--spec/controllers/api/v1/media_controller_spec.rb8
-rw-r--r--spec/controllers/api/v1/mutes_controller_spec.rb2
-rw-r--r--spec/controllers/api/v1/notifications_controller_spec.rb10
-rw-r--r--spec/controllers/api/v1/reports_controller_spec.rb4
-rw-r--r--spec/controllers/api/v1/search_controller_spec.rb2
-rw-r--r--spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb6
-rw-r--r--spec/controllers/api/v1/statuses/favourites_controller_spec.rb4
-rw-r--r--spec/controllers/api/v1/statuses/mutes_controller_spec.rb4
-rw-r--r--spec/controllers/api/v1/statuses/pins_controller_spec.rb4
-rw-r--r--spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb6
-rw-r--r--spec/controllers/api/v1/statuses/reblogs_controller_spec.rb4
-rw-r--r--spec/controllers/api/v1/statuses_controller_spec.rb20
-rw-r--r--spec/controllers/api/v1/timelines/home_controller_spec.rb2
-rw-r--r--spec/controllers/api/v1/timelines/list_controller_spec.rb2
-rw-r--r--spec/controllers/api/v1/timelines/public_controller_spec.rb6
-rw-r--r--spec/controllers/api/v1/timelines/tag_controller_spec.rb4
-rw-r--r--spec/controllers/api/web/settings_controller_spec.rb2
-rw-r--r--spec/controllers/application_controller_spec.rb6
-rw-r--r--spec/controllers/auth/confirmations_controller_spec.rb2
-rw-r--r--spec/controllers/auth/passwords_controller_spec.rb4
-rw-r--r--spec/controllers/auth/registrations_controller_spec.rb12
-rw-r--r--spec/controllers/auth/sessions_controller_spec.rb2
-rw-r--r--spec/controllers/authorize_follows_controller_spec.rb4
-rw-r--r--spec/controllers/concerns/account_controller_concern_spec.rb2
-rw-r--r--spec/controllers/concerns/export_controller_concern_spec.rb2
-rw-r--r--spec/controllers/concerns/localized_spec.rb8
-rw-r--r--spec/controllers/follower_accounts_controller_spec.rb2
-rw-r--r--spec/controllers/following_accounts_controller_spec.rb2
-rw-r--r--spec/controllers/manifests_controller_spec.rb2
-rw-r--r--spec/controllers/media_controller_spec.rb6
-rw-r--r--spec/controllers/oauth/authorizations_controller_spec.rb2
-rw-r--r--spec/controllers/oauth/authorized_applications_controller_spec.rb2
-rw-r--r--spec/controllers/remote_follow_controller_spec.rb4
-rw-r--r--spec/controllers/settings/applications_controller_spec.rb10
-rw-r--r--spec/controllers/settings/deletes_controller_spec.rb2
-rw-r--r--spec/controllers/settings/exports_controller_spec.rb2
-rw-r--r--spec/controllers/settings/follower_domains_controller_spec.rb2
-rw-r--r--spec/controllers/settings/imports_controller_spec.rb2
-rw-r--r--spec/controllers/settings/notifications_controller_spec.rb2
-rw-r--r--spec/controllers/settings/preferences_controller_spec.rb2
-rw-r--r--spec/controllers/settings/profiles_controller_spec.rb2
-rw-r--r--spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb4
-rw-r--r--spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb2
-rw-r--r--spec/controllers/settings/two_factor_authentications_controller_spec.rb4
-rw-r--r--spec/controllers/statuses_controller_spec.rb45
-rw-r--r--spec/controllers/stream_entries_controller_spec.rb2
-rw-r--r--spec/controllers/tags_controller_spec.rb4
-rw-r--r--spec/controllers/well_known/host_meta_controller_spec.rb4
-rw-r--r--spec/controllers/well_known/webfinger_controller_spec.rb6
-rw-r--r--spec/fabricators/account_fabricator.rb8
-rw-r--r--spec/fixtures/requests/activitypub-actor-individual.txt9
-rw-r--r--spec/fixtures/requests/json-ld.activitystreams.txt391
-rw-r--r--spec/fixtures/requests/json-ld.identity.txt100
-rw-r--r--spec/fixtures/requests/json-ld.security.txt61
-rw-r--r--spec/fixtures/requests/oembed_json.html2
-rw-r--r--spec/fixtures/requests/oembed_json_xml.html4
-rw-r--r--spec/fixtures/requests/oembed_xml.html2
-rw-r--r--spec/helpers/jsonld_helper_spec.rb24
-rw-r--r--spec/lib/activitypub/linked_data_signature_spec.rb4
-rw-r--r--spec/lib/formatter_spec.rb6
-rw-r--r--spec/lib/ostatus/atom_serializer_spec.rb32
-rw-r--r--spec/lib/tag_manager_spec.rb34
-rw-r--r--spec/models/account_spec.rb13
-rw-r--r--spec/models/concerns/account_interactions_spec.rb62
-rw-r--r--spec/models/concerns/status_threading_concern_spec.rb12
-rw-r--r--spec/models/report_spec.rb95
-rw-r--r--spec/models/status_pin_spec.rb2
-rw-r--r--spec/models/user_spec.rb214
-rw-r--r--spec/rails_helper.rb14
-rw-r--r--spec/requests/host_meta_request_spec.rb2
-rw-r--r--spec/requests/webfinger_request_spec.rb10
-rw-r--r--spec/services/account_search_service_spec.rb21
-rw-r--r--spec/services/activitypub/fetch_remote_account_service_spec.rb2
-rw-r--r--spec/services/activitypub/fetch_remote_status_service_spec.rb2
-rw-r--r--spec/services/activitypub/process_account_service_spec.rb6
-rw-r--r--spec/services/activitypub/process_collection_service_spec.rb2
-rw-r--r--spec/services/after_block_service_spec.rb2
-rw-r--r--spec/services/authorize_follow_service_spec.rb2
-rw-r--r--spec/services/batched_remove_status_service_spec.rb2
-rw-r--r--spec/services/block_domain_from_account_service_spec.rb2
-rw-r--r--spec/services/block_domain_service_spec.rb2
-rw-r--r--spec/services/block_service_spec.rb2
-rw-r--r--spec/services/bootstrap_timeline_service_spec.rb2
-rw-r--r--spec/services/fan_out_on_write_service_spec.rb2
-rw-r--r--spec/services/favourite_service_spec.rb2
-rw-r--r--spec/services/fetch_atom_service_spec.rb2
-rw-r--r--spec/services/fetch_link_card_service_spec.rb2
-rw-r--r--spec/services/fetch_oembed_service_spec.rb (renamed from spec/lib/provider_discovery_spec.rb)63
-rw-r--r--spec/services/fetch_remote_account_service_spec.rb2
-rw-r--r--spec/services/fetch_remote_status_service_spec.rb2
-rw-r--r--spec/services/follow_service_spec.rb2
-rw-r--r--spec/services/mute_service_spec.rb2
-rw-r--r--spec/services/notify_service_spec.rb2
-rw-r--r--spec/services/post_status_service_spec.rb2
-rw-r--r--spec/services/precompute_feed_service_spec.rb2
-rw-r--r--spec/services/process_feed_service_spec.rb2
-rw-r--r--spec/services/process_interaction_service_spec.rb2
-rw-r--r--spec/services/process_mentions_service_spec.rb2
-rw-r--r--spec/services/pubsubhubbub/subscribe_service_spec.rb2
-rw-r--r--spec/services/pubsubhubbub/unsubscribe_service_spec.rb2
-rw-r--r--spec/services/reblog_service_spec.rb2
-rw-r--r--spec/services/reject_follow_service_spec.rb2
-rw-r--r--spec/services/remove_status_service_spec.rb2
-rw-r--r--spec/services/report_service_spec.rb2
-rw-r--r--spec/services/resolve_account_service_spec.rb16
-rw-r--r--spec/services/resolve_url_service_spec.rb2
-rw-r--r--spec/services/search_service_spec.rb2
-rw-r--r--spec/services/send_interaction_service_spec.rb2
-rw-r--r--spec/services/subscribe_service_spec.rb2
-rw-r--r--spec/services/suspend_account_service_spec.rb2
-rw-r--r--spec/services/unblock_domain_service_spec.rb2
-rw-r--r--spec/services/unblock_service_spec.rb2
-rw-r--r--spec/services/unfollow_service_spec.rb2
-rw-r--r--spec/services/unmute_service_spec.rb2
-rw-r--r--spec/services/unsubscribe_service_spec.rb2
-rw-r--r--spec/services/update_remote_profile_service_spec.rb2
-rw-r--r--spec/spec_helper.rb8
-rw-r--r--spec/views/stream_entries/show.html.haml_spec.rb4
437 files changed, 6000 insertions, 1849 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 000000000..70d03f6b9
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,191 @@
+version: 2
+
+aliases:
+  - &defaults
+    docker:
+      - image: circleci/ruby:2.5.1-stretch-node
+        environment: &ruby_environment
+          BUNDLE_APP_CONFIG: ./.bundle/
+          DB_HOST: localhost
+          DB_USER: root
+          RAILS_ENV: test
+          PARALLEL_TEST_PROCESSORS: 4
+          ALLOW_NOPAM: true
+    working_directory: ~/projects/mastodon/
+
+  - &attach_workspace
+    attach_workspace:
+      at: ~/projects/
+
+  - &persist_to_workspace
+    persist_to_workspace:
+      root: ~/projects/
+      paths:
+        - ./mastodon/
+
+  - &restore_ruby_dependencies
+    restore_cache:
+      keys:
+        - v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
+        - v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-
+        - v2-ruby-dependencies-
+
+  - &install_steps
+    steps:
+      - checkout
+      - *attach_workspace
+
+      - restore_cache:
+          keys:
+            - v1-node-dependencies-{{ checksum "yarn.lock" }}
+            - v1-node-dependencies-
+      - run: yarn install --frozen-lockfile
+      - save_cache:
+          key: v1-node-dependencies-{{ checksum "yarn.lock" }}
+          paths:
+            - ./node_modules/
+
+      - *persist_to_workspace
+
+  - &install_system_dependencies
+      run:
+        name: Install system dependencies
+        command: |
+          sudo apt-get update
+          sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
+
+  - &install_ruby_dependencies
+      steps:
+        - *attach_workspace
+
+        - *install_system_dependencies
+
+        - run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
+        - *restore_ruby_dependencies
+        - run: bundle install --clean --jobs 16 --path ./vendor/bundle/ --retry 3 --with pam_authentication --without development production
+        - save_cache:
+            key: v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
+            paths:
+              - ./.bundle/
+              - ./vendor/bundle/
+
+  - &test_steps
+      steps:
+        - *attach_workspace
+
+        - *install_system_dependencies
+        - run: sudo apt-get install -y ffmpeg
+
+        - run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
+        - *restore_ruby_dependencies
+
+        - restore_cache:
+            keys:
+              - precompiled-assets-{{ .Branch }}-{{ .Revision }}
+              - precompiled-assets-{{ .Branch }}-
+              - precompiled-assets-
+
+        - run:
+            name: Prepare Tests
+            command: ./bin/rails parallel:create parallel:load_schema parallel:prepare
+        - run:
+            name: Run Tests
+            command: bundle exec parallel_test ./spec/ --group-by filesize --type rspec
+
+jobs:
+  install:
+    <<: *defaults
+    <<: *install_steps
+
+  install-ruby2.5:
+    <<: *defaults
+    <<: *install_ruby_dependencies
+
+  install-ruby2.4:
+    <<: *defaults
+    docker:
+      - image: circleci/ruby:2.4.4-stretch-node
+        environment: *ruby_environment
+    <<: *install_ruby_dependencies
+
+  build:
+    <<: *defaults
+    steps:
+      - *attach_workspace
+      - *install_system_dependencies
+      - run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
+      - *restore_ruby_dependencies
+      - run: ./bin/rails assets:precompile
+      - save_cache:
+          key: precompiled-assets-{{ .Branch }}-{{ .Revision }}
+          paths:
+            - ./public/assets
+            - ./public/packs-test/
+
+  test-ruby2.5:
+    <<: *defaults
+    docker:
+      - image: circleci/ruby:2.5.1-stretch-node
+        environment: *ruby_environment
+      - image: circleci/postgres:10.3-alpine
+        environment:
+          POSTGRES_USER: root
+      - image: circleci/redis:4.0.9-alpine
+    <<: *test_steps
+
+  test-ruby2.4:
+    <<: *defaults
+    docker:
+      - image: circleci/ruby:2.4.4-stretch-node
+        environment: *ruby_environment
+      - image: circleci/postgres:10.3-alpine
+        environment:
+          POSTGRES_USER: root
+      - image: circleci/redis:4.0.9-alpine
+    <<: *test_steps
+
+  test-webui:
+    <<: *defaults
+    docker:
+      - image: circleci/node:8.11.1-stretch
+    steps:
+      - *attach_workspace
+      - run: yarn test:jest
+
+  check-i18n:
+    <<: *defaults
+    steps:
+      - *attach_workspace
+      - run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
+      - *restore_ruby_dependencies
+      - run: bundle exec i18n-tasks check-normalized
+      - run: bundle exec i18n-tasks unused
+
+workflows:
+  version: 2
+  build-and-test:
+    jobs:
+      - install
+      - install-ruby2.5:
+          requires:
+            - install
+      - install-ruby2.4:
+          requires:
+            - install
+      - build:
+          requires:
+            - install-ruby2.5
+      - test-ruby2.5:
+          requires:
+            - install-ruby2.5
+            - build
+      - test-ruby2.4:
+          requires:
+            - install-ruby2.4
+            - build
+      - test-webui:
+          requires:
+            - install
+      - check-i18n:
+          requires:
+            - install-ruby2.5
diff --git a/.env.production.sample b/.env.production.sample
index 0c158b06e..eddd38d90 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -217,3 +217,10 @@ STREAMING_CLUSTER_NUM=1
 # SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1"
 # SAML_ATTRIBUTES_STATEMENTS_VERIFIED=
 # SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL=
+
+# Use HTTP proxy for outgoing request (optional)
+# http_proxy=http://gateway.local:8118
+# Access control for hidden service.
+# ALLOW_ACCESS_TO_HIDDEN_SERVICE=true
+# If you use transparent proxy to access to hidden service, uncomment following for skipping private address check.
+# HIDDEN_SERVICE_VIA_TRANSPARENT_PROXY=true
diff --git a/.env.test b/.env.test
index 7da76f8ef..726351c5e 100644
--- a/.env.test
+++ b/.env.test
@@ -1,3 +1,5 @@
+# Node.js
+NODE_ENV=test
 # Federation
 LOCAL_DOMAIN=cb6e6126.ngrok.io
 LOCAL_HTTPS=true
diff --git a/Gemfile b/Gemfile
index 068b4874d..c3f4a62f2 100644
--- a/Gemfile
+++ b/Gemfile
@@ -3,7 +3,7 @@
 source 'https://rubygems.org'
 ruby '>= 2.3.0', '< 2.6.0'
 
-gem 'pkg-config', '~> 1.2'
+gem 'pkg-config', '~> 1.3'
 
 gem 'puma', '~> 3.11'
 gem 'rails', '~> 5.2.0'
@@ -11,11 +11,11 @@ gem 'rails', '~> 5.2.0'
 gem 'hamlit-rails', '~> 0.2'
 gem 'pg', '~> 1.0'
 gem 'pghero', '~> 2.1'
-gem 'dotenv-rails', '~> 2.2'
+gem 'dotenv-rails', '~> 2.2', '< 2.3'
 
-gem 'aws-sdk-s3', '~> 1.8', require: false
+gem 'aws-sdk-s3', '~> 1.9', require: false
 gem 'fog-core', '~> 1.45'
-gem 'fog-local', '~> 0.4', require: false
+gem 'fog-local', '~> 0.5', require: false
 gem 'fog-openstack', '~> 0.1', require: false
 gem 'paperclip', '~> 6.0'
 gem 'paperclip-av-transcoder', '~> 0.6'
@@ -31,7 +31,7 @@ gem 'iso-639'
 gem 'chewy', '~> 5.0'
 gem 'cld3', '~> 3.2.0'
 gem 'devise', '~> 4.4'
-gem 'devise-two-factor', '~> 3.0', git: 'https://github.com/ykzts/devise-two-factor.git', branch: 'rails-5.2'
+gem 'devise-two-factor', '~> 3.0'
 
 group :pam_authentication, optional: true do
   gem 'devise_pam_authenticatable2', '~> 9.1'
@@ -50,18 +50,18 @@ gem 'hiredis', '~> 0.6'
 gem 'redis-namespace', '~> 1.5'
 gem 'html2text'
 gem 'htmlentities', '~> 4.3'
-gem 'http', '~> 3.0'
+gem 'http', '~> 3.2'
 gem 'http_accept_language', '~> 2.1'
 gem 'httplog', '~> 1.0'
 gem 'idn-ruby', require: 'idn'
 gem 'kaminari', '~> 1.1'
 gem 'link_header', '~> 0.0'
-gem 'mime-types', '~> 3.1'
+gem 'mime-types', '~> 3.1', require: 'mime/types/columnar'
 gem 'nokogiri', '~> 1.8'
 gem 'nsa', '~> 0.2'
-gem 'oj', '~> 3.4'
+gem 'oj', '~> 3.5'
 gem 'ostatus2', '~> 2.0'
-gem 'ox', '~> 2.8'
+gem 'ox', '~> 2.9'
 gem 'pundit', '~> 1.1'
 gem 'premailer-rails'
 gem 'rack-attack', '~> 5.2'
@@ -72,7 +72,6 @@ gem 'rails-settings-cached', '~> 0.6'
 gem 'redis', '~> 4.0', require: ['redis', 'redis/connection/hiredis']
 gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
 gem 'rqrcode', '~> 0.10'
-gem 'ruby-oembed', '~> 0.12', require: 'oembed'
 gem 'ruby-progressbar', '~> 1.4'
 gem 'sanitize', '~> 4.6'
 gem 'sidekiq', '~> 5.1'
@@ -84,20 +83,21 @@ gem 'simple_form', '~> 4.0'
 gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
 gem 'stoplight', '~> 2.1.3'
 gem 'strong_migrations', '~> 0.2'
-gem 'tty-command'
-gem 'tty-prompt'
+gem 'tty-command', '~> 0.8', require: false
+gem 'tty-prompt', '~> 0.16', require: false
 gem 'twitter-text', '~> 1.14'
 gem 'tzinfo-data', '~> 1.2018'
 gem 'webpacker', '~> 3.4'
 gem 'webpush'
 
-gem 'json-ld-preloaded', '~> 2.2'
+gem 'json-ld', '~> 2.2'
 gem 'rdf-normalize', '~> 0.3'
 
 group :development, :test do
   gem 'fabrication', '~> 2.20'
   gem 'fuubar', '~> 2.2'
   gem 'i18n-tasks', '~> 0.9', require: false
+  gem 'pry-byebug', '~> 3.6'
   gem 'pry-rails', '~> 0.3'
   gem 'rspec-rails', '~> 3.7'
 end
@@ -113,7 +113,8 @@ group :test do
   gem 'microformats', '~> 4.0'
   gem 'rails-controller-testing', '~> 1.0'
   gem 'rspec-sidekiq', '~> 3.0'
-  gem 'simplecov', '~> 0.14', require: false
+  gem 'rspec-retry', '~> 0.5', require: false
+  gem 'simplecov', '~> 0.16', require: false
   gem 'webmock', '~> 3.3'
   gem 'parallel_tests', '~> 2.21'
 end
@@ -127,18 +128,21 @@ group :development do
   gem 'letter_opener', '~> 1.4'
   gem 'letter_opener_web', '~> 1.3'
   gem 'memory_profiler'
-  gem 'rubocop', require: false
+  gem 'rubocop', '~> 0.55', require: false
   gem 'brakeman', '~> 4.2', require: false
   gem 'bundler-audit', '~> 0.6', require: false
-  gem 'scss_lint', '~> 0.55', require: false
+  gem 'scss_lint', '~> 0.57', require: false
 
   gem 'capistrano', '~> 3.10'
   gem 'capistrano-rails', '~> 1.3'
   gem 'capistrano-rbenv', '~> 2.1'
   gem 'capistrano-yarn', '~> 2.0'
+
+  gem 'derailed_benchmarks'
+  gem 'stackprof'
 end
 
 group :production do
-  gem 'lograge', '~> 0.9'
+  gem 'lograge', '~> 0.10'
   gem 'redis-rails', '~> 5.0'
 end
diff --git a/Gemfile.lock b/Gemfile.lock
index 09ee34f89..2e2cf1f3d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,15 +1,3 @@
-GIT
-  remote: https://github.com/ykzts/devise-two-factor.git
-  revision: f60492b29c174d4c959ac02406392f8eb9c4d374
-  branch: rails-5.2
-  specs:
-    devise-two-factor (3.0.2)
-      activesupport (< 5.3)
-      attr_encrypted (>= 1.3, < 4, != 2)
-      devise (~> 4.0)
-      railties (< 5.3)
-      rotp (~> 2.0)
-
 GEM
   remote: https://rubygems.org/
   specs:
@@ -64,7 +52,7 @@ GEM
       public_suffix (>= 2.0.2, < 4.0)
     airbrussh (1.3.0)
       sshkit (>= 1.6.1, != 1.7.0)
-    annotate (2.7.2)
+    annotate (2.7.3)
       activerecord (>= 3.2, < 6.0)
       rake (>= 10.4, < 13.0)
     arel (9.0.0)
@@ -73,20 +61,21 @@ GEM
       encryptor (~> 3.0.0)
     av (0.9.0)
       cocaine (~> 0.5.3)
-    aws-partitions (1.70.0)
-    aws-sdk-core (3.17.0)
+    aws-partitions (1.80.0)
+    aws-sdk-core (3.19.0)
       aws-partitions (~> 1.0)
       aws-sigv4 (~> 1.0)
       jmespath (~> 1.0)
     aws-sdk-kms (1.5.0)
       aws-sdk-core (~> 3)
       aws-sigv4 (~> 1.0)
-    aws-sdk-s3 (1.8.2)
+    aws-sdk-s3 (1.9.1)
       aws-sdk-core (~> 3)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.0)
     aws-sigv4 (1.0.2)
     bcrypt (3.1.11)
+    benchmark-ips (2.7.2)
     better_errors (2.4.0)
       coderay (>= 1.0.0)
       erubi (>= 1.0.0)
@@ -96,7 +85,7 @@ GEM
     bootsnap (1.3.0)
       msgpack (~> 1.0)
     brakeman (4.2.1)
-    browser (2.5.2)
+    browser (2.5.3)
     builder (3.2.3)
     bullet (5.7.5)
       activesupport (>= 3.0.0)
@@ -104,7 +93,8 @@ GEM
     bundler-audit (0.6.0)
       bundler (~> 1.2)
       thor (~> 0.18)
-    capistrano (3.10.1)
+    byebug (10.0.2)
+    capistrano (3.10.2)
       airbrussh (>= 1.0.0)
       i18n
       rake (>= 10.0.0)
@@ -150,18 +140,32 @@ GEM
     css_parser (1.6.0)
       addressable
     debug_inspector (0.0.3)
+    derailed_benchmarks (1.3.4)
+      benchmark-ips (~> 2)
+      get_process_mem (~> 0)
+      heapy (~> 0)
+      memory_profiler (~> 0)
+      rack (>= 1)
+      rake (> 10, < 13)
+      thor (~> 0.19)
     devise (4.4.3)
       bcrypt (~> 3.0)
       orm_adapter (~> 0.1)
       railties (>= 4.1.0, < 6.0)
       responders
       warden (~> 1.2.3)
+    devise-two-factor (3.0.3)
+      activesupport (< 5.3)
+      attr_encrypted (>= 1.3, < 4, != 2)
+      devise (~> 4.0)
+      railties (< 5.3)
+      rotp (~> 2.0)
     devise_pam_authenticatable2 (9.1.0)
       devise (>= 4.0.0)
       rpam2 (~> 4.0)
     diff-lcs (1.3)
-    docile (1.1.5)
-    domain_name (0.5.20170404)
+    docile (1.3.0)
+    domain_name (0.5.20180417)
       unf (>= 0.0.5, < 1.0.0)
     doorkeeper (4.3.2)
       railties (>= 4.2)
@@ -172,29 +176,29 @@ GEM
     easy_translate (0.5.1)
       thread
       thread_safe
-    elasticsearch (6.0.1)
-      elasticsearch-api (= 6.0.1)
-      elasticsearch-transport (= 6.0.1)
-    elasticsearch-api (6.0.1)
+    elasticsearch (6.0.2)
+      elasticsearch-api (= 6.0.2)
+      elasticsearch-transport (= 6.0.2)
+    elasticsearch-api (6.0.2)
       multi_json
     elasticsearch-dsl (0.1.5)
-    elasticsearch-transport (6.0.1)
+    elasticsearch-transport (6.0.2)
       faraday
       multi_json
     encryptor (3.0.0)
     equatable (0.5.0)
     erubi (1.7.1)
-    et-orbi (1.0.9)
+    et-orbi (1.1.0)
       tzinfo
-    excon (0.60.0)
+    excon (0.62.0)
     fabrication (2.20.1)
     faker (1.8.7)
       i18n (>= 0.7)
-    faraday (0.14.0)
+    faraday (0.15.0)
       multipart-post (>= 1.2, < 3)
     fast_blank (1.0.0)
     fastimage (2.1.1)
-    ffi (1.9.21)
+    ffi (1.9.23)
     fog-core (1.45.0)
       builder
       excon (~> 0.58)
@@ -202,9 +206,9 @@ GEM
     fog-json (1.0.2)
       fog-core (~> 1.0)
       multi_json (~> 1.10)
-    fog-local (0.4.0)
-      fog-core (~> 1.27)
-    fog-openstack (0.1.23)
+    fog-local (0.5.0)
+      fog-core (>= 1.27, < 3.0)
+    fog-openstack (0.1.25)
       fog-core (~> 1.40)
       fog-json (>= 1.0)
       ipaddress (>= 0.8)
@@ -212,6 +216,7 @@ GEM
     fuubar (2.3.1)
       rspec-core (~> 3.0)
       ruby-progressbar (~> 1.4)
+    get_process_mem (0.2.1)
     globalid (0.4.1)
       activesupport (>= 4.2.0)
     goldfinger (2.1.0)
@@ -232,6 +237,7 @@ GEM
       concurrent-ruby (~> 1.0)
     hashdiff (0.3.7)
     hashie (3.5.7)
+    heapy (0.1.3)
     highline (1.7.10)
     hiredis (0.6.1)
     hitimes (1.2.6)
@@ -239,20 +245,20 @@ GEM
     html2text (0.2.1)
       nokogiri (~> 1.6)
     htmlentities (4.3.4)
-    http (3.0.0)
+    http (3.2.0)
       addressable (~> 2.3)
       http-cookie (~> 1.0)
-      http-form_data (>= 2.0.0.pre.pre2, < 3)
+      http-form_data (~> 2.0)
       http_parser.rb (~> 0.6.0)
     http-cookie (1.0.3)
       domain_name (~> 0.5)
-    http-form_data (2.0.0)
+    http-form_data (2.1.0)
     http_accept_language (2.1.1)
     http_parser.rb (0.6.0)
     httplog (1.0.2)
       colorize (~> 0.8)
       rack (>= 1.0)
-    i18n (1.0.0)
+    i18n (1.0.1)
       concurrent-ruby (~> 1.0)
     i18n-tasks (0.9.21)
       activesupport (>= 4.0.2)
@@ -267,15 +273,11 @@ GEM
     idn-ruby (0.1.0)
     ipaddress (0.8.3)
     iso-639 (0.2.8)
-    jmespath (1.3.1)
+    jmespath (1.4.0)
     json (2.1.0)
     json-ld (2.2.1)
       multi_json (~> 1.12)
       rdf (>= 2.2.8, < 4.0)
-    json-ld-preloaded (2.2.3)
-      json-ld (>= 2.2, < 4.0)
-      multi_json (~> 1.12)
-      rdf (>= 2.2, < 4.0)
     jsonapi-renderer (0.2.0)
     jwt (2.1.0)
     kaminari (1.1.1)
@@ -299,7 +301,7 @@ GEM
       letter_opener (~> 1.0)
       railties (>= 3.2)
     link_header (0.0.8)
-    lograge (0.9.0)
+    lograge (0.10.0)
       actionpack (>= 4)
       activesupport (>= 4)
       railties (>= 4)
@@ -343,7 +345,7 @@ GEM
       concurrent-ruby (~> 1.0.0)
       sidekiq (>= 3.5.0)
       statsd-ruby (~> 1.2.0)
-    oj (3.4.0)
+    oj (3.5.1)
     omniauth (1.8.1)
       hashie (>= 3.4.6, < 3.6.0)
       rack (>= 1.6.2, < 3)
@@ -359,7 +361,7 @@ GEM
       addressable (~> 2.5)
       http (~> 3.0)
       nokogiri (~> 1.8)
-    ox (2.8.2)
+    ox (2.9.2)
     paperclip (6.0.0)
       activemodel (>= 4.2.0)
       activesupport (>= 4.2.0)
@@ -370,7 +372,7 @@ GEM
       av (~> 0.9.0)
       paperclip (>= 2.5.2)
     parallel (1.12.1)
-    parallel_tests (2.21.1)
+    parallel_tests (2.21.3)
       parallel
     parser (2.5.1.0)
       ast (~> 2.4.0)
@@ -380,7 +382,7 @@ GEM
     pg (1.0.0)
     pghero (2.1.0)
       activerecord
-    pkg-config (1.2.9)
+    pkg-config (1.3.0)
     posix-spawn (0.3.13)
     powerpack (0.1.1)
     premailer (1.11.1)
@@ -394,10 +396,13 @@ GEM
     pry (0.11.3)
       coderay (~> 1.1.0)
       method_source (~> 0.9.0)
+    pry-byebug (3.6.0)
+      byebug (~> 10.0)
+      pry (~> 0.10)
     pry-rails (0.3.6)
       pry (>= 0.10.4)
     public_suffix (3.0.2)
-    puma (3.11.3)
+    puma (3.11.4)
     pundit (1.1.0)
       activesupport (>= 3.0.0)
     rack (2.0.4)
@@ -446,10 +451,10 @@ GEM
       thor (>= 0.18.1, < 2.0)
     rainbow (3.0.0)
     rake (12.3.1)
-    rb-fsevent (0.10.2)
+    rb-fsevent (0.10.3)
     rb-inotify (0.9.10)
       ffi (>= 0.5.0, < 2)
-    rdf (3.0.1)
+    rdf (3.0.2)
       hamster (~> 3.0)
       link_header (~> 0.0, >= 0.0.8)
     rdf-normalize (0.3.3)
@@ -471,9 +476,9 @@ GEM
       redis-actionpack (>= 5.0, < 6)
       redis-activesupport (>= 5.0, < 6)
       redis-store (>= 1.2, < 2)
-    redis-store (1.4.1)
+    redis-store (1.5.0)
       redis (>= 2.2, < 5)
-    request_store (1.4.0)
+    request_store (1.4.1)
       rack (>= 1.4)
     responders (2.4.0)
       actionpack (>= 4.2.0, < 5.3)
@@ -498,18 +503,19 @@ GEM
       rspec-expectations (~> 3.7.0)
       rspec-mocks (~> 3.7.0)
       rspec-support (~> 3.7.0)
+    rspec-retry (0.5.7)
+      rspec-core (> 3.3)
     rspec-sidekiq (3.0.3)
       rspec-core (~> 3.0, >= 3.0.0)
       sidekiq (>= 2.4.0)
     rspec-support (3.7.1)
-    rubocop (0.52.1)
+    rubocop (0.55.0)
       parallel (~> 1.10)
-      parser (>= 2.4.0.2, < 3.0)
+      parser (>= 2.5)
       powerpack (~> 0.1)
       rainbow (>= 2.2.2, < 4.0)
       ruby-progressbar (~> 1.7)
       unicode-display_width (~> 1.0, >= 1.0.1)
-    ruby-oembed (0.12.0)
     ruby-progressbar (1.9.0)
     ruby-saml (1.7.2)
       nokogiri (>= 1.5.10)
@@ -520,14 +526,14 @@ GEM
       crass (~> 1.0.2)
       nokogiri (>= 1.4.4)
       nokogumbo (~> 1.4)
-    sass (3.5.5)
+    sass (3.5.6)
       sass-listen (~> 4.0.0)
     sass-listen (4.0.0)
       rb-fsevent (~> 0.9, >= 0.9.4)
       rb-inotify (~> 0.9, >= 0.9.7)
-    scss_lint (0.56.0)
+    scss_lint (0.57.0)
       rake (>= 0.9, < 13)
-      sass (~> 3.5.3)
+      sass (~> 3.5.5)
     sidekiq (5.1.3)
       concurrent-ruby (~> 1.0)
       connection_pool (~> 2.2, >= 2.2.0)
@@ -549,8 +555,8 @@ GEM
     simple_form (4.0.0)
       actionpack (> 4)
       activemodel (> 4)
-    simplecov (0.15.1)
-      docile (~> 1.1.0)
+    simplecov (0.16.1)
+      docile (~> 1.1)
       json (>= 1.8, < 3)
       simplecov-html (~> 0.10.0)
     simplecov-html (0.10.2)
@@ -564,6 +570,7 @@ GEM
     sshkit (1.16.0)
       net-scp (>= 1.1.2)
       net-ssh (>= 2.8.0)
+    stackprof (0.2.11)
     statsd-ruby (1.2.1)
     stoplight (2.1.3)
     streamio-ffmpeg (3.0.2)
@@ -582,10 +589,10 @@ GEM
     timers (4.1.2)
       hitimes
     tty-color (0.4.2)
-    tty-command (0.7.0)
+    tty-command (0.8.0)
       pastel (~> 0.7.0)
     tty-cursor (0.5.0)
-    tty-prompt (0.15.0)
+    tty-prompt (0.16.0)
       necromancer (~> 0.4.0)
       pastel (~> 0.7.0)
       timers (~> 4.0)
@@ -605,7 +612,7 @@ GEM
     unf (0.1.4)
       unf_ext
     unf_ext (0.0.7.5)
-    unicode-display_width (1.3.0)
+    unicode-display_width (1.3.2)
     uniform_notifier (1.11.0)
     warden (1.2.7)
       rack (>= 1.0)
@@ -635,7 +642,7 @@ DEPENDENCIES
   active_record_query_trace (~> 1.5)
   addressable (~> 2.5)
   annotate (~> 2.7)
-  aws-sdk-s3 (~> 1.8)
+  aws-sdk-s3 (~> 1.9)
   better_errors (~> 2.4)
   binding_of_caller (~> 0.7)
   bootsnap (~> 1.3)
@@ -652,17 +659,18 @@ DEPENDENCIES
   chewy (~> 5.0)
   cld3 (~> 3.2.0)
   climate_control (~> 0.2)
+  derailed_benchmarks
   devise (~> 4.4)
-  devise-two-factor (~> 3.0)!
+  devise-two-factor (~> 3.0)
   devise_pam_authenticatable2 (~> 9.1)
   doorkeeper (~> 4.3)
-  dotenv-rails (~> 2.2)
+  dotenv-rails (~> 2.2, < 2.3)
   fabrication (~> 2.20)
   faker (~> 1.8)
   fast_blank (~> 1.0)
   fastimage
   fog-core (~> 1.45)
-  fog-local (~> 0.4)
+  fog-local (~> 0.5)
   fog-openstack (~> 0.1)
   fuubar (~> 2.2)
   goldfinger (~> 2.1)
@@ -670,18 +678,18 @@ DEPENDENCIES
   hiredis (~> 0.6)
   html2text
   htmlentities (~> 4.3)
-  http (~> 3.0)
+  http (~> 3.2)
   http_accept_language (~> 2.1)
   httplog (~> 1.0)
   i18n-tasks (~> 0.9)
   idn-ruby
   iso-639
-  json-ld-preloaded (~> 2.2)
+  json-ld (~> 2.2)
   kaminari (~> 1.1)
   letter_opener (~> 1.4)
   letter_opener_web (~> 1.3)
   link_header (~> 0.0)
-  lograge (~> 0.9)
+  lograge (~> 0.10)
   mario-redis-lock (~> 1.2)
   memory_profiler
   microformats (~> 4.0)
@@ -689,21 +697,22 @@ DEPENDENCIES
   net-ldap (~> 0.10)
   nokogiri (~> 1.8)
   nsa (~> 0.2)
-  oj (~> 3.4)
+  oj (~> 3.5)
   omniauth (~> 1.2)
   omniauth-cas (~> 1.1)
   omniauth-saml (~> 1.10)
   ostatus2 (~> 2.0)
-  ox (~> 2.8)
+  ox (~> 2.9)
   paperclip (~> 6.0)
   paperclip-av-transcoder (~> 0.6)
   parallel_tests (~> 2.21)
   pg (~> 1.0)
   pghero (~> 2.1)
-  pkg-config (~> 1.2)
+  pkg-config (~> 1.3)
   posix-spawn
   premailer-rails
   private_address_check (~> 0.4.1)
+  pry-byebug (~> 3.6)
   pry-rails (~> 0.3)
   puma (~> 3.11)
   pundit (~> 1.1)
@@ -720,25 +729,26 @@ DEPENDENCIES
   redis-rails (~> 5.0)
   rqrcode (~> 0.10)
   rspec-rails (~> 3.7)
+  rspec-retry (~> 0.5)
   rspec-sidekiq (~> 3.0)
-  rubocop
-  ruby-oembed (~> 0.12)
+  rubocop (~> 0.55)
   ruby-progressbar (~> 1.4)
   sanitize (~> 4.6)
-  scss_lint (~> 0.55)
+  scss_lint (~> 0.57)
   sidekiq (~> 5.1)
   sidekiq-bulk (~> 0.1.1)
   sidekiq-scheduler (~> 2.2)
   sidekiq-unique-jobs (~> 5.0)
   simple-navigation (~> 4.0)
   simple_form (~> 4.0)
-  simplecov (~> 0.14)
+  simplecov (~> 0.16)
   sprockets-rails (~> 3.2)
+  stackprof
   stoplight (~> 2.1.3)
   streamio-ffmpeg (~> 3.0)
   strong_migrations (~> 0.2)
-  tty-command
-  tty-prompt
+  tty-command (~> 0.8)
+  tty-prompt (~> 0.16)
   twitter-text (~> 1.14)
   tzinfo-data (~> 1.2018)
   webmock (~> 3.3)
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 1efaf619b..50f5d0b11 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -21,9 +21,10 @@ class AccountsController < ApplicationController
         @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
         @statuses        = filtered_status_page(params)
         @statuses        = cache_collection(@statuses, Status)
+
         unless @statuses.empty?
-          @older_url        = older_url if @statuses.last.id > filtered_statuses.last.id
-          @newer_url        = newer_url if @statuses.first.id < filtered_statuses.first.id
+          @older_url = older_url if @statuses.last.id > filtered_statuses.last.id
+          @newer_url = newer_url if @statuses.first.id < filtered_statuses.first.id
         end
       end
 
@@ -32,6 +33,11 @@ class AccountsController < ApplicationController
         render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? }))
       end
 
+      format.rss do
+        @statuses = cache_collection(default_statuses.without_reblogs.without_replies.limit(PAGE_SIZE), Status)
+        render xml: RSS::AccountSerializer.render(@account, @statuses)
+      end
+
       format.json do
         skip_session!
 
diff --git a/app/controllers/admin/reported_statuses_controller.rb b/app/controllers/admin/reported_statuses_controller.rb
index 535bd11d4..522f68c98 100644
--- a/app/controllers/admin/reported_statuses_controller.rb
+++ b/app/controllers/admin/reported_statuses_controller.rb
@@ -8,7 +8,7 @@ module Admin
     def create
       authorize :status, :update?
 
-      @form         = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account))
+      @form         = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button))
       flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
 
       redirect_to admin_report_path(@report)
@@ -35,7 +35,17 @@ module Admin
     end
 
     def form_status_batch_params
-      params.require(:form_status_batch).permit(:action, status_ids: [])
+      params.require(:form_status_batch).permit(status_ids: [])
+    end
+
+    def action_from_button
+      if params[:nsfw_on]
+        'nsfw_on'
+      elsif params[:nsfw_off]
+        'nsfw_off'
+      elsif params[:delete]
+        'delete'
+      end
     end
 
     def set_report
diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb
index a4ae9507d..d00b3d222 100644
--- a/app/controllers/admin/reports_controller.rb
+++ b/app/controllers/admin/reports_controller.rb
@@ -11,10 +11,10 @@ module Admin
 
     def show
       authorize @report, :show?
-      @report_note = @report.notes.new
-      @report_notes = @report.notes.latest
-      @report_history = @report.history
-      @form = Form::StatusBatch.new
+
+      @report_note  = @report.notes.new
+      @report_notes = (@report.notes.latest + @report.history).sort_by(&:created_at)
+      @form         = Form::StatusBatch.new
     end
 
     def update
diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index 7b5168b31..b5c084e14 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -66,8 +66,10 @@ class Api::BaseController < ApplicationController
   end
 
   def require_user!
-    if current_user
+    if current_user && !current_user.disabled?
       set_user_activity
+    elsif current_user
+      render json: { error: 'Your login is currently disabled' }, status: 403
     else
       render json: { error: 'This method requires an authenticated user' }, status: 422
     end
diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb
index 062d490a7..a3c4008e6 100644
--- a/app/controllers/api/v1/accounts/credentials_controller.rb
+++ b/app/controllers/api/v1/accounts/credentials_controller.rb
@@ -21,7 +21,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
   private
 
   def account_params
-    params.permit(:display_name, :note, :avatar, :header, :locked)
+    params.permit(:display_name, :note, :avatar, :header, :locked, fields_attributes: [:name, :value])
   end
 
   def user_settings_params
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index d64325944..b7133ca8e 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -5,6 +5,7 @@ class Api::V1::AccountsController < Api::BaseController
   before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock, :mute, :unmute]
   before_action :require_user!, except: [:show]
   before_action :set_account
+  before_action :check_account_suspension, only: [:show]
 
   respond_to :json
 
@@ -54,4 +55,8 @@ class Api::V1::AccountsController < Api::BaseController
   def relationships(**options)
     AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
   end
+
+  def check_account_suspension
+    gone if @account.suspended?
+  end
 end
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index e98241323..01880565c 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -18,7 +18,7 @@ class Api::V1::StatusesController < Api::BaseController
 
   def context
     ancestors_results   = @status.in_reply_to_id.nil? ? [] : @status.ancestors(DEFAULT_STATUSES_LIMIT, current_account)
-    descendants_results = @status.descendants(current_account)
+    descendants_results = @status.descendants(DEFAULT_STATUSES_LIMIT, current_account)
     loaded_ancestors    = cache_collection(ancestors_results, Status)
     loaded_descendants  = cache_collection(descendants_results, Status)
 
diff --git a/app/controllers/api/web/embeds_controller.rb b/app/controllers/api/web/embeds_controller.rb
index f2fe74b17..987290a14 100644
--- a/app/controllers/api/web/embeds_controller.rb
+++ b/app/controllers/api/web/embeds_controller.rb
@@ -9,9 +9,12 @@ class Api::Web::EmbedsController < Api::Web::BaseController
     status = StatusFinder.new(params[:url]).status
     render json: status, serializer: OEmbedSerializer, width: 400
   rescue ActiveRecord::RecordNotFound
-    oembed = OEmbed::Providers.get(params[:url])
-    render json: Oj.dump(oembed.fields)
-  rescue OEmbed::NotFound
-    render json: {}, status: :not_found
+    oembed = FetchOEmbedService.new.call(params[:url])
+
+    if oembed
+      render json: oembed
+    else
+      render json: {}, status: :not_found
+    end
   end
 end
diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb
index abd85ea27..145549bcd 100644
--- a/app/controllers/concerns/localized.rb
+++ b/app/controllers/concerns/localized.rb
@@ -29,10 +29,14 @@ module Localized
   end
 
   def preferred_locale
-    http_accept_language.preferred_language_from(I18n.available_locales)
+    http_accept_language.preferred_language_from(available_locales)
   end
 
   def compatible_locale
-    http_accept_language.compatible_language_from(I18n.available_locales)
+    http_accept_language.compatible_language_from(available_locales)
+  end
+
+  def available_locales
+    I18n.available_locales.reverse
   end
 end
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index 3237a15b9..2e9cf14e0 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -4,7 +4,9 @@ class StatusesController < ApplicationController
   include SignatureAuthentication
   include Authorization
 
-  ANCESTORS_LIMIT = 20
+  ANCESTORS_LIMIT         = 40
+  DESCENDANTS_LIMIT       = 60
+  DESCENDANTS_DEPTH_LIMIT = 20
 
   layout 'public'
 
@@ -20,9 +22,8 @@ class StatusesController < ApplicationController
     respond_to do |format|
       format.html do
         use_pack 'public'
-        @ancestors     = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : []
-        @next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift
-        @descendants   = cache_collection(@status.descendants(current_account), Status)
+        set_ancestors
+        set_descendants
 
         render 'stream_entries/show'
       end
@@ -53,10 +54,77 @@ class StatusesController < ApplicationController
 
   private
 
+  def create_descendant_thread(depth, statuses)
+    if depth < DESCENDANTS_DEPTH_LIMIT
+      { statuses: statuses }
+    else
+      next_status = statuses.pop
+      { statuses: statuses, next_status: next_status }
+    end
+  end
+
   def set_account
     @account = Account.find_local!(params[:account_username])
   end
 
+  def set_ancestors
+    @ancestors     = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : []
+    @next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift
+  end
+
+  def set_descendants
+    @max_descendant_thread_id   = params[:max_descendant_thread_id]&.to_i
+    @since_descendant_thread_id = params[:since_descendant_thread_id]&.to_i
+
+    descendants = cache_collection(
+      @status.descendants(
+        DESCENDANTS_LIMIT,
+        current_account,
+        @max_descendant_thread_id,
+        @since_descendant_thread_id,
+        DESCENDANTS_DEPTH_LIMIT
+      ),
+      Status
+    )
+
+    @descendant_threads = []
+
+    if descendants.present?
+      statuses = [descendants.first]
+      depth    = 1
+
+      descendants.drop(1).each_with_index do |descendant, index|
+        if descendants[index].id == descendant.in_reply_to_id
+          depth += 1
+          statuses << descendant
+        else
+          @descendant_threads << create_descendant_thread(depth, statuses)
+
+          @descendant_threads.reverse_each do |descendant_thread|
+            statuses = descendant_thread[:statuses]
+
+            index = statuses.find_index do |thread_status|
+              thread_status.id == descendant.in_reply_to_id
+            end
+
+            if index.present?
+              depth += index - statuses.size
+              break
+            end
+
+            depth -= statuses.size
+          end
+
+          statuses = [descendant]
+        end
+      end
+
+      @descendant_threads << create_descendant_thread(depth, statuses)
+    end
+
+    @max_descendant_thread_id = @descendant_threads.pop[:statuses].first.id if descendants.size >= DESCENDANTS_LIMIT
+  end
+
   def set_link_headers
     response.headers['Link'] = LinkHeader.new(
       [
diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb
index 44e9c0bb8..8cb54a148 100644
--- a/app/controllers/stream_entries_controller.rb
+++ b/app/controllers/stream_entries_controller.rb
@@ -24,6 +24,7 @@ class StreamEntriesController < ApplicationController
           skip_session!
           expires_in 3.minutes, public: true
         end
+
         render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
       end
     end
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index 5d11a8139..a76be26e5 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class TagsController < ApplicationController
+  PAGE_SIZE = 20
+
   before_action :set_body_classes
   before_action :set_instance_presenter
 
@@ -14,8 +16,15 @@ class TagsController < ApplicationController
         @initial_state_json   = serializable_resource.to_json
       end
 
+      format.rss do
+        @statuses = Status.as_tag_timeline(@tag).limit(PAGE_SIZE)
+        @statuses = cache_collection(@statuses, Status)
+
+        render xml: RSS::TagSerializer.render(@tag, @statuses)
+      end
+
       format.json do
-        @statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id])
+        @statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id])
         @statuses = cache_collection(@statuses, Status)
 
         render json: collection_presenter,
diff --git a/app/helpers/admin/account_moderation_notes_helper.rb b/app/helpers/admin/account_moderation_notes_helper.rb
index b17c52264..fdfadef08 100644
--- a/app/helpers/admin/account_moderation_notes_helper.rb
+++ b/app/helpers/admin/account_moderation_notes_helper.rb
@@ -1,4 +1,20 @@
 # frozen_string_literal: true
 
 module Admin::AccountModerationNotesHelper
+  def admin_account_link_to(account)
+    link_to admin_account_path(account.id), class: name_tag_classes(account) do
+      safe_join([
+                  image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'),
+                  content_tag(:span, account.acct, class: 'username'),
+                ], ' ')
+    end
+  end
+
+  private
+
+  def name_tag_classes(account)
+    classes = ['name-tag']
+    classes << 'suspended' if account.suspended?
+    classes.join(' ')
+  end
 end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index bab4615a1..95863ab1f 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -63,4 +63,8 @@ module ApplicationHelper
   def opengraph(property, content)
     tag(:meta, content: content, property: property)
   end
+
+  def react_component(name, props = {})
+    content_tag(:div, nil, data: { component: name.to_s.camelcase, props: Oj.dump(props) })
+  end
 end
diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb
index dfb8fcb8b..e9056166c 100644
--- a/app/helpers/jsonld_helper.rb
+++ b/app/helpers/jsonld_helper.rb
@@ -5,6 +5,10 @@ module JsonLdHelper
     haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle
   end
 
+  def equals_or_includes_any?(haystack, needles)
+    needles.any? { |needle| equals_or_includes?(haystack, needle) }
+  end
+
   def first_of_value(value)
     value.is_a?(Array) ? value.first : value
   end
@@ -44,7 +48,7 @@ module JsonLdHelper
   end
 
   def canonicalize(json)
-    graph = RDF::Graph.new << JSON::LD::API.toRdf(json)
+    graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context))
     graph.dump(:normalize)
   end
 
@@ -86,4 +90,19 @@ module JsonLdHelper
     request.add_headers('Accept' => 'application/activity+json, application/ld+json')
     request
   end
+
+  def load_jsonld_context(url, _options = {}, &_block)
+    json = Rails.cache.fetch("jsonld:context:#{url}", expires_in: 30.days, raw: true) do
+      request = Request.new(:get, url)
+      request.add_headers('Accept' => 'application/ld+json')
+
+      request.perform do |res|
+        raise JSON::LD::JsonLdError::LoadingDocumentFailed unless res.code == 200 && res.mime_type == 'application/ld+json'
+        res.body_with_limit
+      end
+    end
+
+    doc = JSON::LD::API::RemoteDocument.new(url, json)
+    block_given? ? yield(doc) : doc
+  end
 end
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index a2f5917f9..f78e5fbc3 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -7,12 +7,14 @@ module SettingsHelper
     bg: 'Български',
     ca: 'Català',
     de: 'Deutsch',
+    el: 'Ελληνικά',
     eo: 'Esperanto',
     es: 'Español',
+    eu: 'Euskara',
     fa: 'فارسی',
-    gl: 'Galego',
     fi: 'Suomi',
     fr: 'Français',
+    gl: 'Galego',
     he: 'עברית',
     hr: 'Hrvatski',
     hu: 'Magyar',
@@ -33,6 +35,7 @@ module SettingsHelper
     sr: 'Српски',
     'sr-Latn': 'Srpski (latinica)',
     sv: 'Svenska',
+    te: 'తెలుగు',
     th: 'ภาษาไทย',
     tr: 'Türkçe',
     uk: 'Українська',
diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb
index 3992432db..c6f12ecd4 100644
--- a/app/helpers/stream_entries_helper.rb
+++ b/app/helpers/stream_entries_helper.rb
@@ -12,17 +12,17 @@ module StreamEntriesHelper
     prepend_str = [
       [
         number_to_human(account.statuses_count, strip_insignificant_zeros: true),
-        t('accounts.posts'),
+        I18n.t('accounts.posts'),
       ].join(' '),
 
       [
         number_to_human(account.following_count, strip_insignificant_zeros: true),
-        t('accounts.following'),
+        I18n.t('accounts.following'),
       ].join(' '),
 
       [
         number_to_human(account.followers_count, strip_insignificant_zeros: true),
-        t('accounts.followers'),
+        I18n.t('accounts.followers'),
       ].join(' '),
     ].join(', ')
 
@@ -40,16 +40,16 @@ module StreamEntriesHelper
       end
     end
 
-    text = attachments.to_a.reject { |_, value| value.zero? }.map { |key, value| t("statuses.attached.#{key}", count: value) }.join(' · ')
+    text = attachments.to_a.reject { |_, value| value.zero? }.map { |key, value| I18n.t("statuses.attached.#{key}", count: value) }.join(' · ')
 
     return if text.blank?
 
-    t('statuses.attached.description', attached: text)
+    I18n.t('statuses.attached.description', attached: text)
   end
 
   def status_text_summary(status)
     return if status.spoiler_text.blank?
-    t('statuses.content_warning', warning: status.spoiler_text)
+    I18n.t('statuses.content_warning', warning: status.spoiler_text)
   end
 
   def status_description(status)
@@ -113,6 +113,19 @@ module StreamEntriesHelper
     end
   end
 
+  def fa_visibility_icon(status)
+    case status.visibility
+    when 'public'
+      fa_icon 'globe fw'
+    when 'unlisted'
+      fa_icon 'unlock-alt fw'
+    when 'private'
+      fa_icon 'lock fw'
+    when 'direct'
+      fa_icon 'envelope fw'
+    end
+  end
+
   private
 
   def simplified_text(text)
diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js
index b4125e84e..28f27fbc6 100644
--- a/app/javascript/core/admin.js
+++ b/app/javascript/core/admin.js
@@ -26,6 +26,7 @@ delegate(document, batchCheckboxClassName, 'change', () => {
   const checkAllElement = document.querySelector('#batch_checkbox_all');
   if (checkAllElement) {
     checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
+    checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
   }
 });
 
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index eee9c6928..fe3e831d5 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -4,6 +4,7 @@ import { throttle } from 'lodash';
 import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
 import { tagHistory } from '../settings';
 import { useEmoji } from './emojis';
+import resizeImage from '../utils/resize_image';
 import { importFetchedAccounts } from './importer';
 import { updateTimeline } from './timelines';
 import { showAlertForError } from './alerts';
@@ -182,18 +183,14 @@ export function uploadCompose(files) {
 
     dispatch(uploadComposeRequest());
 
-    let data = new FormData();
-    data.append('file', files[0]);
+    resizeImage(files[0]).then(file => {
+      const data = new FormData();
+      data.append('file', file);
 
-    api(getState).post('/api/v1/media', data, {
-      onUploadProgress: function (e) {
-        dispatch(uploadComposeProgress(e.loaded, e.total));
-      },
-    }).then(function (response) {
-      dispatch(uploadComposeSuccess(response.data));
-    }).catch(function (error) {
-      dispatch(uploadComposeFail(error));
-    });
+      return api(getState).post('/api/v1/media', data, {
+        onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)),
+      }).then(({ data }) => dispatch(uploadComposeSuccess(data)));
+    }).catch(error => dispatch(uploadComposeFail(error)));
   };
 };
 
diff --git a/app/javascript/mastodon/actions/push_notifications/registerer.js b/app/javascript/mastodon/actions/push_notifications/registerer.js
index 60b215f02..82fe4519a 100644
--- a/app/javascript/mastodon/actions/push_notifications/registerer.js
+++ b/app/javascript/mastodon/actions/push_notifications/registerer.js
@@ -1,4 +1,5 @@
 import api from '../../api';
+import { decode as decodeBase64 } from '../../utils/base64';
 import { pushNotificationsSetting } from '../../settings';
 import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
 import { me } from '../../initial_state';
@@ -10,13 +11,7 @@ const urlBase64ToUint8Array = (base64String) => {
     .replace(/\-/g, '+')
     .replace(/_/g, '/');
 
-  const rawData = window.atob(base64);
-  const outputArray = new Uint8Array(rawData.length);
-
-  for (let i = 0; i < rawData.length; ++i) {
-    outputArray[i] = rawData.charCodeAt(i);
-  }
-  return outputArray;
+  return decodeBase64(base64);
 };
 
 const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
diff --git a/app/javascript/mastodon/base_polyfills.js b/app/javascript/mastodon/base_polyfills.js
index 8fbb17785..997813a04 100644
--- a/app/javascript/mastodon/base_polyfills.js
+++ b/app/javascript/mastodon/base_polyfills.js
@@ -5,6 +5,7 @@ import includes from 'array-includes';
 import assign from 'object-assign';
 import values from 'object.values';
 import isNaN from 'is-nan';
+import { decode as decodeBase64 } from './utils/base64';
 
 if (!Array.prototype.includes) {
   includes.shim();
@@ -21,3 +22,23 @@ if (!Object.values) {
 if (!Number.isNaN) {
   Number.isNaN = isNaN;
 }
+
+if (!HTMLCanvasElement.prototype.toBlob) {
+  const BASE64_MARKER = ';base64,';
+
+  Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
+    value(callback, type = 'image/png', quality) {
+      const dataURL = this.toDataURL(type, quality);
+      let data;
+
+      if (dataURL.indexOf(BASE64_MARKER) >= 0) {
+        const [, base64] = dataURL.split(BASE64_MARKER);
+        data = decodeBase64(base64);
+      } else {
+        [, data] = dataURL.split(',');
+      }
+
+      callback(new Blob([data], { type }));
+    },
+  });
+}
diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js
index 34904194f..a4f5cf50c 100644
--- a/app/javascript/mastodon/components/autosuggest_textarea.js
+++ b/app/javascript/mastodon/components/autosuggest_textarea.js
@@ -84,9 +84,17 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
       return;
     }
 
+    if (e.which === 229 || e.isComposing) {
+      // Ignore key events during text composition
+      // e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac)
+      return;
+    }
+
     switch(e.key) {
     case 'Escape':
-      if (!suggestionsHidden) {
+      if (suggestions.size === 0 || suggestionsHidden) {
+        document.querySelector('.ui').parentElement.focus();
+      } else {
         e.preventDefault();
         this.setState({ suggestionsHidden: true });
       }
@@ -125,16 +133,6 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
     this.props.onKeyDown(e);
   }
 
-  onKeyUp = e => {
-    if (e.key === 'Escape' && this.state.suggestionsHidden) {
-      document.querySelector('.ui').parentElement.focus();
-    }
-
-    if (this.props.onKeyUp) {
-      this.props.onKeyUp(e);
-    }
-  }
-
   onBlur = () => {
     this.setState({ suggestionsHidden: true });
   }
@@ -186,7 +184,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
   }
 
   render () {
-    const { value, suggestions, disabled, placeholder, autoFocus } = this.props;
+    const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus } = this.props;
     const { suggestionsHidden } = this.state;
     const style = { direction: 'ltr' };
 
@@ -208,7 +206,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
             value={value}
             onChange={this.onChange}
             onKeyDown={this.onKeyDown}
-            onKeyUp={this.onKeyUp}
+            onKeyUp={onKeyUp}
             onBlur={this.onBlur}
             onPaste={this.onPaste}
             style={style}
diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js
index c5c6f73b3..982d34718 100644
--- a/app/javascript/mastodon/components/dropdown_menu.js
+++ b/app/javascript/mastodon/components/dropdown_menu.js
@@ -63,7 +63,7 @@ class DropdownMenu extends React.PureComponent {
 
     if (typeof action === 'function') {
       e.preventDefault();
-      action();
+      action(e);
     } else if (to) {
       e.preventDefault();
       this.context.router.history.push(to);
diff --git a/app/javascript/mastodon/components/relative_timestamp.js b/app/javascript/mastodon/components/relative_timestamp.js
index 51588e78c..3c8db7092 100644
--- a/app/javascript/mastodon/components/relative_timestamp.js
+++ b/app/javascript/mastodon/components/relative_timestamp.js
@@ -20,7 +20,7 @@ const dateFormatOptions = {
 };
 
 const shortDateFormatOptions = {
-  month: 'numeric',
+  month: 'short',
   day: 'numeric',
 };
 
@@ -66,12 +66,17 @@ export default class RelativeTimestamp extends React.Component {
   static propTypes = {
     intl: PropTypes.object.isRequired,
     timestamp: PropTypes.string.isRequired,
+    year: PropTypes.number.isRequired,
   };
 
   state = {
     now: this.props.intl.now(),
   };
 
+  static defaultProps = {
+    year: (new Date()).getFullYear(),
+  };
+
   shouldComponentUpdate (nextProps, nextState) {
     // As of right now the locale doesn't change without a new page load,
     // but we might as well check in case that ever changes.
@@ -114,7 +119,7 @@ export default class RelativeTimestamp extends React.Component {
   }
 
   render () {
-    const { timestamp, intl } = this.props;
+    const { timestamp, intl, year } = this.props;
 
     const date  = new Date(timestamp);
     const delta = this.state.now - date.getTime();
@@ -123,7 +128,7 @@ export default class RelativeTimestamp extends React.Component {
 
     if (delta < 10 * SECOND) {
       relativeTime = intl.formatMessage(messages.just_now);
-    } else if (delta < 3 * DAY) {
+    } else if (delta < 7 * DAY) {
       if (delta < MINUTE) {
         relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) });
       } else if (delta < HOUR) {
@@ -133,8 +138,10 @@ export default class RelativeTimestamp extends React.Component {
       } else {
         relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) });
       }
-    } else {
+    } else if (date.getFullYear() === year) {
       relativeTime = intl.formatDate(date, shortDateFormatOptions);
+    } else {
+      relativeTime = intl.formatDate(date, { ...shortDateFormatOptions, year: 'numeric' });
     }
 
     return (
diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js
index fd6858d05..f8a7f91d2 100644
--- a/app/javascript/mastodon/components/scrollable_list.js
+++ b/app/javascript/mastodon/components/scrollable_list.js
@@ -35,6 +35,7 @@ export default class ScrollableList extends PureComponent {
 
   state = {
     fullscreen: null,
+    mouseOver: false,
   };
 
   intersectionObserverWrapper = new IntersectionObserverWrapper();
@@ -71,7 +72,7 @@ export default class ScrollableList extends PureComponent {
     const someItemInserted = React.Children.count(prevProps.children) > 0 &&
       React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
       this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
-    if (someItemInserted && this.node.scrollTop > 0) {
+    if (someItemInserted && this.node.scrollTop > 0 || this.state.mouseOver) {
       return this.node.scrollHeight - this.node.scrollTop;
     } else {
       return null;
@@ -139,6 +140,14 @@ export default class ScrollableList extends PureComponent {
     this.props.onLoadMore();
   }
 
+  handleMouseEnter = () => {
+    this.setState({ mouseOver: true });
+  }
+
+  handleMouseLeave = () => {
+    this.setState({ mouseOver: false });
+  }
+
   render () {
     const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage, onLoadMore } = this.props;
     const { fullscreen } = this.state;
@@ -149,7 +158,7 @@ export default class ScrollableList extends PureComponent {
 
     if (isLoading || childrenCount > 0 || !emptyMessage) {
       scrollableArea = (
-        <div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
+        <div className={classNames('scrollable', { fullscreen })} ref={this.setRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
           <div role='feed' className='item-list'>
             {prepend}
 
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index e5f7c9399..402d558c4 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -114,12 +114,12 @@ export default class Status extends ImmutablePureComponent {
     this.context.router.history.push(`/accounts/${this._properStatus().getIn(['account', 'id'])}`);
   }
 
-  handleHotkeyMoveUp = () => {
-    this.props.onMoveUp(this.props.status.get('id'));
+  handleHotkeyMoveUp = e => {
+    this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured'));
   }
 
-  handleHotkeyMoveDown = () => {
-    this.props.onMoveDown(this.props.status.get('id'));
+  handleHotkeyMoveDown = e => {
+    this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured'));
   }
 
   handleHotkeyToggleHidden = () => {
@@ -233,7 +233,7 @@ export default class Status extends ImmutablePureComponent {
 
     return (
       <HotKeys handlers={handlers}>
-        <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0}>
+        <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null}>
           {prepend}
 
           <div className={classNames('status', `status-${status.get('visibility')}`, { muted: this.props.muted })} data-id={status.get('id')}>
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
index e58625582..d605dbc8a 100644
--- a/app/javascript/mastodon/components/status_action_bar.js
+++ b/app/javascript/mastodon/components/status_action_bar.js
@@ -153,7 +153,9 @@ export default class StatusActionBar extends ImmutablePureComponent {
       if (publicStatus) {
         menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
       } else {
-        menu.push({ text: intl.formatMessage(status.get('reblog') ? messages.reblog_private : messages.cancel_reblog_private), action: this.handleReblogClick });
+        if (status.get('visibility') === 'private') {
+          menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick });
+        }
       }
 
       menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js
index c98d4564e..0c971ceb0 100644
--- a/app/javascript/mastodon/components/status_list.js
+++ b/app/javascript/mastodon/components/status_list.js
@@ -30,13 +30,25 @@ export default class StatusList extends ImmutablePureComponent {
     trackScroll: true,
   };
 
-  handleMoveUp = id => {
-    const elementIndex = this.props.statusIds.indexOf(id) - 1;
+  getFeaturedStatusCount = () => {
+    return this.props.featuredStatusIds ? this.props.featuredStatusIds.size : 0;
+  }
+
+  getCurrentStatusIndex = (id, featured) => {
+    if (featured) {
+      return this.props.featuredStatusIds.indexOf(id);
+    } else {
+      return this.props.statusIds.indexOf(id) + this.getFeaturedStatusCount();
+    }
+  }
+
+  handleMoveUp = (id, featured) => {
+    const elementIndex = this.getCurrentStatusIndex(id, featured) - 1;
     this._selectChild(elementIndex);
   }
 
-  handleMoveDown = id => {
-    const elementIndex = this.props.statusIds.indexOf(id) + 1;
+  handleMoveDown = (id, featured) => {
+    const elementIndex = this.getCurrentStatusIndex(id, featured) + 1;
     this._selectChild(elementIndex);
   }
 
diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
index dc8fc02ba..84665a7e8 100644
--- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
@@ -162,12 +162,12 @@ class EmojiPickerMenu extends React.PureComponent {
   static defaultProps = {
     style: {},
     loading: true,
-    placement: 'bottom',
     frequentlyUsedEmojis: [],
   };
 
   state = {
     modifierOpen: false,
+    placement: null,
   };
 
   handleDocumentClick = e => {
@@ -298,7 +298,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
     this.dropdown = c;
   }
 
-  onShowDropdown = () => {
+  onShowDropdown = ({ target }) => {
     this.setState({ active: true });
 
     if (!EmojiPicker) {
@@ -313,6 +313,9 @@ export default class EmojiPickerDropdown extends React.PureComponent {
         this.setState({ loading: false });
       });
     }
+
+    const { top } = target.getBoundingClientRect();
+    this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
   }
 
   onHideDropdown = () => {
@@ -324,7 +327,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
       if (this.state.active) {
         this.onHideDropdown();
       } else {
-        this.onShowDropdown();
+        this.onShowDropdown(e);
       }
     }
   }
@@ -346,7 +349,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
   render () {
     const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
     const title = intl.formatMessage(messages.emoji);
-    const { active, loading } = this.state;
+    const { active, loading, placement } = this.state;
 
     return (
       <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
@@ -358,7 +361,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
           />
         </div>
 
-        <Overlay show={active} placement='bottom' target={this.findTarget}>
+        <Overlay show={active} placement={placement} target={this.findTarget}>
           <EmojiPickerMenu
             custom_emojis={this.props.custom_emojis}
             loading={loading}
diff --git a/app/javascript/mastodon/features/compose/components/reply_indicator.js b/app/javascript/mastodon/features/compose/components/reply_indicator.js
index d8cda96f3..5b4b81eac 100644
--- a/app/javascript/mastodon/features/compose/components/reply_indicator.js
+++ b/app/javascript/mastodon/features/compose/components/reply_indicator.js
@@ -51,7 +51,7 @@ export default class ReplyIndicator extends ImmutablePureComponent {
     return (
       <div className='reply-indicator'>
         <div className='reply-indicator__header'>
-          <div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div>
+          <div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted /></div>
 
           <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name'>
             <div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div>
diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js
index fc34c8cdc..bb9b75505 100644
--- a/app/javascript/mastodon/features/status/components/action_bar.js
+++ b/app/javascript/mastodon/features/status/components/action_bar.js
@@ -123,7 +123,9 @@ export default class ActionBar extends React.PureComponent {
       if (publicStatus) {
         menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
       } else {
-        menu.push({ text: intl.formatMessage(status.get('reblog') ? messages.reblog_private : messages.cancel_reblog_private), action: this.handleReblogClick });
+        if (status.get('visibility') === 'private') {
+          menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick });
+        }
       }
 
       menu.push(null);
diff --git a/app/javascript/mastodon/load_polyfills.js b/app/javascript/mastodon/load_polyfills.js
index 815e1905b..8cb81c1a6 100644
--- a/app/javascript/mastodon/load_polyfills.js
+++ b/app/javascript/mastodon/load_polyfills.js
@@ -12,12 +12,13 @@ function importExtraPolyfills() {
 
 function loadPolyfills() {
   const needsBasePolyfills = !(
+    Array.prototype.includes &&
+    HTMLCanvasElement.prototype.toBlob &&
     window.Intl &&
+    Number.isNaN &&
     Object.assign &&
     Object.values &&
-    Number.isNaN &&
-    window.Symbol &&
-    Array.prototype.includes
+    window.Symbol
   );
 
   // Latest version of Firefox and Safari do not have IntersectionObserver.
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 24c8a5b54..947348f70 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -2,7 +2,7 @@
   "account.block": "حظر @{name}",
   "account.block_domain": "إخفاء كل شيئ قادم من إسم النطاق {domain}",
   "account.blocked": "محظور",
-  "account.direct": "Direct message @{name}",
+  "account.direct": "رسالة خاصة إلى @{name}",
   "account.disclaimer_full": "قد لا تعكس المعلومات أدناه الملف الشخصي الكامل للمستخدم.",
   "account.domain_blocked": "النطاق مخفي",
   "account.edit_profile": "تعديل الملف الشخصي",
@@ -18,7 +18,7 @@
   "account.mute_notifications": "كتم إخطارات @{name}",
   "account.muted": "مكتوم",
   "account.posts": "التبويقات",
-  "account.posts_with_replies": "تبويقات تحتوي على رُدود",
+  "account.posts_with_replies": "التبويقات و الردود",
   "account.report": "أبلغ عن @{name}",
   "account.requested": "في انتظار الموافقة",
   "account.share": "مشاركة @{name}'s profile",
@@ -29,8 +29,8 @@
   "account.unmute": "إلغاء الكتم عن @{name}",
   "account.unmute_notifications": "إلغاء كتم إخطارات @{name}",
   "account.view_full_profile": "عرض الملف الشخصي كاملا",
-  "alert.unexpected.message": "An unexpected error occurred.",
-  "alert.unexpected.title": "Oops!",
+  "alert.unexpected.message": "لقد طرأ هناك خطأ غير متوقّع.",
+  "alert.unexpected.title": "المعذرة !",
   "boost_modal.combo": "يمكنك ضغط {combo} لتخطّي هذه في المرّة القادمة",
   "bundle_column_error.body": "لقد وقع هناك خطأ أثناء عملية تحميل هذا العنصر.",
   "bundle_column_error.retry": "إعادة المحاولة",
@@ -40,8 +40,8 @@
   "bundle_modal_error.retry": "إعادة المحاولة",
   "column.blocks": "الحسابات المحجوبة",
   "column.community": "الخيط العام المحلي",
-  "column.direct": "Direct messages",
-  "column.domain_blocks": "Hidden domains",
+  "column.direct": "الرسائل المباشرة",
+  "column.domain_blocks": "النطاقات المخفية",
   "column.favourites": "المفضلة",
   "column.follow_requests": "طلبات المتابعة",
   "column.home": "الرئيسية",
@@ -59,7 +59,7 @@
   "column_header.unpin": "فك التدبيس",
   "column_subheading.navigation": "التصفح",
   "column_subheading.settings": "الإعدادات",
-  "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning": "لن يَظهر هذا التبويق إلا للمستخدمين المذكورين.",
   "compose_form.hashtag_warning": "هذا التبويق لن يُدرَج تحت أي وسم كان بما أنه غير مُدرَج. لا يُسمح بالبحث إلّا عن التبويقات العمومية عن طريق الوسوم.",
   "compose_form.lock_disclaimer": "حسابك ليس {locked}. يمكن لأي شخص متابعتك و عرض المنشورات.",
   "compose_form.lock_disclaimer.lock": "مقفل",
@@ -101,7 +101,7 @@
   "emoji_button.symbols": "رموز",
   "emoji_button.travel": "أماكن و أسفار",
   "empty_column.community": "الخط الزمني المحلي فارغ. أكتب شيئا ما للعامة كبداية !",
-  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+  "empty_column.direct": "لم تتلق أية رسالة خاصة مباشِرة بعد. سوف يتم عرض الرسائل المباشرة هنا إن قمت بإرسال واحدة أو تلقيت البعض منها.",
   "empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.",
   "empty_column.home": "إنك لا تتبع بعد أي شخص إلى حد الآن. زر {public} أو استخدام حقل البحث لكي تبدأ على التعرف على مستخدمين آخرين.",
   "empty_column.home.public_timeline": "الخيط العام",
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "لذِكر الناشر",
   "keyboard_shortcuts.reply": "للردّ",
   "keyboard_shortcuts.search": "للتركيز على البحث",
+  "keyboard_shortcuts.toggle_hidden": "لعرض أو إخفاء النص مِن وراء التحذير",
   "keyboard_shortcuts.toot": "لتحرير تبويق جديد",
   "keyboard_shortcuts.unfocus": "لإلغاء التركيز على حقل النص أو نافذة البحث",
   "keyboard_shortcuts.up": "للإنتقال إلى أعلى القائمة",
@@ -156,8 +157,8 @@
   "mute_modal.hide_notifications": "هل تود إخفاء الإخطارات القادمة من هذا المستخدم ؟",
   "navigation_bar.blocks": "الحسابات المحجوبة",
   "navigation_bar.community_timeline": "الخيط العام المحلي",
-  "navigation_bar.direct": "Direct messages",
-  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.direct": "الرسائل المباشِرة",
+  "navigation_bar.domain_blocks": "النطاقات المخفية",
   "navigation_bar.edit_profile": "تعديل الملف الشخصي",
   "navigation_bar.favourites": "المفضلة",
   "navigation_bar.follow_requests": "طلبات المتابعة",
@@ -241,10 +242,10 @@
   "search_results.total": "{count, number} {count, plural, one {result} و {results}}",
   "standalone.public_title": "نظرة على ...",
   "status.block": "Block @{name}",
-  "status.cancel_reblog_private": "Unboost",
+  "status.cancel_reblog_private": "إلغاء الترقية",
   "status.cannot_reblog": "تعذرت ترقية هذا المنشور",
   "status.delete": "إحذف",
-  "status.direct": "Direct message @{name}",
+  "status.direct": "رسالة خاصة إلى @{name}",
   "status.embed": "إدماج",
   "status.favourite": "أضف إلى المفضلة",
   "status.load_more": "حمّل المزيد",
@@ -275,7 +276,7 @@
   "tabs_bar.home": "الرئيسية",
   "tabs_bar.local_timeline": "المحلي",
   "tabs_bar.notifications": "الإخطارات",
-  "tabs_bar.search": "Search",
+  "tabs_bar.search": "البحث",
   "ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.",
   "upload_area.title": "إسحب ثم أفلت للرفع",
   "upload_button.label": "إضافة وسائط",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index 25ef6db65..971475114 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "to mention author",
   "keyboard_shortcuts.reply": "to reply",
   "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "to start a brand new toot",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 6a44808e0..f2e3699d5 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -2,7 +2,7 @@
   "account.block": "Bloca @{name}",
   "account.block_domain": "Amaga-ho tot de {domain}",
   "account.blocked": "Bloquejat",
-  "account.direct": "Direct message @{name}",
+  "account.direct": "Missatge directe @{name}",
   "account.disclaimer_full": "La informació següent pot reflectir incompleta el perfil de l'usuari.",
   "account.domain_blocked": "Domini ocult",
   "account.edit_profile": "Edita el perfil",
@@ -18,7 +18,7 @@
   "account.mute_notifications": "Notificacions desactivades de @{name}",
   "account.muted": "Silenciat",
   "account.posts": "Toots",
-  "account.posts_with_replies": "Toots amb respostes",
+  "account.posts_with_replies": "Toots i respostes",
   "account.report": "Informe @{name}",
   "account.requested": "Esperant aprovació. Clic per a cancel·lar la petició de seguiment",
   "account.share": "Comparteix el perfil de @{name}",
@@ -29,8 +29,8 @@
   "account.unmute": "Treure silenci de @{name}",
   "account.unmute_notifications": "Activar notificacions de @{name}",
   "account.view_full_profile": "Mostra el perfil complet",
-  "alert.unexpected.message": "An unexpected error occurred.",
-  "alert.unexpected.title": "Oops!",
+  "alert.unexpected.message": "S'ha produït un error inesperat.",
+  "alert.unexpected.title": "Vaja!",
   "boost_modal.combo": "Pots premer {combo} per saltar-te això el proper cop",
   "bundle_column_error.body": "S'ha produït un error en carregar aquest component.",
   "bundle_column_error.retry": "Torna-ho a provar",
@@ -40,8 +40,8 @@
   "bundle_modal_error.retry": "Torna-ho a provar",
   "column.blocks": "Usuaris blocats",
   "column.community": "Línia de temps local",
-  "column.direct": "Direct messages",
-  "column.domain_blocks": "Hidden domains",
+  "column.direct": "Missatges directes",
+  "column.domain_blocks": "Dominis ocults",
   "column.favourites": "Favorits",
   "column.follow_requests": "Peticions per seguir-te",
   "column.home": "Inici",
@@ -59,7 +59,7 @@
   "column_header.unpin": "No fixis",
   "column_subheading.navigation": "Navegació",
   "column_subheading.settings": "Configuració",
-  "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning": "Aquest toot només serà visible per a tots els usuaris esmentats.",
   "compose_form.hashtag_warning": "Aquest toot no es mostrarà en cap etiqueta ja que no està llistat. Només els toots públics poden ser cercats per etiqueta.",
   "compose_form.lock_disclaimer": "El teu compte no està bloquejat {locked}. Tothom pot seguir-te i veure els teus missatges a seguidors.",
   "compose_form.lock_disclaimer.lock": "blocat",
@@ -68,7 +68,7 @@
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive.marked": "Mèdia marcat com a sensible",
   "compose_form.sensitive.unmarked": "Mèdia no està marcat com a sensible",
-  "compose_form.spoiler.marked": "Text ocult sota l'avís",
+  "compose_form.spoiler.marked": "Text es ocult sota l'avís",
   "compose_form.spoiler.unmarked": "Text no ocult",
   "compose_form.spoiler_placeholder": "Escriu l'avís aquí",
   "confirmation_modal.cancel": "Cancel·la",
@@ -101,7 +101,7 @@
   "emoji_button.symbols": "Símbols",
   "emoji_button.travel": "Viatges i Llocs",
   "empty_column.community": "La línia de temps local és buida. Escriu alguna cosa públicament per fer rodar la pilota!",
-  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+  "empty_column.direct": "Encara no tens missatges directes. Quan enviïs o rebis un, es mostrarà aquí.",
   "empty_column.hashtag": "Encara no hi ha res amb aquesta etiqueta.",
   "empty_column.home": "Encara no segueixes ningú. Visita {public} o fes cerca per començar i conèixer altres usuaris.",
   "empty_column.home.public_timeline": "la línia de temps pública",
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "per esmentar l'autor",
   "keyboard_shortcuts.reply": "respondre",
   "keyboard_shortcuts.search": "per centrar la cerca",
+  "keyboard_shortcuts.toggle_hidden": "per a mostrar/amagar text sota CW",
   "keyboard_shortcuts.toot": "per a començar un toot nou de trinca",
   "keyboard_shortcuts.unfocus": "descentrar l'area de composició de text/cerca",
   "keyboard_shortcuts.up": "moure amunt en la llista",
@@ -156,8 +157,8 @@
   "mute_modal.hide_notifications": "Amagar notificacions d'aquest usuari?",
   "navigation_bar.blocks": "Usuaris bloquejats",
   "navigation_bar.community_timeline": "Línia de temps Local",
-  "navigation_bar.direct": "Direct messages",
-  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.direct": "Missatges directes",
+  "navigation_bar.domain_blocks": "Dominis ocults",
   "navigation_bar.edit_profile": "Editar perfil",
   "navigation_bar.favourites": "Favorits",
   "navigation_bar.follow_requests": "Sol·licituds de seguiment",
@@ -241,10 +242,10 @@
   "search_results.total": "{count, number} {count, plural, un {result} altres {results}}",
   "standalone.public_title": "Una mirada a l'interior ...",
   "status.block": "Block @{name}",
-  "status.cancel_reblog_private": "Unboost",
+  "status.cancel_reblog_private": "Desfer l'impuls",
   "status.cannot_reblog": "Aquesta publicació no pot ser retootejada",
   "status.delete": "Esborrar",
-  "status.direct": "Direct message @{name}",
+  "status.direct": "Missatge directe @{name}",
   "status.embed": "Incrustar",
   "status.favourite": "Favorit",
   "status.load_more": "Carrega més",
@@ -257,7 +258,7 @@
   "status.pin": "Fixat en el perfil",
   "status.pinned": "Toot fixat",
   "status.reblog": "Impuls",
-  "status.reblog_private": "Boost to original audience",
+  "status.reblog_private": "Impulsar a l'audiència original",
   "status.reblogged_by": "{name} ha retootejat",
   "status.reply": "Respondre",
   "status.replyAll": "Respondre al tema",
@@ -275,7 +276,7 @@
   "tabs_bar.home": "Inici",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificacions",
-  "tabs_bar.search": "Search",
+  "tabs_bar.search": "Cerca",
   "ui.beforeunload": "El vostre esborrany es perdrà si sortiu de Mastodon.",
   "upload_area.title": "Arrossega i deixa anar per carregar",
   "upload_button.label": "Afegir multimèdia",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 69c2ae8d8..f442e0675 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -18,7 +18,7 @@
   "account.mute_notifications": "Benachrichtigungen von @{name} verbergen",
   "account.muted": "Stummgeschaltet",
   "account.posts": "Beiträge",
-  "account.posts_with_replies": "Beiträge mit Antworten",
+  "account.posts_with_replies": "Beiträge und Antworten",
   "account.report": "@{name} melden",
   "account.requested": "Warte auf Erlaubnis. Klicke zum Abbrechen",
   "account.share": "Profil von @{name} teilen",
@@ -29,8 +29,8 @@
   "account.unmute": "@{name} nicht mehr stummschalten",
   "account.unmute_notifications": "Benachrichtigungen von @{name} einschalten",
   "account.view_full_profile": "Vollständiges Profil anzeigen",
-  "alert.unexpected.message": "An unexpected error occurred.",
-  "alert.unexpected.title": "Oops!",
+  "alert.unexpected.message": "Ein unerwarteter Fehler ist aufgetreten.",
+  "alert.unexpected.title": "Hoppla!",
   "boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen",
   "bundle_column_error.body": "Etwas ist beim Laden schiefgelaufen.",
   "bundle_column_error.retry": "Erneut versuchen",
@@ -40,8 +40,8 @@
   "bundle_modal_error.retry": "Erneut versuchen",
   "column.blocks": "Blockierte Profile",
   "column.community": "Lokale Zeitleiste",
-  "column.direct": "Direct messages",
-  "column.domain_blocks": "Hidden domains",
+  "column.direct": "Direktnachrichten",
+  "column.domain_blocks": "Versteckte Domains",
   "column.favourites": "Favoriten",
   "column.follow_requests": "Folgeanfragen",
   "column.home": "Startseite",
@@ -59,17 +59,17 @@
   "column_header.unpin": "Lösen",
   "column_subheading.navigation": "Navigation",
   "column_subheading.settings": "Einstellungen",
-  "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning": "Dieser Beitrag wird nur für die erwähnten Nutzer sichtbar sein.",
   "compose_form.hashtag_warning": "Dieser Beitrag wird nicht unter einen dieser Hashtags sichtbar sein, solange er ungelistet ist. Bei einer Suche kann er nicht gefunden werden.",
   "compose_form.lock_disclaimer": "Dein Profil ist nicht {locked}. Wer dir folgen will, kann das jederzeit tun und dann auch deine privaten Beiträge sehen.",
   "compose_form.lock_disclaimer.lock": "gesperrt",
   "compose_form.placeholder": "Worüber möchtest du schreiben?",
   "compose_form.publish": "Tröt",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive.marked": "Media is marked as sensitive",
-  "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
-  "compose_form.spoiler.marked": "Text is hidden behind warning",
-  "compose_form.spoiler.unmarked": "Text is not hidden",
+  "compose_form.sensitive.marked": "Medien sind als heikel markiert",
+  "compose_form.sensitive.unmarked": "Medien sind nicht als heikel markiert",
+  "compose_form.spoiler.marked": "Text ist hinter einer Warnung versteckt",
+  "compose_form.spoiler.unmarked": "Text ist nicht versteckt",
   "compose_form.spoiler_placeholder": "Inhaltswarnung",
   "confirmation_modal.cancel": "Abbrechen",
   "confirmations.block.confirm": "Blockieren",
@@ -101,7 +101,7 @@
   "emoji_button.symbols": "Symbole",
   "emoji_button.travel": "Reisen und Orte",
   "empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe einen öffentlichen Beitrag, um den Ball ins Rollen zu bringen!",
-  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+  "empty_column.direct": "Du hast noch keine Direktnachrichten erhalten. Wenn du eine sendest oder empfängst, wird sie hier zu sehen sein.",
   "empty_column.hashtag": "Unter diesem Hashtag gibt es noch nichts.",
   "empty_column.home": "Deine Startseite ist leer! Besuche {public} oder nutze die Suche, um loszulegen und andere Leute zu finden.",
   "empty_column.home.public_timeline": "die öffentliche Zeitleiste",
@@ -130,11 +130,12 @@
   "keyboard_shortcuts.enter": "um den Status zu öffnen",
   "keyboard_shortcuts.favourite": "um zu favorisieren",
   "keyboard_shortcuts.heading": "Tastenkombinationen",
-  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.hotkey": "Tastenkürzel",
   "keyboard_shortcuts.legend": "um diese Übersicht anzuzeigen",
   "keyboard_shortcuts.mention": "um Autor_in zu erwähnen",
   "keyboard_shortcuts.reply": "um zu antworten",
   "keyboard_shortcuts.search": "um die Suche zu fokussieren",
+  "keyboard_shortcuts.toggle_hidden": "um den Text hinter einer Inhaltswarnung zu verstecken oder ihn anzuzeigen",
   "keyboard_shortcuts.toot": "um einen neuen Toot zu beginnen",
   "keyboard_shortcuts.unfocus": "um das Textfeld/die Suche nicht mehr zu fokussieren",
   "keyboard_shortcuts.up": "sich in der Liste hinauf bewegen",
@@ -156,8 +157,8 @@
   "mute_modal.hide_notifications": "Benachrichtigungen von diesem Account verbergen?",
   "navigation_bar.blocks": "Blockierte Profile",
   "navigation_bar.community_timeline": "Lokale Zeitleiste",
-  "navigation_bar.direct": "Direct messages",
-  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.direct": "Direktnachrichten",
+  "navigation_bar.domain_blocks": "Versteckte Domains",
   "navigation_bar.edit_profile": "Profil bearbeiten",
   "navigation_bar.favourites": "Favoriten",
   "navigation_bar.follow_requests": "Folgeanfragen",
@@ -190,8 +191,8 @@
   "onboarding.page_four.home": "Die Startseite zeigt dir Beiträge von Leuten, denen du folgst.",
   "onboarding.page_four.notifications": "Wenn jemand mit dir interagiert, bekommst du eine Mitteilung.",
   "onboarding.page_one.federation": "Mastodon ist ein soziales Netzwerk, das aus unabhängigen Servern besteht. Diese Server nennen wir auch Instanzen.",
-  "onboarding.page_one.full_handle": "Your full handle",
-  "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.",
+  "onboarding.page_one.full_handle": "Dein vollständiger Benutzername",
+  "onboarding.page_one.handle_hint": "Das ist das, was du deinen Freunden sagst, um nach dir zu suchen.",
   "onboarding.page_one.welcome": "Willkommen bei Mastodon!",
   "onboarding.page_six.admin": "Für deine Instanz ist {admin} zuständig.",
   "onboarding.page_six.almost_done": "Fast fertig …",
@@ -214,50 +215,50 @@
   "privacy.public.short": "Öffentlich",
   "privacy.unlisted.long": "Nicht in öffentlichen Zeitleisten anzeigen",
   "privacy.unlisted.short": "Nicht gelistet",
-  "regeneration_indicator.label": "Loading…",
-  "regeneration_indicator.sublabel": "Your home feed is being prepared!",
+  "regeneration_indicator.label": "Laden…",
+  "regeneration_indicator.sublabel": "Deine Heimzeitleiste wird gerade vorbereitet!",
   "relative_time.days": "{number}d",
   "relative_time.hours": "{number}h",
-  "relative_time.just_now": "now",
+  "relative_time.just_now": "jetzt",
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Abbrechen",
-  "report.forward": "Forward to {target}",
-  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
-  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.forward": "An {target} weiterleiten",
+  "report.forward_hint": "Dieses Konto ist von einem anderen Server. Soll eine anonymisierte Kopie des Berichts auch dorthin geschickt werden?",
+  "report.hint": "Der Bericht wird an die Moderatoren deiner Instanz geschickt. Du kannst hier eine Erklärung angeben, warum du dieses Konto meldest:",
   "report.placeholder": "Zusätzliche Kommentare",
   "report.submit": "Absenden",
   "report.target": "{target} melden",
   "search.placeholder": "Suche",
-  "search_popout.search_format": "Advanced search format",
-  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
-  "search_popout.tips.hashtag": "hashtag",
+  "search_popout.search_format": "Fortgeschrittenes Suchformat",
+  "search_popout.tips.full_text": "Simpler Text gibt Beiträge, die du geschrieben, favorisiert und geteilt hast zurück. Außerdem auch Beiträge in denen du erwähnt wurdest, als auch passende Nutzernamen, Anzeigenamen oder Hashtags.",
+  "search_popout.tips.hashtag": "Hashtag",
   "search_popout.tips.status": "status",
-  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
-  "search_popout.tips.user": "user",
-  "search_results.accounts": "People",
+  "search_popout.tips.text": "Einfacher Text gibt Anzeigenamen, Benutzernamen und Hashtags zurück",
+  "search_popout.tips.user": "Nutzer",
+  "search_results.accounts": "Personen",
   "search_results.hashtags": "Hashtags",
-  "search_results.statuses": "Toots",
+  "search_results.statuses": "Beiträge",
   "search_results.total": "{count, number} {count, plural, one {Ergebnis} other {Ergebnisse}}",
   "standalone.public_title": "Ein kleiner Einblick …",
   "status.block": "Block @{name}",
-  "status.cancel_reblog_private": "Unboost",
+  "status.cancel_reblog_private": "Nicht mehr teilen",
   "status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden",
   "status.delete": "Löschen",
-  "status.direct": "Direct message @{name}",
+  "status.direct": "Direktnachricht @{name}",
   "status.embed": "Einbetten",
   "status.favourite": "Favorisieren",
   "status.load_more": "Weitere laden",
   "status.media_hidden": "Medien versteckt",
   "status.mention": "@{name} erwähnen",
   "status.more": "Mehr",
-  "status.mute": "Mute @{name}",
+  "status.mute": "@{name} stummschalten",
   "status.mute_conversation": "Thread stummschalten",
   "status.open": "Diesen Beitrag öffnen",
   "status.pin": "Im Profil anheften",
-  "status.pinned": "Pinned toot",
+  "status.pinned": "Angehefteter Beitrag",
   "status.reblog": "Teilen",
-  "status.reblog_private": "Boost to original audience",
+  "status.reblog_private": "An das eigentliche Publikum teilen",
   "status.reblogged_by": "{name} teilte",
   "status.reply": "Antworten",
   "status.replyAll": "Auf Thread antworten",
@@ -266,21 +267,21 @@
   "status.sensitive_warning": "Heikle Inhalte",
   "status.share": "Teilen",
   "status.show_less": "Weniger anzeigen",
-  "status.show_less_all": "Show less for all",
+  "status.show_less_all": "Zeige weniger für alles",
   "status.show_more": "Mehr anzeigen",
-  "status.show_more_all": "Show more for all",
+  "status.show_more_all": "Zeige mehr für alles",
   "status.unmute_conversation": "Stummschaltung von Thread aufheben",
   "status.unpin": "Vom Profil lösen",
   "tabs_bar.federated_timeline": "Föderation",
   "tabs_bar.home": "Startseite",
   "tabs_bar.local_timeline": "Lokal",
   "tabs_bar.notifications": "Mitteilungen",
-  "tabs_bar.search": "Search",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "tabs_bar.search": "Suchen",
+  "ui.beforeunload": "Dein Entwurf geht verloren, wenn du Mastodon verlässt.",
   "upload_area.title": "Zum Hochladen hereinziehen",
   "upload_button.label": "Mediendatei hinzufügen",
   "upload_form.description": "Für Menschen mit Sehbehinderung beschreiben",
-  "upload_form.focus": "Crop",
+  "upload_form.focus": "Zuschneiden",
   "upload_form.undo": "Entfernen",
   "upload_progress.label": "Wird hochgeladen …",
   "video.close": "Video schließen",
diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json
new file mode 100644
index 000000000..a7e1c408f
--- /dev/null
+++ b/app/javascript/mastodon/locales/el.json
@@ -0,0 +1,296 @@
+{
+  "account.block": "Απόκλεισε τον/την @{name}",
+  "account.block_domain": "Απόκρυψε τα πάντα από τον/την",
+  "account.blocked": "Αποκλεισμένος/η",
+  "account.direct": "Απευθείας μήνυμα προς @{name}",
+  "account.disclaimer_full": "Οι παρακάτω πληροφορίες μπορει να μην αντανακλούν το προφίλ του χρήστη επαρκως.",
+  "account.domain_blocked": "Domain hidden",
+  "account.edit_profile": "Επεξεργάσου το προφίλ",
+  "account.follow": "Ακολούθησε",
+  "account.followers": "Ακόλουθοι",
+  "account.follows": "Ακολουθεί",
+  "account.follows_you": "Σε ακολουθεί",
+  "account.hide_reblogs": "Απόκρυψη προωθήσεων από τον/την @{name}",
+  "account.media": "Πολυμέσα",
+  "account.mention": "Ανέφερε τον/την @{name}",
+  "account.moved_to": "{name} μετακόμισε στο:",
+  "account.mute": "Σώπασε τον/την @{name}",
+  "account.mute_notifications": "Σώπασε τις ειδοποιήσεις από τον/την @{name}",
+  "account.muted": "Αποσιωπημένος/η",
+  "account.posts": "Τουτ",
+  "account.posts_with_replies": "Τουτ και απαντήσεις",
+  "account.report": "Ανέφερε τον/την @{name}",
+  "account.requested": "Εκκρεμεί έγκριση. Κάνε κλικ για να ακυρώσεις το αίτημα ακολούθησης",
+  "account.share": "Μοιράσου το προφίλ του/της @{name}",
+  "account.show_reblogs": "Δείξε τις προωθήσεις του/της @{name}",
+  "account.unblock": "Unblock @{name}",
+  "account.unblock_domain": "Αποκάλυψε το {domain}",
+  "account.unfollow": "Unfollow",
+  "account.unmute": "Unmute @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.view_full_profile": "Δες το πλήρες προφίλ",
+  "alert.unexpected.message": "Προέκυψε απροσδόκητο σφάλμα.",
+  "alert.unexpected.title": "Εεπ!",
+  "boost_modal.combo": "You can press {combo} to skip this next time",
+  "bundle_column_error.body": "Κάτι πήγε στραβά ενώ φορτωνόταν αυτό το στοιχείο.",
+  "bundle_column_error.retry": "Δοκίμασε ξανά",
+  "bundle_column_error.title": "Σφάλμα δικτύου",
+  "bundle_modal_error.close": "Κλείσε",
+  "bundle_modal_error.message": "Κάτι πήγε στραβά ενώ φορτωνόταν αυτό το στοιχείο.",
+  "bundle_modal_error.retry": "Δοκίμασε ξανά",
+  "column.blocks": "Αποκλεισμένοι χρήστες",
+  "column.community": "Τοπική ροή",
+  "column.direct": "Απευθείας μηνύματα",
+  "column.domain_blocks": "Hidden domains",
+  "column.favourites": "Αγαπημένα",
+  "column.follow_requests": "Αιτήματα παρακολούθησης",
+  "column.home": "Αρχική",
+  "column.lists": "Λίστες",
+  "column.mutes": "Αποσιωπημένοι χρήστες",
+  "column.notifications": "Ειδοποιήσεις",
+  "column.pins": "Καρφιτσωμένα τουτ",
+  "column.public": "Ομοσπονδιακή ροή",
+  "column_back_button.label": "Πίσω",
+  "column_header.hide_settings": "Απόκρυψη ρυθμίσεων",
+  "column_header.moveLeft_settings": "Μεταφορά κολώνας αριστερά",
+  "column_header.moveRight_settings": "Μεταφορά κολώνας δεξιά",
+  "column_header.pin": "Καρφίτσωμα",
+  "column_header.show_settings": "Εμφάνιση ρυθμίσεων",
+  "column_header.unpin": "Ξεκαρφίτσωμα",
+  "column_subheading.navigation": "Πλοήγηση",
+  "column_subheading.settings": "Ρυθμίσεις",
+  "compose_form.direct_message_warning": "Αυτό το τουτ θα εμφανίζεται μόνο σε όλους τους αναφερόμενους χρήστες.",
+  "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
+  "compose_form.lock_disclaimer": "Ο λογαριασμός σου δεν είναι {locked}. Οποιοσδήποτε μπορεί να σε ακολουθήσει για να δει τις δημοσιεύσεις σας προς τους ακολούθους σας.",
+  "compose_form.lock_disclaimer.lock": "κλειδωμένος",
+  "compose_form.placeholder": "Τι σκέφτεσαι;",
+  "compose_form.publish": "Τουτ",
+  "compose_form.publish_loud": "{publish}!",
+  "compose_form.sensitive.marked": "Το πολυμέσο έχει σημειωθεί ως ευαίσθητο",
+  "compose_form.sensitive.unmarked": "Το πολυμέσο δεν έχει σημειωθεί ως ευαίσθητο",
+  "compose_form.spoiler.marked": "Κείμενο κρυμμένο πίσω από προειδοποίηση",
+  "compose_form.spoiler.unmarked": "Κείμενο μη κρυμμένο",
+  "compose_form.spoiler_placeholder": "Γράψε την προειδοποίησή σου εδώ",
+  "confirmation_modal.cancel": "Άκυρο",
+  "confirmations.block.confirm": "Απόκλεισε",
+  "confirmations.block.message": "Σίγουρα θες να αποκλείσεις τον/την {name};",
+  "confirmations.delete.confirm": "Διέγραψε",
+  "confirmations.delete.message": "Σίγουρα θες να διαγράψεις αυτή την κατάσταση;",
+  "confirmations.delete_list.confirm": "Διέγραψε",
+  "confirmations.delete_list.message": "Σίγουρα θες να διαγράψεις οριστικά αυτή τη λίστα;",
+  "confirmations.domain_block.confirm": "Hide entire domain",
+  "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
+  "confirmations.mute.confirm": "Mute",
+  "confirmations.mute.message": "Are you sure you want to mute {name}?",
+  "confirmations.unfollow.confirm": "Unfollow",
+  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
+  "emoji_button.activity": "Activity",
+  "emoji_button.custom": "Custom",
+  "emoji_button.flags": "Flags",
+  "emoji_button.food": "Food & Drink",
+  "emoji_button.label": "Insert emoji",
+  "emoji_button.nature": "Nature",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.objects": "Objects",
+  "emoji_button.people": "People",
+  "emoji_button.recent": "Frequently used",
+  "emoji_button.search": "Search...",
+  "emoji_button.search_results": "Search results",
+  "emoji_button.symbols": "Symbols",
+  "emoji_button.travel": "Travel & Places",
+  "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
+  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+  "empty_column.hashtag": "There is nothing in this hashtag yet.",
+  "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
+  "empty_column.home.public_timeline": "the public timeline",
+  "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
+  "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
+  "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
+  "follow_request.authorize": "Authorize",
+  "follow_request.reject": "Reject",
+  "getting_started.appsshort": "Apps",
+  "getting_started.faq": "FAQ",
+  "getting_started.heading": "Getting started",
+  "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.",
+  "getting_started.userguide": "User Guide",
+  "home.column_settings.advanced": "Advanced",
+  "home.column_settings.basic": "Basic",
+  "home.column_settings.filter_regex": "Filter out by regular expressions",
+  "home.column_settings.show_reblogs": "Show boosts",
+  "home.column_settings.show_replies": "Show replies",
+  "home.settings": "Column settings",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
+  "lightbox.close": "Close",
+  "lightbox.next": "Next",
+  "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among people you follow",
+  "lists.subheading": "Your lists",
+  "loading_indicator.label": "Loading...",
+  "media_gallery.toggle_visible": "Toggle visibility",
+  "missing_indicator.label": "Not found",
+  "missing_indicator.sublabel": "This resource could not be found",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "navigation_bar.blocks": "Blocked users",
+  "navigation_bar.community_timeline": "Local timeline",
+  "navigation_bar.direct": "Direct messages",
+  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.edit_profile": "Edit profile",
+  "navigation_bar.favourites": "Favourites",
+  "navigation_bar.follow_requests": "Follow requests",
+  "navigation_bar.info": "Extended information",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
+  "navigation_bar.logout": "Logout",
+  "navigation_bar.mutes": "Muted users",
+  "navigation_bar.pins": "Pinned toots",
+  "navigation_bar.preferences": "Preferences",
+  "navigation_bar.public_timeline": "Federated timeline",
+  "notification.favourite": "{name} favourited your status",
+  "notification.follow": "{name} followed you",
+  "notification.mention": "{name} mentioned you",
+  "notification.reblog": "{name} boosted your status",
+  "notifications.clear": "Clear notifications",
+  "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
+  "notifications.column_settings.alert": "Desktop notifications",
+  "notifications.column_settings.favourite": "Favourites:",
+  "notifications.column_settings.follow": "New followers:",
+  "notifications.column_settings.mention": "Mentions:",
+  "notifications.column_settings.push": "Push notifications",
+  "notifications.column_settings.push_meta": "This device",
+  "notifications.column_settings.reblog": "Boosts:",
+  "notifications.column_settings.show": "Show in column",
+  "notifications.column_settings.sound": "Play sound",
+  "onboarding.done": "Done",
+  "onboarding.next": "Next",
+  "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
+  "onboarding.page_four.home": "The home timeline shows posts from people you follow.",
+  "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
+  "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
+  "onboarding.page_one.full_handle": "Your full handle",
+  "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.",
+  "onboarding.page_one.welcome": "Welcome to Mastodon!",
+  "onboarding.page_six.admin": "Your instance's admin is {admin}.",
+  "onboarding.page_six.almost_done": "Almost done...",
+  "onboarding.page_six.appetoot": "Bon Appetoot!",
+  "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
+  "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
+  "onboarding.page_six.guidelines": "community guidelines",
+  "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
+  "onboarding.page_six.various_app": "mobile apps",
+  "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
+  "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
+  "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
+  "onboarding.skip": "Skip",
+  "privacy.change": "Adjust status privacy",
+  "privacy.direct.long": "Post to mentioned users only",
+  "privacy.direct.short": "Direct",
+  "privacy.private.long": "Post to followers only",
+  "privacy.private.short": "Followers-only",
+  "privacy.public.long": "Post to public timelines",
+  "privacy.public.short": "Public",
+  "privacy.unlisted.long": "Do not show in public timelines",
+  "privacy.unlisted.short": "Unlisted",
+  "regeneration_indicator.label": "Loading…",
+  "regeneration_indicator.sublabel": "Your home feed is being prepared!",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
+  "reply_indicator.cancel": "Cancel",
+  "report.forward": "Forward to {target}",
+  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
+  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.placeholder": "Additional comments",
+  "report.submit": "Submit",
+  "report.target": "Report {target}",
+  "search.placeholder": "Search",
+  "search_popout.search_format": "Advanced search format",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
+  "search_popout.tips.hashtag": "hashtag",
+  "search_popout.tips.status": "status",
+  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+  "search_popout.tips.user": "user",
+  "search_results.accounts": "People",
+  "search_results.hashtags": "Hashtags",
+  "search_results.statuses": "Toots",
+  "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
+  "standalone.public_title": "A look inside...",
+  "status.block": "Block @{name}",
+  "status.cancel_reblog_private": "Unboost",
+  "status.cannot_reblog": "This post cannot be boosted",
+  "status.delete": "Delete",
+  "status.direct": "Direct message @{name}",
+  "status.embed": "Embed",
+  "status.favourite": "Favourite",
+  "status.load_more": "Load more",
+  "status.media_hidden": "Media hidden",
+  "status.mention": "Mention @{name}",
+  "status.more": "More",
+  "status.mute": "Mute @{name}",
+  "status.mute_conversation": "Mute conversation",
+  "status.open": "Expand this status",
+  "status.pin": "Pin on profile",
+  "status.pinned": "Pinned toot",
+  "status.reblog": "Boost",
+  "status.reblog_private": "Boost to original audience",
+  "status.reblogged_by": "{name} boosted",
+  "status.reply": "Reply",
+  "status.replyAll": "Reply to thread",
+  "status.report": "Report @{name}",
+  "status.sensitive_toggle": "Click to view",
+  "status.sensitive_warning": "Sensitive content",
+  "status.share": "Share",
+  "status.show_less": "Show less",
+  "status.show_less_all": "Show less for all",
+  "status.show_more": "Show more",
+  "status.show_more_all": "Show more for all",
+  "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
+  "tabs_bar.federated_timeline": "Federated",
+  "tabs_bar.home": "Home",
+  "tabs_bar.local_timeline": "Local",
+  "tabs_bar.notifications": "Notifications",
+  "tabs_bar.search": "Search",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "upload_area.title": "Drag & drop to upload",
+  "upload_button.label": "Add media",
+  "upload_form.description": "Describe for the visually impaired",
+  "upload_form.focus": "Crop",
+  "upload_form.undo": "Undo",
+  "upload_progress.label": "Uploading...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound"
+}
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index ad6f3b712..d8e69fd3c 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -138,6 +138,7 @@
   "keyboard_shortcuts.mention": "to mention author",
   "keyboard_shortcuts.reply": "to reply",
   "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "to start a brand new toot",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index e51163971..37587c14c 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "por mencii la aŭtoron",
   "keyboard_shortcuts.reply": "por respondi",
   "keyboard_shortcuts.search": "por fokusigi la serĉilon",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "por komenci tute novan mesaĝon",
   "keyboard_shortcuts.unfocus": "por malfokusigi la tekstujon aŭ la serĉilon",
   "keyboard_shortcuts.up": "por iri supren en la listo",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 61ea0588d..41d7db9da 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "para mencionar al autor",
   "keyboard_shortcuts.reply": "para responder",
   "keyboard_shortcuts.search": "para poner el foco en la búsqueda",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "para comenzar un nuevo toot",
   "keyboard_shortcuts.unfocus": "para retirar el foco de la caja de redacción/búsqueda",
   "keyboard_shortcuts.up": "para ir hacia arriba en la lista",
diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json
new file mode 100644
index 000000000..49cdf5630
--- /dev/null
+++ b/app/javascript/mastodon/locales/eu.json
@@ -0,0 +1,296 @@
+{
+  "account.block": "Blokeatu @{name}",
+  "account.block_domain": "{domain}(e)ko guztia ezkutatu",
+  "account.blocked": "Blokeatuta",
+  "account.direct": "@{name}(e)ri mezu zuzena bidali",
+  "account.disclaimer_full": "Baliteke beheko informazioak erabiltzailearen profilaren zati bat baino ez erakustea.",
+  "account.domain_blocked": "Ezkutatutako domeinua",
+  "account.edit_profile": "Profila aldatu",
+  "account.follow": "Jarraitu",
+  "account.followers": "Jarraitzaileak",
+  "account.follows": "Jarraitzen",
+  "account.follows_you": "Jarraitzen dizu",
+  "account.hide_reblogs": "@{name}(e)k sustatutakoak ezkutatu",
+  "account.media": "Media",
+  "account.mention": "@{name} aipatu",
+  "account.moved_to": "{name} hona lekualdatu da:",
+  "account.mute": "@{name} isilarazi",
+  "account.mute_notifications": "@{name}(e)ren jakinarazpenak isilarazi",
+  "account.muted": "Isilarazita",
+  "account.posts": "Toots",
+  "account.posts_with_replies": "Toots and replies",
+  "account.report": "@{name} salatu",
+  "account.requested": "Onarpenaren zain. Klikatu jarraitzeko eskaera ezeztatzeko",
+  "account.share": "@{name}(e)ren profila elkarbanatu",
+  "account.show_reblogs": "@{name}(e)k sustatutakoak erakutsi",
+  "account.unblock": "@{name} desblokeatu",
+  "account.unblock_domain": "Berriz erakutsi {domain}",
+  "account.unfollow": "Jarraitzeari utzi",
+  "account.unmute": "Unmute @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.view_full_profile": "View full profile",
+  "alert.unexpected.message": "An unexpected error occurred.",
+  "alert.unexpected.title": "Oops!",
+  "boost_modal.combo": "You can press {combo} to skip this next time",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
+  "column.blocks": "Blocked users",
+  "column.community": "Local timeline",
+  "column.direct": "Direct messages",
+  "column.domain_blocks": "Hidden domains",
+  "column.favourites": "Favourites",
+  "column.follow_requests": "Follow requests",
+  "column.home": "Home",
+  "column.lists": "Lists",
+  "column.mutes": "Muted users",
+  "column.notifications": "Notifications",
+  "column.pins": "Pinned toot",
+  "column.public": "Federated timeline",
+  "column_back_button.label": "Back",
+  "column_header.hide_settings": "Hide settings",
+  "column_header.moveLeft_settings": "Move column to the left",
+  "column_header.moveRight_settings": "Move column to the right",
+  "column_header.pin": "Pin",
+  "column_header.show_settings": "Show settings",
+  "column_header.unpin": "Unpin",
+  "column_subheading.navigation": "Navigation",
+  "column_subheading.settings": "Settings",
+  "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
+  "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
+  "compose_form.lock_disclaimer.lock": "locked",
+  "compose_form.placeholder": "What is on your mind?",
+  "compose_form.publish": "Toot",
+  "compose_form.publish_loud": "{publish}!",
+  "compose_form.sensitive.marked": "Media is marked as sensitive",
+  "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
+  "compose_form.spoiler.marked": "Text is hidden behind warning",
+  "compose_form.spoiler.unmarked": "Text is not hidden",
+  "compose_form.spoiler_placeholder": "Write your warning here",
+  "confirmation_modal.cancel": "Cancel",
+  "confirmations.block.confirm": "Block",
+  "confirmations.block.message": "Are you sure you want to block {name}?",
+  "confirmations.delete.confirm": "Delete",
+  "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.domain_block.confirm": "Hide entire domain",
+  "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
+  "confirmations.mute.confirm": "Mute",
+  "confirmations.mute.message": "Are you sure you want to mute {name}?",
+  "confirmations.unfollow.confirm": "Unfollow",
+  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
+  "emoji_button.activity": "Activity",
+  "emoji_button.custom": "Custom",
+  "emoji_button.flags": "Flags",
+  "emoji_button.food": "Food & Drink",
+  "emoji_button.label": "Insert emoji",
+  "emoji_button.nature": "Nature",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.objects": "Objects",
+  "emoji_button.people": "People",
+  "emoji_button.recent": "Frequently used",
+  "emoji_button.search": "Search...",
+  "emoji_button.search_results": "Search results",
+  "emoji_button.symbols": "Symbols",
+  "emoji_button.travel": "Travel & Places",
+  "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
+  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+  "empty_column.hashtag": "There is nothing in this hashtag yet.",
+  "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
+  "empty_column.home.public_timeline": "the public timeline",
+  "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
+  "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
+  "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
+  "follow_request.authorize": "Authorize",
+  "follow_request.reject": "Reject",
+  "getting_started.appsshort": "Apps",
+  "getting_started.faq": "FAQ",
+  "getting_started.heading": "Getting started",
+  "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.",
+  "getting_started.userguide": "User Guide",
+  "home.column_settings.advanced": "Advanced",
+  "home.column_settings.basic": "Basic",
+  "home.column_settings.filter_regex": "Filter out by regular expressions",
+  "home.column_settings.show_reblogs": "Show boosts",
+  "home.column_settings.show_replies": "Show replies",
+  "home.settings": "Column settings",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
+  "lightbox.close": "Close",
+  "lightbox.next": "Next",
+  "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among people you follow",
+  "lists.subheading": "Your lists",
+  "loading_indicator.label": "Loading...",
+  "media_gallery.toggle_visible": "Toggle visibility",
+  "missing_indicator.label": "Not found",
+  "missing_indicator.sublabel": "This resource could not be found",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "navigation_bar.blocks": "Blocked users",
+  "navigation_bar.community_timeline": "Local timeline",
+  "navigation_bar.direct": "Direct messages",
+  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.edit_profile": "Edit profile",
+  "navigation_bar.favourites": "Favourites",
+  "navigation_bar.follow_requests": "Follow requests",
+  "navigation_bar.info": "Extended information",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
+  "navigation_bar.logout": "Logout",
+  "navigation_bar.mutes": "Muted users",
+  "navigation_bar.pins": "Pinned toots",
+  "navigation_bar.preferences": "Preferences",
+  "navigation_bar.public_timeline": "Federated timeline",
+  "notification.favourite": "{name} favourited your status",
+  "notification.follow": "{name} followed you",
+  "notification.mention": "{name} mentioned you",
+  "notification.reblog": "{name} boosted your status",
+  "notifications.clear": "Clear notifications",
+  "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
+  "notifications.column_settings.alert": "Desktop notifications",
+  "notifications.column_settings.favourite": "Favourites:",
+  "notifications.column_settings.follow": "New followers:",
+  "notifications.column_settings.mention": "Mentions:",
+  "notifications.column_settings.push": "Push notifications",
+  "notifications.column_settings.push_meta": "This device",
+  "notifications.column_settings.reblog": "Boosts:",
+  "notifications.column_settings.show": "Show in column",
+  "notifications.column_settings.sound": "Play sound",
+  "onboarding.done": "Done",
+  "onboarding.next": "Next",
+  "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
+  "onboarding.page_four.home": "The home timeline shows posts from people you follow.",
+  "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
+  "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
+  "onboarding.page_one.full_handle": "Your full handle",
+  "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.",
+  "onboarding.page_one.welcome": "Welcome to Mastodon!",
+  "onboarding.page_six.admin": "Your instance's admin is {admin}.",
+  "onboarding.page_six.almost_done": "Almost done...",
+  "onboarding.page_six.appetoot": "Bon Appetoot!",
+  "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
+  "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
+  "onboarding.page_six.guidelines": "community guidelines",
+  "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
+  "onboarding.page_six.various_app": "mobile apps",
+  "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
+  "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
+  "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
+  "onboarding.skip": "Skip",
+  "privacy.change": "Adjust status privacy",
+  "privacy.direct.long": "Post to mentioned users only",
+  "privacy.direct.short": "Direct",
+  "privacy.private.long": "Post to followers only",
+  "privacy.private.short": "Followers-only",
+  "privacy.public.long": "Post to public timelines",
+  "privacy.public.short": "Public",
+  "privacy.unlisted.long": "Do not show in public timelines",
+  "privacy.unlisted.short": "Unlisted",
+  "regeneration_indicator.label": "Loading…",
+  "regeneration_indicator.sublabel": "Your home feed is being prepared!",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
+  "reply_indicator.cancel": "Cancel",
+  "report.forward": "Forward to {target}",
+  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
+  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.placeholder": "Additional comments",
+  "report.submit": "Submit",
+  "report.target": "Report {target}",
+  "search.placeholder": "Search",
+  "search_popout.search_format": "Advanced search format",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
+  "search_popout.tips.hashtag": "hashtag",
+  "search_popout.tips.status": "status",
+  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+  "search_popout.tips.user": "user",
+  "search_results.accounts": "People",
+  "search_results.hashtags": "Hashtags",
+  "search_results.statuses": "Toots",
+  "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
+  "standalone.public_title": "A look inside...",
+  "status.block": "Block @{name}",
+  "status.cancel_reblog_private": "Unboost",
+  "status.cannot_reblog": "This post cannot be boosted",
+  "status.delete": "Delete",
+  "status.direct": "Direct message @{name}",
+  "status.embed": "Embed",
+  "status.favourite": "Favourite",
+  "status.load_more": "Load more",
+  "status.media_hidden": "Media hidden",
+  "status.mention": "Mention @{name}",
+  "status.more": "More",
+  "status.mute": "Mute @{name}",
+  "status.mute_conversation": "Mute conversation",
+  "status.open": "Expand this status",
+  "status.pin": "Pin on profile",
+  "status.pinned": "Pinned toot",
+  "status.reblog": "Boost",
+  "status.reblog_private": "Boost to original audience",
+  "status.reblogged_by": "{name} boosted",
+  "status.reply": "Reply",
+  "status.replyAll": "Reply to thread",
+  "status.report": "Report @{name}",
+  "status.sensitive_toggle": "Click to view",
+  "status.sensitive_warning": "Sensitive content",
+  "status.share": "Share",
+  "status.show_less": "Show less",
+  "status.show_less_all": "Show less for all",
+  "status.show_more": "Show more",
+  "status.show_more_all": "Show more for all",
+  "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
+  "tabs_bar.federated_timeline": "Federated",
+  "tabs_bar.home": "Home",
+  "tabs_bar.local_timeline": "Local",
+  "tabs_bar.notifications": "Notifications",
+  "tabs_bar.search": "Search",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "upload_area.title": "Drag & drop to upload",
+  "upload_button.label": "Add media",
+  "upload_form.description": "Describe for the visually impaired",
+  "upload_form.focus": "Crop",
+  "upload_form.undo": "Undo",
+  "upload_progress.label": "Uploading...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound"
+}
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index cfe93007d..99aba00c3 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "برای نام‌بردن از نویسنده",
   "keyboard_shortcuts.reply": "برای پاسخ‌دادن",
   "keyboard_shortcuts.search": "برای فعال‌کردن جستجو",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "برای آغاز یک بوق تازه",
   "keyboard_shortcuts.unfocus": "برای برداشتن توجه از نوشتن/جستجو",
   "keyboard_shortcuts.up": "برای بالا رفتن در فهرست",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index 1677c3c6c..07d4d9aa5 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "mainitse julkaisija",
   "keyboard_shortcuts.reply": "vastaa",
   "keyboard_shortcuts.search": "siirry hakukenttään",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "ala kirjoittaa uutta tuuttausta",
   "keyboard_shortcuts.unfocus": "siirry pois tekstikentästä tai hakukentästä",
   "keyboard_shortcuts.up": "siirry listassa ylöspäin",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 98c1c43d2..a4af97dda 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -2,7 +2,7 @@
   "account.block": "Bloquer @{name}",
   "account.block_domain": "Tout masquer venant de {domain}",
   "account.blocked": "Bloqué",
-  "account.direct": "Direct message @{name}",
+  "account.direct": "Message direct @{name}",
   "account.disclaimer_full": "Les données ci-dessous peuvent ne pas refléter ce profil dans sa totalité.",
   "account.domain_blocked": "Domaine caché",
   "account.edit_profile": "Modifier le profil",
@@ -18,7 +18,7 @@
   "account.mute_notifications": "Ignorer les notifications de @{name}",
   "account.muted": "Silencé",
   "account.posts": "Pouets",
-  "account.posts_with_replies": "Pouets avec réponses",
+  "account.posts_with_replies": "Pouets et réponses",
   "account.report": "Signaler",
   "account.requested": "En attente d'approbation. Cliquez pour annuler la requête",
   "account.share": "Partager le profil de @{name}",
@@ -29,8 +29,8 @@
   "account.unmute": "Ne plus masquer",
   "account.unmute_notifications": "Réactiver les notifications de @{name}",
   "account.view_full_profile": "Afficher le profil complet",
-  "alert.unexpected.message": "An unexpected error occurred.",
-  "alert.unexpected.title": "Oops!",
+  "alert.unexpected.message": "Une erreur non-attendue s'est produite.",
+  "alert.unexpected.title": "Oups !",
   "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois",
   "bundle_column_error.body": "Une erreur s’est produite lors du chargement de ce composant.",
   "bundle_column_error.retry": "Réessayer",
@@ -40,8 +40,8 @@
   "bundle_modal_error.retry": "Réessayer",
   "column.blocks": "Comptes bloqués",
   "column.community": "Fil public local",
-  "column.direct": "Direct messages",
-  "column.domain_blocks": "Hidden domains",
+  "column.direct": "Messages directs",
+  "column.domain_blocks": "Domaines cachés",
   "column.favourites": "Favoris",
   "column.follow_requests": "Demandes de suivi",
   "column.home": "Accueil",
@@ -59,7 +59,7 @@
   "column_header.unpin": "Retirer",
   "column_subheading.navigation": "Navigation",
   "column_subheading.settings": "Paramètres",
-  "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning": "Ce pouet sera uniquement visible à tous les utilisateurs mentionnés.",
   "compose_form.hashtag_warning": "Ce pouet ne sera pas listé dans les recherches par hashtag car sa visibilité est réglée sur \"non-listé\". Seuls les pouets avec une visibilité \"publique\" peuvent être recherchés par hashtag.",
   "compose_form.lock_disclaimer": "Votre compte n’est pas {locked}. Tout le monde peut vous suivre et voir vos pouets privés.",
   "compose_form.lock_disclaimer.lock": "verrouillé",
@@ -101,7 +101,7 @@
   "emoji_button.symbols": "Symboles",
   "emoji_button.travel": "Lieux & Voyages",
   "empty_column.community": "Le fil public local est vide. Écrivez donc quelque chose pour le remplir !",
-  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+  "empty_column.direct": "Vous n'avez pas encore de messages directs. Lorsque vous en enverrez ou recevrez un, il s'affichera ici.",
   "empty_column.hashtag": "Il n’y a encore aucun contenu associé à ce hashtag.",
   "empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres personnes.",
   "empty_column.home.public_timeline": "le fil public",
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "pour mentionner l'auteur",
   "keyboard_shortcuts.reply": "pour répondre",
   "keyboard_shortcuts.search": "pour cibler la recherche",
+  "keyboard_shortcuts.toggle_hidden": "pour afficher/cacher un texte derrière CW",
   "keyboard_shortcuts.toot": "pour démarrer un tout nouveau pouet",
   "keyboard_shortcuts.unfocus": "pour recentrer composer textarea/search",
   "keyboard_shortcuts.up": "pour remonter dans la liste",
@@ -156,8 +157,8 @@
   "mute_modal.hide_notifications": "Masquer les notifications de cette personne ?",
   "navigation_bar.blocks": "Comptes bloqués",
   "navigation_bar.community_timeline": "Fil public local",
-  "navigation_bar.direct": "Direct messages",
-  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.direct": "Messages directs",
+  "navigation_bar.domain_blocks": "Domaines cachés",
   "navigation_bar.edit_profile": "Modifier le profil",
   "navigation_bar.favourites": "Favoris",
   "navigation_bar.follow_requests": "Demandes de suivi",
@@ -241,10 +242,10 @@
   "search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}",
   "standalone.public_title": "Un aperçu …",
   "status.block": "Block @{name}",
-  "status.cancel_reblog_private": "Unboost",
+  "status.cancel_reblog_private": "Dé-booster",
   "status.cannot_reblog": "Cette publication ne peut être boostée",
   "status.delete": "Effacer",
-  "status.direct": "Direct message @{name}",
+  "status.direct": "Message direct @{name}",
   "status.embed": "Intégrer",
   "status.favourite": "Ajouter aux favoris",
   "status.load_more": "Charger plus",
@@ -257,7 +258,7 @@
   "status.pin": "Épingler sur le profil",
   "status.pinned": "Pouet épinglé",
   "status.reblog": "Partager",
-  "status.reblog_private": "Boost to original audience",
+  "status.reblog_private": "Booster vers l'audience originale",
   "status.reblogged_by": "{name} a partagé :",
   "status.reply": "Répondre",
   "status.replyAll": "Répondre au fil",
@@ -275,7 +276,7 @@
   "tabs_bar.home": "Accueil",
   "tabs_bar.local_timeline": "Fil public local",
   "tabs_bar.notifications": "Notifications",
-  "tabs_bar.search": "Search",
+  "tabs_bar.search": "Chercher",
   "ui.beforeunload": "Votre brouillon sera perdu si vous quittez Mastodon.",
   "upload_area.title": "Glissez et déposez pour envoyer",
   "upload_button.label": "Joindre un média",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index fca42374d..652ca31d1 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -18,7 +18,7 @@
   "account.mute_notifications": "Acalar as notificacións de @{name}",
   "account.muted": "Muted",
   "account.posts": "Toots",
-  "account.posts_with_replies": "Toots with replies",
+  "account.posts_with_replies": "Toots e respostas",
   "account.report": "Informar sobre @{name}",
   "account.requested": "Agardando aceptación. Pulse para cancelar a solicitude de seguimento",
   "account.share": "Compartir o perfil de @{name}",
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "para mencionar o autor",
   "keyboard_shortcuts.reply": "para responder",
   "keyboard_shortcuts.search": "para centrar a busca",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "escribir un toot novo",
   "keyboard_shortcuts.unfocus": "quitar o foco do área de escritura/busca",
   "keyboard_shortcuts.up": "ir hacia arriba na lista",
@@ -242,7 +243,7 @@
   "standalone.public_title": "Ollada dentro...",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Unboost",
-  "status.cannot_reblog": "Esta mensaxe non pode ser promocionada",
+  "status.cannot_reblog": "Esta mensaxe non pode ser promovida",
   "status.delete": "Eliminar",
   "status.direct": "Direct message @{name}",
   "status.embed": "Incrustar",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index e3e87f1d0..0ffbb14f3 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "לאזכר את המחבר(ת)",
   "keyboard_shortcuts.reply": "לענות",
   "keyboard_shortcuts.search": "להתמקד בחלון החיפוש",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "להתחיל חיצרוץ חדש",
   "keyboard_shortcuts.unfocus": "לצאת מתיבת חיבור/חיפוש",
   "keyboard_shortcuts.up": "לנוע במעלה הרשימה",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index b41c98394..c41cc3ea1 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "to mention author",
   "keyboard_shortcuts.reply": "to reply",
   "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "to start a brand new toot",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 956accc67..a0c186184 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "szerző megjelenítése",
   "keyboard_shortcuts.reply": "válaszolás",
   "keyboard_shortcuts.search": "kereső kiemelése",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "új tülk megkezdése",
   "keyboard_shortcuts.unfocus": "tülk szerkesztés/keresés fókuszpontból való kivétele",
   "keyboard_shortcuts.up": "fennebb helyezés a listában",
diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json
index 33e079201..a0442bad4 100644
--- a/app/javascript/mastodon/locales/hy.json
+++ b/app/javascript/mastodon/locales/hy.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "հեղինակին նշելու համար",
   "keyboard_shortcuts.reply": "պատասխանելու համար",
   "keyboard_shortcuts.search": "որոնման դաշտին սեւեռվելու համար",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "թարմ թութ սկսելու համար",
   "keyboard_shortcuts.unfocus": "տեքստի/որոնման տիրույթից ապասեւեռվելու համար",
   "keyboard_shortcuts.up": "ցանկով վերեւ շարժվելու համար",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index 412ffd3a0..2fd922544 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "to mention author",
   "keyboard_shortcuts.reply": "to reply",
   "keyboard_shortcuts.search": "untuk fokus mencari",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "to start a brand new toot",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index 9730bf934..ed45ee11e 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "to mention author",
   "keyboard_shortcuts.reply": "to reply",
   "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "to start a brand new toot",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 5146d7ca2..a7ca62015 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -1,7 +1,7 @@
 {
   "account.block": "Blocca @{name}",
   "account.block_domain": "Hide everything from {domain}",
-  "account.blocked": "Blocked",
+  "account.blocked": "Bloccato",
   "account.direct": "Direct Message @{name}",
   "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
   "account.domain_blocked": "Domain hidden",
@@ -17,8 +17,8 @@
   "account.mute": "Silenzia @{name}",
   "account.mute_notifications": "Mute notifications from @{name}",
   "account.muted": "Muted",
-  "account.posts": "Posts",
-  "account.posts_with_replies": "Toots with replies",
+  "account.posts": "Toot",
+  "account.posts_with_replies": "Toot con risposte",
   "account.report": "Segnala @{name}",
   "account.requested": "In attesa di approvazione",
   "account.share": "Share @{name}'s profile",
@@ -105,7 +105,7 @@
   "empty_column.hashtag": "Non c'è ancora nessun post con questo hashtag.",
   "empty_column.home": "Non stai ancora seguendo nessuno. Visita {public} o usa la ricerca per incontrare nuove persone.",
   "empty_column.home.public_timeline": "la timeline pubblica",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "Non c'è niente in questo elenco ancora. Quando i membri di questo elenco postano nuovi stati, questi appariranno qui.",
   "empty_column.notifications": "Non hai ancora nessuna notifica. Interagisci con altri per iniziare conversazioni.",
   "empty_column.public": "Qui non c'è nulla! Scrivi qualcosa pubblicamente, o aggiungi utenti da altri server per riempire questo spazio.",
   "follow_request.authorize": "Autorizza",
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "to mention author",
   "keyboard_shortcuts.reply": "to reply",
   "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "to start a brand new toot",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index abd18742a..dbb4562de 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -29,8 +29,8 @@
   "account.unmute": "@{name}さんのミュートを解除",
   "account.unmute_notifications": "@{name}さんからの通知を受け取るようにする",
   "account.view_full_profile": "全ての情報を見る",
-  "alert.unexpected.message": "不明なエラーが発生しました",
-  "alert.unexpected.title": "エラー",
+  "alert.unexpected.message": "不明なエラーが発生しました。",
+  "alert.unexpected.title": "エラー!",
   "boost_modal.combo": "次からは{combo}を押せばスキップできます",
   "bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。",
   "bundle_column_error.retry": "再試行",
@@ -104,7 +104,7 @@
   "emoji_button.symbols": "記号",
   "emoji_button.travel": "旅行と場所",
   "empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",
-  "empty_column.direct": "あなたはまだダイレクトメッセージを受け取っていません。あなたが送ったり受け取ったりすると、ここに表示されます。",
+  "empty_column.direct": "ダイレクトメッセージはまだありません。ダイレクトメッセージをやりとりすると、ここに表示されます。",
   "empty_column.hashtag": "このハッシュタグはまだ使われていません。",
   "empty_column.home": "まだ誰もフォローしていません。{public}を見に行くか、検索を使って他のユーザーを見つけましょう。",
   "empty_column.home.public_timeline": "連合タイムライン",
@@ -138,6 +138,7 @@
   "keyboard_shortcuts.mention": "メンション",
   "keyboard_shortcuts.reply": "返信",
   "keyboard_shortcuts.search": "検索欄に移動",
+  "keyboard_shortcuts.toggle_hidden": "CWで隠れた文を見る/隠す",
   "keyboard_shortcuts.toot": "新規トゥート",
   "keyboard_shortcuts.unfocus": "トゥート入力欄・検索欄から離れる",
   "keyboard_shortcuts.up": "カラム内一つ上に移動",
@@ -159,7 +160,7 @@
   "mute_modal.hide_notifications": "このユーザーからの通知を隠しますか?",
   "navigation_bar.blocks": "ブロックしたユーザー",
   "navigation_bar.community_timeline": "ローカルタイムライン",
-  "navigation_bar.direct": "Direct messages",
+  "navigation_bar.direct": "ダイレクトメッセージ",
   "navigation_bar.domain_blocks": "非表示にしたドメイン",
   "navigation_bar.edit_profile": "プロフィールを編集",
   "navigation_bar.favourites": "お気に入り",
@@ -245,7 +246,7 @@
   "search_results.total": "{count, number}件の結果",
   "standalone.public_title": "今こんな話をしています...",
   "status.block": "@{name}さんをブロック",
-  "status.cancel_reblog_private": "Unboost",
+  "status.cancel_reblog_private": "ブースト解除",
   "status.cannot_reblog": "この投稿はブーストできません",
   "status.delete": "削除",
   "status.direct": "@{name}さんにダイレクトメッセージ",
@@ -261,7 +262,7 @@
   "status.pin": "プロフィールに固定表示",
   "status.pinned": "固定されたトゥート",
   "status.reblog": "ブースト",
-  "status.reblog_private": "Boost to original audience",
+  "status.reblog_private": "ブースト",
   "status.reblogged_by": "{name}さんがブースト",
   "status.reply": "返信",
   "status.replyAll": "全員に返信",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 92367dc95..2a2734673 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -2,7 +2,7 @@
   "account.block": "@{name}을 차단",
   "account.block_domain": "{domain} 전체를 숨김",
   "account.blocked": "차단 됨",
-  "account.direct": "Direct message @{name}",
+  "account.direct": "@{name}으로부터의 다이렉트 메시지",
   "account.disclaimer_full": "여기 있는 정보는 유저의 프로파일을 정확히 반영하지 못 할 수도 있습니다.",
   "account.domain_blocked": "도메인 숨겨짐",
   "account.edit_profile": "프로필 편집",
@@ -12,7 +12,7 @@
   "account.follows_you": "날 팔로우합니다",
   "account.hide_reblogs": "@{name}의 부스트를 숨기기",
   "account.media": "미디어",
-  "account.mention": "답장",
+  "account.mention": "@{name}에게 글쓰기",
   "account.moved_to": "{name}는 계정을 이동했습니다:",
   "account.mute": "@{name} 뮤트",
   "account.mute_notifications": "@{name}의 알림을 뮤트",
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "멘션",
   "keyboard_shortcuts.reply": "답장",
   "keyboard_shortcuts.search": "검색창에 포커스",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "새 툿 작성",
   "keyboard_shortcuts.unfocus": "작성창에서 포커스 해제",
   "keyboard_shortcuts.up": "리스트에서 위로 이동",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index c18ddbd01..adc1d19a7 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -18,7 +18,7 @@
   "account.mute_notifications": "Negeer meldingen van @{name}",
   "account.muted": "Genegeerd",
   "account.posts": "Toots",
-  "account.posts_with_replies": "Toots met reacties",
+  "account.posts_with_replies": "Toots en reacties",
   "account.report": "Rapporteer @{name}",
   "account.requested": "Wacht op goedkeuring. Klik om het volgverzoek te annuleren",
   "account.share": "Profiel van @{name} delen",
@@ -29,8 +29,8 @@
   "account.unmute": "@{name} niet meer negeren",
   "account.unmute_notifications": "@{name} meldingen niet meer negeren",
   "account.view_full_profile": "Volledig profiel tonen",
-  "alert.unexpected.message": "An unexpected error occurred.",
-  "alert.unexpected.title": "Oops!",
+  "alert.unexpected.message": "Er deed zich een onverwachte fout voor",
+  "alert.unexpected.title": "Oeps!",
   "boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan",
   "bundle_column_error.body": "Tijdens het laden van dit onderdeel is er iets fout gegaan.",
   "bundle_column_error.retry": "Opnieuw proberen",
@@ -40,8 +40,8 @@
   "bundle_modal_error.retry": "Opnieuw proberen",
   "column.blocks": "Geblokkeerde gebruikers",
   "column.community": "Lokale tijdlijn",
-  "column.direct": "Direct messages",
-  "column.domain_blocks": "Hidden domains",
+  "column.direct": "Directe berichten",
+  "column.domain_blocks": "Verborgen domeinen",
   "column.favourites": "Favorieten",
   "column.follow_requests": "Volgverzoeken",
   "column.home": "Start",
@@ -59,7 +59,7 @@
   "column_header.unpin": "Losmaken",
   "column_subheading.navigation": "Navigatie",
   "column_subheading.settings": "Instellingen",
-  "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning": "Deze toot zal alleen zichtbaar zijn voor alle vermelde gebruikers.",
   "compose_form.hashtag_warning": "Deze toot valt niet onder een hashtag te bekijken, omdat deze niet op openbare tijdlijnen wordt getoond. Alleen openbare toots kunnen via hashtags gevonden worden.",
   "compose_form.lock_disclaimer": "Jouw account is niet {locked}. Iedereen kan jou volgen en toots zien die je alleen aan volgers hebt gericht.",
   "compose_form.lock_disclaimer.lock": "besloten",
@@ -101,7 +101,7 @@
   "emoji_button.symbols": "Symbolen",
   "emoji_button.travel": "Reizen en plekken",
   "empty_column.community": "De lokale tijdlijn is nog leeg. Toot iets in het openbaar om de bal aan het rollen te krijgen!",
-  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+  "empty_column.direct": "Je hebt nog geen directe berichten. Wanneer je er een verzend of ontvangt, zijn deze hier te zien.",
   "empty_column.hashtag": "Er is nog niks te vinden onder deze hashtag.",
   "empty_column.home": "Jij volgt nog niemand. Bezoek {public} of gebruik het zoekvenster om andere mensen te ontmoeten.",
   "empty_column.home.public_timeline": "de globale tijdlijn",
@@ -127,7 +127,7 @@
   "keyboard_shortcuts.compose": "om het tekstvak voor toots te focussen",
   "keyboard_shortcuts.description": "Omschrijving",
   "keyboard_shortcuts.down": "om naar beneden door de lijst te bewegen",
-  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.enter": "om toot volledig te tonen",
   "keyboard_shortcuts.favourite": "om als favoriet te markeren",
   "keyboard_shortcuts.heading": "Sneltoetsen",
   "keyboard_shortcuts.hotkey": "Sneltoets",
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "om de auteur te vermelden",
   "keyboard_shortcuts.reply": "om te reageren",
   "keyboard_shortcuts.search": "om het zoekvak te focussen",
+  "keyboard_shortcuts.toggle_hidden": "om tekst achter een waarschuwing (CW) te tonen/verbergen",
   "keyboard_shortcuts.toot": "om een nieuwe toot te starten",
   "keyboard_shortcuts.unfocus": "om het tekst- en zoekvak te ontfocussen",
   "keyboard_shortcuts.up": "om omhoog te bewegen in de lijst",
@@ -156,8 +157,8 @@
   "mute_modal.hide_notifications": "Verberg meldingen van deze persoon?",
   "navigation_bar.blocks": "Geblokkeerde gebruikers",
   "navigation_bar.community_timeline": "Lokale tijdlijn",
-  "navigation_bar.direct": "Direct messages",
-  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.direct": "Directe berichten",
+  "navigation_bar.domain_blocks": "Verborgen domeinen",
   "navigation_bar.edit_profile": "Profiel bewerken",
   "navigation_bar.favourites": "Favorieten",
   "navigation_bar.follow_requests": "Volgverzoeken",
@@ -241,10 +242,10 @@
   "search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}",
   "standalone.public_title": "Een kijkje binnenin...",
   "status.block": "Blokkeer @{name}",
-  "status.cancel_reblog_private": "Unboost",
+  "status.cancel_reblog_private": "Niet meer boosten",
   "status.cannot_reblog": "Deze toot kan niet geboost worden",
   "status.delete": "Verwijderen",
-  "status.direct": "Direct message @{name}",
+  "status.direct": "Directe toot @{name}",
   "status.embed": "Embed",
   "status.favourite": "Favoriet",
   "status.load_more": "Meer laden",
@@ -257,7 +258,7 @@
   "status.pin": "Aan profielpagina vastmaken",
   "status.pinned": "Vastgemaakte toot",
   "status.reblog": "Boost",
-  "status.reblog_private": "Boost to original audience",
+  "status.reblog_private": "Boost naar oorspronkelijke ontvangers",
   "status.reblogged_by": "{name} boostte",
   "status.reply": "Reageren",
   "status.replyAll": "Reageer op iedereen",
@@ -275,7 +276,7 @@
   "tabs_bar.home": "Start",
   "tabs_bar.local_timeline": "Lokaal",
   "tabs_bar.notifications": "Meldingen",
-  "tabs_bar.search": "Search",
+  "tabs_bar.search": "Zoeken",
   "ui.beforeunload": "Je concept zal verloren gaan als je Mastodon verlaat.",
   "upload_area.title": "Hierin slepen om te uploaden",
   "upload_button.label": "Media toevoegen",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index 282a72acb..0ee6d0722 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "å nevne forfatter",
   "keyboard_shortcuts.reply": "for å svare",
   "keyboard_shortcuts.search": "å fokusere søk",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "å starte en helt ny tut",
   "keyboard_shortcuts.unfocus": "å ufokusere komponerings-/søkefeltet",
   "keyboard_shortcuts.up": "å flytte opp i listen",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 7170aefb8..d4836e9fe 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -18,7 +18,7 @@
   "account.mute_notifications": "Rescondre las notificacions de @{name}",
   "account.muted": "Mes en silenci",
   "account.posts": "Tuts",
-  "account.posts_with_replies": "Tuts amb responsas",
+  "account.posts_with_replies": "Tuts e responsas",
   "account.report": "Senhalar @{name}",
   "account.requested": "Invitacion mandada. Clicatz per anullar",
   "account.share": "Partejar lo perfil a @{name}",
@@ -29,8 +29,8 @@
   "account.unmute": "Quitar de rescondre @{name}",
   "account.unmute_notifications": "Mostrar las notificacions de @{name}",
   "account.view_full_profile": "Veire lo perfil complèt",
-  "alert.unexpected.message": "An unexpected error occurred.",
-  "alert.unexpected.title": "Oops!",
+  "alert.unexpected.message": "Una error s’es producha.",
+  "alert.unexpected.title": "Ops !",
   "boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven",
   "bundle_column_error.body": "Quicòm a fach mèuca pendent lo cargament d’aqueste compausant.",
   "bundle_column_error.retry": "Tornar ensajar",
@@ -40,8 +40,8 @@
   "bundle_modal_error.retry": "Tornar ensajar",
   "column.blocks": "Personas blocadas",
   "column.community": "Flux public local",
-  "column.direct": "Direct messages",
-  "column.domain_blocks": "Hidden domains",
+  "column.direct": "Messatges dirèctes",
+  "column.domain_blocks": "Domenis blocats",
   "column.favourites": "Favorits",
   "column.follow_requests": "Demandas d’abonament",
   "column.home": "Acuèlh",
@@ -59,7 +59,7 @@
   "column_header.unpin": "Despenjar",
   "column_subheading.navigation": "Navigacion",
   "column_subheading.settings": "Paramètres",
-  "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning": "Aqueste tut serà pas que visibile pel monde mencionat.",
   "compose_form.hashtag_warning": "Aqueste tut serà pas ligat a cap etiqueta estant qu’es pas listat. Òm pas cercar que los tuts publics per etiqueta.",
   "compose_form.lock_disclaimer": "Vòstre compte es pas {locked}. Tot lo mond pòt vos sègre e veire los estatuts reservats als seguidors.",
   "compose_form.lock_disclaimer.lock": "clavat",
@@ -73,13 +73,13 @@
   "compose_form.spoiler_placeholder": "Escrivètz l’avertiment aquí",
   "confirmation_modal.cancel": "Anullar",
   "confirmations.block.confirm": "Blocar",
-  "confirmations.block.message": "Sètz segur de voler blocar {name} ?",
+  "confirmations.block.message": "Volètz vertadièrament blocar {name} ?",
   "confirmations.delete.confirm": "Escafar",
-  "confirmations.delete.message": "Sètz segur de voler escafar l’estatut ?",
+  "confirmations.delete.message": "Volètz vertadièrament escafar l’estatut ?",
   "confirmations.delete_list.confirm": "Suprimir",
-  "confirmations.delete_list.message": "Sètz segur de voler suprimir aquesta lista per totjorn ?",
+  "confirmations.delete_list.message": "Volètz vertadièrament suprimir aquesta lista per totjorn ?",
   "confirmations.domain_block.confirm": "Amagar tot lo domeni",
-  "confirmations.domain_block.message": "Sètz segur segur de voler blocar complètament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
+  "confirmations.domain_block.message": "Volètz vertadièrament blocar complètament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
   "confirmations.mute.confirm": "Rescondre",
   "confirmations.mute.message": "Sètz segur de voler rescondre {name} ?",
   "confirmations.unfollow.confirm": "Quitar de sègre",
@@ -101,7 +101,7 @@
   "emoji_button.symbols": "Simbòls",
   "emoji_button.travel": "Viatges & lòcs",
   "empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir !",
-  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+  "empty_column.direct": "Avètz pas encara de messatges. Quand ne mandatz un o que ne recebètz un, serà mostrat aquí.",
   "empty_column.hashtag": "I a pas encara de contengut ligat a aquesta etiqueta.",
   "empty_column.home": "Vòstre flux d’acuèlh es void. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.",
   "empty_column.home.public_timeline": "lo flux public",
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "mencionar l’autor",
   "keyboard_shortcuts.reply": "respondre",
   "keyboard_shortcuts.search": "anar a la recèrca",
+  "keyboard_shortcuts.toggle_hidden": "mostrar/amagar lo tèxte dels avertiments",
   "keyboard_shortcuts.toot": "començar un estatut tot novèl",
   "keyboard_shortcuts.unfocus": "quitar lo camp tèxte/de recèrca",
   "keyboard_shortcuts.up": "far montar dins la lista",
@@ -156,7 +157,7 @@
   "mute_modal.hide_notifications": "Rescondre las notificacions d’aquesta persona ?",
   "navigation_bar.blocks": "Personas blocadas",
   "navigation_bar.community_timeline": "Flux public local",
-  "navigation_bar.direct": "Direct messages",
+  "navigation_bar.direct": "Messatges dirèctes",
   "navigation_bar.domain_blocks": "Hidden domains",
   "navigation_bar.edit_profile": "Modificar lo perfil",
   "navigation_bar.favourites": "Favorits",
@@ -216,7 +217,7 @@
   "privacy.unlisted.short": "Pas-listat",
   "regeneration_indicator.label": "Cargament…",
   "regeneration_indicator.sublabel": "Sèm a preparar vòstre flux d’acuèlh !",
-  "relative_time.days": "fa {number} d",
+  "relative_time.days": "fa {number}d",
   "relative_time.hours": "fa {number}h",
   "relative_time.just_now": "ara",
   "relative_time.minutes": "fa {number} min",
@@ -235,16 +236,16 @@
   "search_popout.tips.status": "estatut",
   "search_popout.tips.text": "Lo tèxt brut tòrna escais, noms d’utilizaire e etiquetas correspondents",
   "search_popout.tips.user": "utilizaire",
-  "search_results.accounts": "Monde",
+  "search_results.accounts": "Gents",
   "search_results.hashtags": "Etiquetas",
   "search_results.statuses": "Tuts",
   "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}",
   "standalone.public_title": "Una ulhada dedins…",
   "status.block": "Blocar @{name}",
-  "status.cancel_reblog_private": "Unboost",
+  "status.cancel_reblog_private": "Quitar de partejar",
   "status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat",
   "status.delete": "Escafar",
-  "status.direct": "Direct message @{name}",
+  "status.direct": "Messatge per @{name}",
   "status.embed": "Embarcar",
   "status.favourite": "Apondre als favorits",
   "status.load_more": "Cargar mai",
@@ -257,7 +258,7 @@
   "status.pin": "Penjar al perfil",
   "status.pinned": "Tut penjat",
   "status.reblog": "Partejar",
-  "status.reblog_private": "Boost to original audience",
+  "status.reblog_private": "Partejar al l’audiéncia d’origina",
   "status.reblogged_by": "{name} a partejat",
   "status.reply": "Respondre",
   "status.replyAll": "Respondre a la conversacion",
@@ -275,7 +276,7 @@
   "tabs_bar.home": "Acuèlh",
   "tabs_bar.local_timeline": "Flux public local",
   "tabs_bar.notifications": "Notificacions",
-  "tabs_bar.search": "Search",
+  "tabs_bar.search": "Recèrcas",
   "ui.beforeunload": "Vòstre brolhon serà perdut se quitatz Mastodon.",
   "upload_area.title": "Lisatz e depausatz per mandar",
   "upload_button.label": "Ajustar un mèdia",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 08aea797d..6d6db7c82 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -106,8 +106,8 @@
   "empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!",
   "empty_column.direct": "Nie masz żadnych wiadomości bezpośrednich. Jeżeli wyślesz lub otrzymasz jakąś, będzie tu widoczna.",
   "empty_column.hashtag": "Nie ma wpisów oznaczonych tym hashtagiem. Możesz napisać pierwszy!",
-  "empty_column.home": "Nie śledzisz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.",
-  "empty_column.home.public_timeline": "publiczna oś czasu",
+  "empty_column.home": "Nie śledzisz nikogo. Odwiedź globalną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.",
+  "empty_column.home.public_timeline": "globalna oś czasu",
   "empty_column.list": "Nie ma nic na tej liście. Kiedy członkowie listy dodadzą nowe wpisy, pojawia się one tutaj.",
   "empty_column.notifications": "Nie masz żadnych powiadomień. Rozpocznij interakcje z innymi użytkownikami.",
   "empty_column.public": "Tu nic nie ma! Napisz coś publicznie, lub dodaj ludzi z innych instancji, aby to wyświetlić",
@@ -173,7 +173,7 @@
   "navigation_bar.mutes": "Wyciszeni użytkownicy",
   "navigation_bar.pins": "Przypięte wpisy",
   "navigation_bar.preferences": "Preferencje",
-  "navigation_bar.public_timeline": "Oś czasu federacji",
+  "navigation_bar.public_timeline": "Globalna oś czasu",
   "notification.favourite": "{name} dodał Twój wpis do ulubionych",
   "notification.follow": "{name} zaczął Cię śledzić",
   "notification.mention": "{name} wspomniał o tobie",
@@ -191,7 +191,7 @@
   "notifications.column_settings.sound": "Odtwarzaj dźwięk",
   "onboarding.done": "Gotowe",
   "onboarding.next": "Dalej",
-  "onboarding.page_five.public_timelines": "Lokalna oś czasu zawiera wszystkie publiczne wpisy z {domain}. Federalna oś czasu wyświetla publiczne wpisy śledzonych przez członków {domain}. Są to publiczne osie czasu – najlepszy sposób na poznanie nowych osób.",
+  "onboarding.page_five.public_timelines": "Lokalna oś czasu zawiera wszystkie publiczne wpisy z {domain}. Globalna oś czasu wyświetla publiczne wpisy śledzonych przez członków {domain}. Są to publiczne osie czasu – najlepszy sposób na poznanie nowych osób.",
   "onboarding.page_four.home": "Główna oś czasu wyświetla publiczne wpisy.",
   "onboarding.page_four.notifications": "Kolumna powiadomień wyświetla, gdy ktoś dokonuje interakcji z tobą.",
   "onboarding.page_one.federation": "Mastodon jest siecią niezależnych serwerów połączonych w jeden portal społecznościowy. Nazywamy te serwery instancjami.",
@@ -251,7 +251,7 @@
   "status.delete": "Usuń",
   "status.direct": "Wyślij wiadomość bezpośrednią do @{name}",
   "status.embed": "Osadź",
-  "status.favourite": "Ulubione",
+  "status.favourite": "Dodaj do ulubionych",
   "status.load_more": "Załaduj więcej",
   "status.media_hidden": "Zawartość multimedialna ukryta",
   "status.mention": "Wspomnij o @{name}",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index c604476c7..7f8690f91 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -29,7 +29,7 @@
   "account.unmute": "Não silenciar @{name}",
   "account.unmute_notifications": "Retirar silêncio das notificações vindas de @{name}",
   "account.view_full_profile": "Ver perfil completo",
-  "alert.unexpected.message": "An unexpected error occurred.",
+  "alert.unexpected.message": "Um erro inesperado ocorreu.",
   "alert.unexpected.title": "Oops!",
   "boost_modal.combo": "Você pode pressionar {combo} para ignorar este diálogo na próxima vez",
   "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.",
@@ -40,8 +40,8 @@
   "bundle_modal_error.retry": "Tente novamente",
   "column.blocks": "Usuários bloqueados",
   "column.community": "Local",
-  "column.direct": "Direct messages",
-  "column.domain_blocks": "Hidden domains",
+  "column.direct": "Mensagens diretas",
+  "column.domain_blocks": "Domínios escondidos",
   "column.favourites": "Favoritos",
   "column.follow_requests": "Seguidores pendentes",
   "column.home": "Página inicial",
@@ -59,7 +59,7 @@
   "column_header.unpin": "Desafixar",
   "column_subheading.navigation": "Navegação",
   "column_subheading.settings": "Configurações",
-  "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning": "Este toot só será visível a todos os usuários mencionados.",
   "compose_form.hashtag_warning": "Esse toot não será listado em nenhuma hashtag por ser não listado. Somente toots públicos podem ser pesquisados por hashtag.",
   "compose_form.lock_disclaimer": "A sua conta não está {locked}. Qualquer pessoa pode te seguir e visualizar postagens direcionadas a apenas seguidores.",
   "compose_form.lock_disclaimer.lock": "trancada",
@@ -101,7 +101,7 @@
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viagens & Lugares",
   "empty_column.community": "A timeline local está vazia. Escreva algo publicamente para começar!",
-  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+  "empty_column.direct": "Você não tem nenhuma mensagem direta ainda. Quando você enviar ou receber uma, as mensagens aparecerão por aqui.",
   "empty_column.hashtag": "Ainda não há qualquer conteúdo com essa hashtag.",
   "empty_column.home": "Você ainda não segue usuário algum. Visite a timeline {public} ou use o buscador para procurar e conhecer outros usuários.",
   "empty_column.home.public_timeline": "global",
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "para mencionar o autor",
   "keyboard_shortcuts.reply": "para responder",
   "keyboard_shortcuts.search": "para focar a pesquisa",
+  "keyboard_shortcuts.toggle_hidden": "mostrar/esconder o texto com aviso de conteúdo",
   "keyboard_shortcuts.toot": "para compor um novo toot",
   "keyboard_shortcuts.unfocus": "para remover o foco da área de composição/pesquisa",
   "keyboard_shortcuts.up": "para mover para cima na lista",
@@ -156,8 +157,8 @@
   "mute_modal.hide_notifications": "Esconder notificações deste usuário?",
   "navigation_bar.blocks": "Usuários bloqueados",
   "navigation_bar.community_timeline": "Local",
-  "navigation_bar.direct": "Direct messages",
-  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.direct": "Mensagens diretas",
+  "navigation_bar.domain_blocks": "Domínios escondidos",
   "navigation_bar.edit_profile": "Editar perfil",
   "navigation_bar.favourites": "Favoritos",
   "navigation_bar.follow_requests": "Seguidores pendentes",
@@ -241,10 +242,10 @@
   "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
   "standalone.public_title": "Dê uma espiada...",
   "status.block": "Block @{name}",
-  "status.cancel_reblog_private": "Unboost",
+  "status.cancel_reblog_private": "Retirar o compartilhamento",
   "status.cannot_reblog": "Esta postagem não pode ser compartilhada",
   "status.delete": "Excluir",
-  "status.direct": "Direct message @{name}",
+  "status.direct": "Enviar mensagem direta à @{name}",
   "status.embed": "Incorporar",
   "status.favourite": "Adicionar aos favoritos",
   "status.load_more": "Carregar mais",
@@ -257,7 +258,7 @@
   "status.pin": "Fixar no perfil",
   "status.pinned": "Toot fixado",
   "status.reblog": "Compartilhar",
-  "status.reblog_private": "Boost to original audience",
+  "status.reblog_private": "Compartilhar com a audiência original",
   "status.reblogged_by": "{name} compartilhou",
   "status.reply": "Responder",
   "status.replyAll": "Responder à sequência",
@@ -275,7 +276,7 @@
   "tabs_bar.home": "Página inicial",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificações",
-  "tabs_bar.search": "Search",
+  "tabs_bar.search": "Buscar",
   "ui.beforeunload": "Seu rascunho será perdido se você sair do Mastodon.",
   "upload_area.title": "Arraste e solte para enviar",
   "upload_button.label": "Adicionar mídia",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index 826785aad..ce816dc41 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "para mencionar o autor",
   "keyboard_shortcuts.reply": "para responder",
   "keyboard_shortcuts.search": "para focar na pesquisa",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "para compor um novo post",
   "keyboard_shortcuts.unfocus": "para remover o foco da área de publicação/pesquisa",
   "keyboard_shortcuts.up": "para mover para cima na lista",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index bb3cc1794..8eeebaf73 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "упомянуть автора поста",
   "keyboard_shortcuts.reply": "ответить",
   "keyboard_shortcuts.search": "перейти к поиску",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "начать писать новый пост",
   "keyboard_shortcuts.unfocus": "убрать фокус с поля ввода/поиска",
   "keyboard_shortcuts.up": "вверх по списку",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index 58274fd2d..e5e826c96 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -2,7 +2,7 @@
   "account.block": "Blokovať @{name}",
   "account.block_domain": "Ukryť všetko z {domain}",
   "account.blocked": "Blokovaný/á",
-  "account.direct": "Direct message @{name}",
+  "account.direct": "Súkromná správa pre @{name}",
   "account.disclaimer_full": "Inofrmácie nižšie nemusia byť úplným odrazom uživateľovho účtu.",
   "account.domain_blocked": "Doména ukrytá",
   "account.edit_profile": "Upraviť profil",
@@ -29,7 +29,7 @@
   "account.unmute": "Prestať ignorovať @{name}",
   "account.unmute_notifications": "Odtĺmiť notifikácie od @{name}",
   "account.view_full_profile": "Pozri celý profil",
-  "alert.unexpected.message": "An unexpected error occurred.",
+  "alert.unexpected.message": "Vyskytla sa neočakávaná chyba.",
   "alert.unexpected.title": "Oops!",
   "boost_modal.combo": "Nabudúce môžete kliknúť {combo} aby ste preskočili",
   "bundle_column_error.body": "Nastala chyba pri načítaní tohto komponentu.",
@@ -40,8 +40,8 @@
   "bundle_modal_error.retry": "Skúsiť znova",
   "column.blocks": "Blokovaní užívatelia",
   "column.community": "Lokálna časová os",
-  "column.direct": "Direct messages",
-  "column.domain_blocks": "Hidden domains",
+  "column.direct": "Súkromné správy",
+  "column.domain_blocks": "Skryté domény",
   "column.favourites": "Obľúbené",
   "column.follow_requests": "Žiadosti o sledovanie",
   "column.home": "Domov",
@@ -59,7 +59,7 @@
   "column_header.unpin": "Odopnúť",
   "column_subheading.navigation": "Navigácia",
   "column_subheading.settings": "Nastavenia",
-  "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning": "Tento príspevok bude videný výhradne iba spomenutými užívateľmi.",
   "compose_form.hashtag_warning": "Tento toot nebude zobrazený pod žiadným haštagom lebo nieje listovaný. Iba verejné tooty môžu byť nájdené podľa haštagu.",
   "compose_form.lock_disclaimer": "Váš účet nie je zamknutý. Ktokoľvek ťa môže nasledovať a vidieť tvoje správy pre sledujúcich.",
   "compose_form.lock_disclaimer.lock": "zamknutý",
@@ -101,7 +101,7 @@
   "emoji_button.symbols": "Symboly",
   "emoji_button.travel": "Cestovanie a miesta",
   "empty_column.community": "Lokálna časová os je prázdna. Napíšte niečo, aby sa to tu začalo hýbať!",
-  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+  "empty_column.direct": "Ešte nemáš žiadne súkromné správy. Keď nejakú pošleš, alebo dostaneš, ukáže sa tu.",
   "empty_column.hashtag": "Pod týmto hashtagom sa ešte nič nenachádza.",
   "empty_column.home": "Vaša lokálna osa je zatiaľ prázdna! Pre začiatok pozrite {public} alebo použite vyhľadávanie a nájdite tak ostatných používateľov.",
   "empty_column.home.public_timeline": "verejná časová os",
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "spomenúť autora",
   "keyboard_shortcuts.reply": "odpovedať",
   "keyboard_shortcuts.search": "zamerať sa na vyhľadávanie",
+  "keyboard_shortcuts.toggle_hidden": "ukáž/skry text za CW",
   "keyboard_shortcuts.toot": "začať úplne novú hlášku",
   "keyboard_shortcuts.unfocus": "nesústrediť sa na písaciu plochu, alebo hľadanie",
   "keyboard_shortcuts.up": "posunúť sa vyššie v zozname",
@@ -156,8 +157,8 @@
   "mute_modal.hide_notifications": "Skryť notifikácie od tohoto užívateľa?",
   "navigation_bar.blocks": "Blokovaní užívatelia",
   "navigation_bar.community_timeline": "Lokálna časová os",
-  "navigation_bar.direct": "Direct messages",
-  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.direct": "Súkromné správy",
+  "navigation_bar.domain_blocks": "Skryté domény",
   "navigation_bar.edit_profile": "Upraviť profil",
   "navigation_bar.favourites": "Obľúbené",
   "navigation_bar.follow_requests": "Žiadosti o sledovanie",
@@ -244,7 +245,7 @@
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "Tento príspevok nemôže byť re-tootnutý",
   "status.delete": "Zmazať",
-  "status.direct": "Direct message @{name}",
+  "status.direct": "Súkromná správa @{name}",
   "status.embed": "Vložiť",
   "status.favourite": "Páči sa mi",
   "status.load_more": "Zobraz viac",
@@ -275,7 +276,7 @@
   "tabs_bar.home": "Domov",
   "tabs_bar.local_timeline": "Lokálna",
   "tabs_bar.notifications": "Notifikácie",
-  "tabs_bar.search": "Search",
+  "tabs_bar.search": "Hľadaj",
   "ui.beforeunload": "Čo máte rozpísané sa stratí, ak opustíte Mastodon.",
   "upload_area.title": "Ťahaj a pusti pre nahratie",
   "upload_button.label": "Pridať médiá",
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index e4d07edd1..b1ea0d179 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "da pomenete autora",
   "keyboard_shortcuts.reply": "da odgovorite",
   "keyboard_shortcuts.search": "da se prebacite na pretragu",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "da započnete skroz novi tut",
   "keyboard_shortcuts.unfocus": "da ne budete više na pretrazi/pravljenju novog tuta",
   "keyboard_shortcuts.up": "da se pomerite na gore u listi",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index 60c781e9d..aa978675f 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "да поменете аутора",
   "keyboard_shortcuts.reply": "да одговорите",
   "keyboard_shortcuts.search": "да се пребаците на претрагу",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "да започнете скроз нови тут",
   "keyboard_shortcuts.unfocus": "да не будете више на претрази/прављењу новог тута",
   "keyboard_shortcuts.up": "да се померите на горе у листи",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 8fa6992f1..4efe88a7e 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -18,7 +18,7 @@
   "account.mute_notifications": "Stäng av notifieringar från @{name}",
   "account.muted": "Nertystad",
   "account.posts": "Inlägg",
-  "account.posts_with_replies": "Toots med svar",
+  "account.posts_with_replies": "Toots och svar",
   "account.report": "Rapportera @{name}",
   "account.requested": "Inväntar godkännande. Klicka för att avbryta följförfrågan",
   "account.share": "Dela @{name}'s profil",
@@ -29,7 +29,7 @@
   "account.unmute": "Ta bort tystad @{name}",
   "account.unmute_notifications": "Återaktivera notifikationer från @{name}",
   "account.view_full_profile": "Visa hela profilen",
-  "alert.unexpected.message": "An unexpected error occurred.",
+  "alert.unexpected.message": "Ett oväntat fel uppstod.",
   "alert.unexpected.title": "Oops!",
   "boost_modal.combo": "Du kan trycka {combo} för att slippa denna nästa gång",
   "bundle_column_error.body": "Något gick fel när du laddade denna komponent.",
@@ -40,8 +40,8 @@
   "bundle_modal_error.retry": "Försök igen",
   "column.blocks": "Blockerade användare",
   "column.community": "Lokal tidslinje",
-  "column.direct": "Direct messages",
-  "column.domain_blocks": "Hidden domains",
+  "column.direct": "Direktmeddelande",
+  "column.domain_blocks": "Dolda domäner",
   "column.favourites": "Favoriter",
   "column.follow_requests": "Följ förfrågningar",
   "column.home": "Hem",
@@ -59,7 +59,7 @@
   "column_header.unpin": "Ångra fäst",
   "column_subheading.navigation": "Navigation",
   "column_subheading.settings": "Inställningar",
-  "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning": "Denna toot kommer endast vara synlig för nämnda användare.",
   "compose_form.hashtag_warning": "Denna toot kommer inte att listas under någon hashtag eftersom den är onoterad. Endast offentliga toots kan sökas med hashtag.",
   "compose_form.lock_disclaimer": "Ditt konto är inte {locked}. Vemsomhelst kan följa dig och även se dina inlägg skrivna för endast dina följare.",
   "compose_form.lock_disclaimer.lock": "låst",
@@ -101,7 +101,7 @@
   "emoji_button.symbols": "Symboler",
   "emoji_button.travel": "Resor & Platser",
   "empty_column.community": "Den lokala tidslinjen är tom. Skriv något offentligt för att få bollen att rulla!",
-  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+  "empty_column.direct": "Du har inga direktmeddelanden än. När du skickar eller tar emot kommer den att dyka upp här.",
   "empty_column.hashtag": "Det finns inget i denna hashtag ännu.",
   "empty_column.home": "Din hemma-tidslinje är tom! Besök {public} eller använd sökning för att komma igång och träffa andra användare.",
   "empty_column.home.public_timeline": "den publika tidslinjen",
@@ -113,7 +113,7 @@
   "getting_started.appsshort": "Appar",
   "getting_started.faq": "FAQ",
   "getting_started.heading": "Kom igång",
-  "getting_started.open_source_notice": "Mastodon är programvara med öppen källkod. Du kan bidra eller rapportera problem på GitHub på {github}.",
+  "getting_started.open_source_notice": "Mastodon är programvara med öppen källkod. Du kan bidra eller rapportera problem via GitHub på {github}.",
   "getting_started.userguide": "Användarguide",
   "home.column_settings.advanced": "Avancerad",
   "home.column_settings.basic": "Grundläggande",
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "att nämna författaren",
   "keyboard_shortcuts.reply": "att svara",
   "keyboard_shortcuts.search": "att fokusera sökfältet",
+  "keyboard_shortcuts.toggle_hidden": "att visa/gömma text bakom CW",
   "keyboard_shortcuts.toot": "att börja en helt ny toot",
   "keyboard_shortcuts.unfocus": "att avfokusera komponera text fält / sökfält",
   "keyboard_shortcuts.up": "att flytta upp i listan",
@@ -156,8 +157,8 @@
   "mute_modal.hide_notifications": "Dölj notifikationer från denna användare?",
   "navigation_bar.blocks": "Blockerade användare",
   "navigation_bar.community_timeline": "Lokal tidslinje",
-  "navigation_bar.direct": "Direct messages",
-  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.direct": "Direktmeddelanden",
+  "navigation_bar.domain_blocks": "Dolda domäner",
   "navigation_bar.edit_profile": "Redigera profil",
   "navigation_bar.favourites": "Favoriter",
   "navigation_bar.follow_requests": "Följförfrågningar",
@@ -205,7 +206,7 @@
   "onboarding.page_three.search": "Använd sökfältet för att hitta personer och titta på hashtags, till exempel {illustration} och {introductions}. För att leta efter en person som inte befinner sig i detta fall använd deras fulla handhavande.",
   "onboarding.page_two.compose": "Skriv inlägg från skrivkolumnen. Du kan ladda upp bilder, ändra integritetsinställningar och lägga till varningar med ikonerna nedan.",
   "onboarding.skip": "Hoppa över",
-  "privacy.change": "Justera status sekretess",
+  "privacy.change": "Justera sekretess",
   "privacy.direct.long": "Skicka endast till nämnda användare",
   "privacy.direct.short": "Direkt",
   "privacy.private.long": "Skicka endast till följare",
@@ -241,10 +242,10 @@
   "search_results.total": "{count, number} {count, plural, ett {result} andra {results}}",
   "standalone.public_title": "En titt inuti...",
   "status.block": "Block @{name}",
-  "status.cancel_reblog_private": "Unboost",
+  "status.cancel_reblog_private": "Ta bort knuff",
   "status.cannot_reblog": "Detta inlägg kan inte knuffas",
   "status.delete": "Ta bort",
-  "status.direct": "Direct message @{name}",
+  "status.direct": "Direktmeddela @{name}",
   "status.embed": "Bädda in",
   "status.favourite": "Favorit",
   "status.load_more": "Ladda fler",
@@ -257,7 +258,7 @@
   "status.pin": "Fäst i profil",
   "status.pinned": "Fäst toot",
   "status.reblog": "Knuff",
-  "status.reblog_private": "Boost to original audience",
+  "status.reblog_private": "Knuffa till de ursprungliga åhörarna",
   "status.reblogged_by": "{name} knuffade",
   "status.reply": "Svara",
   "status.replyAll": "Svara på tråden",
@@ -275,7 +276,7 @@
   "tabs_bar.home": "Hem",
   "tabs_bar.local_timeline": "Lokal",
   "tabs_bar.notifications": "Meddelanden",
-  "tabs_bar.search": "Search",
+  "tabs_bar.search": "Sök",
   "ui.beforeunload": "Ditt utkast kommer att förloras om du lämnar Mastodon.",
   "upload_area.title": "Dra & släpp för att ladda upp",
   "upload_button.label": "Lägg till media",
diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json
new file mode 100644
index 000000000..a56720fee
--- /dev/null
+++ b/app/javascript/mastodon/locales/te.json
@@ -0,0 +1,296 @@
+{
+  "account.block": "Block @{name}",
+  "account.block_domain": "Hide everything from {domain}",
+  "account.blocked": "Blocked",
+  "account.direct": "Direct message @{name}",
+  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.domain_blocked": "Domain hidden",
+  "account.edit_profile": "Edit profile",
+  "account.follow": "Follow",
+  "account.followers": "Followers",
+  "account.follows": "Follows",
+  "account.follows_you": "Follows you",
+  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.media": "Media",
+  "account.mention": "Mention @{name}",
+  "account.moved_to": "{name} has moved to:",
+  "account.mute": "Mute @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.muted": "Muted",
+  "account.posts": "Toots",
+  "account.posts_with_replies": "Toots and replies",
+  "account.report": "Report @{name}",
+  "account.requested": "Awaiting approval. Click to cancel follow request",
+  "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
+  "account.unblock": "Unblock @{name}",
+  "account.unblock_domain": "Unhide {domain}",
+  "account.unfollow": "Unfollow",
+  "account.unmute": "Unmute @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.view_full_profile": "View full profile",
+  "alert.unexpected.message": "An unexpected error occurred.",
+  "alert.unexpected.title": "Oops!",
+  "boost_modal.combo": "You can press {combo} to skip this next time",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
+  "column.blocks": "Blocked users",
+  "column.community": "Local timeline",
+  "column.direct": "Direct messages",
+  "column.domain_blocks": "Hidden domains",
+  "column.favourites": "Favourites",
+  "column.follow_requests": "Follow requests",
+  "column.home": "Home",
+  "column.lists": "Lists",
+  "column.mutes": "Muted users",
+  "column.notifications": "Notifications",
+  "column.pins": "Pinned toot",
+  "column.public": "Federated timeline",
+  "column_back_button.label": "Back",
+  "column_header.hide_settings": "Hide settings",
+  "column_header.moveLeft_settings": "Move column to the left",
+  "column_header.moveRight_settings": "Move column to the right",
+  "column_header.pin": "Pin",
+  "column_header.show_settings": "Show settings",
+  "column_header.unpin": "Unpin",
+  "column_subheading.navigation": "Navigation",
+  "column_subheading.settings": "Settings",
+  "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
+  "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
+  "compose_form.lock_disclaimer.lock": "locked",
+  "compose_form.placeholder": "What is on your mind?",
+  "compose_form.publish": "Toot",
+  "compose_form.publish_loud": "{publish}!",
+  "compose_form.sensitive.marked": "Media is marked as sensitive",
+  "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
+  "compose_form.spoiler.marked": "Text is hidden behind warning",
+  "compose_form.spoiler.unmarked": "Text is not hidden",
+  "compose_form.spoiler_placeholder": "Write your warning here",
+  "confirmation_modal.cancel": "Cancel",
+  "confirmations.block.confirm": "Block",
+  "confirmations.block.message": "Are you sure you want to block {name}?",
+  "confirmations.delete.confirm": "Delete",
+  "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.domain_block.confirm": "Hide entire domain",
+  "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
+  "confirmations.mute.confirm": "Mute",
+  "confirmations.mute.message": "Are you sure you want to mute {name}?",
+  "confirmations.unfollow.confirm": "Unfollow",
+  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "embed.instructions": "Embed this status on your website by copying the code below.",
+  "embed.preview": "Here is what it will look like:",
+  "emoji_button.activity": "Activity",
+  "emoji_button.custom": "Custom",
+  "emoji_button.flags": "Flags",
+  "emoji_button.food": "Food & Drink",
+  "emoji_button.label": "Insert emoji",
+  "emoji_button.nature": "Nature",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.objects": "Objects",
+  "emoji_button.people": "People",
+  "emoji_button.recent": "Frequently used",
+  "emoji_button.search": "Search...",
+  "emoji_button.search_results": "Search results",
+  "emoji_button.symbols": "Symbols",
+  "emoji_button.travel": "Travel & Places",
+  "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
+  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+  "empty_column.hashtag": "There is nothing in this hashtag yet.",
+  "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
+  "empty_column.home.public_timeline": "the public timeline",
+  "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
+  "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
+  "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
+  "follow_request.authorize": "Authorize",
+  "follow_request.reject": "Reject",
+  "getting_started.appsshort": "Apps",
+  "getting_started.faq": "FAQ",
+  "getting_started.heading": "Getting started",
+  "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.",
+  "getting_started.userguide": "User Guide",
+  "home.column_settings.advanced": "Advanced",
+  "home.column_settings.basic": "Basic",
+  "home.column_settings.filter_regex": "Filter out by regular expressions",
+  "home.column_settings.show_reblogs": "Show boosts",
+  "home.column_settings.show_replies": "Show replies",
+  "home.settings": "Column settings",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
+  "lightbox.close": "Close",
+  "lightbox.next": "Next",
+  "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among people you follow",
+  "lists.subheading": "Your lists",
+  "loading_indicator.label": "Loading...",
+  "media_gallery.toggle_visible": "Toggle visibility",
+  "missing_indicator.label": "Not found",
+  "missing_indicator.sublabel": "This resource could not be found",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "navigation_bar.blocks": "Blocked users",
+  "navigation_bar.community_timeline": "Local timeline",
+  "navigation_bar.direct": "Direct messages",
+  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.edit_profile": "Edit profile",
+  "navigation_bar.favourites": "Favourites",
+  "navigation_bar.follow_requests": "Follow requests",
+  "navigation_bar.info": "Extended information",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
+  "navigation_bar.logout": "Logout",
+  "navigation_bar.mutes": "Muted users",
+  "navigation_bar.pins": "Pinned toots",
+  "navigation_bar.preferences": "Preferences",
+  "navigation_bar.public_timeline": "Federated timeline",
+  "notification.favourite": "{name} favourited your status",
+  "notification.follow": "{name} followed you",
+  "notification.mention": "{name} mentioned you",
+  "notification.reblog": "{name} boosted your status",
+  "notifications.clear": "Clear notifications",
+  "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
+  "notifications.column_settings.alert": "Desktop notifications",
+  "notifications.column_settings.favourite": "Favourites:",
+  "notifications.column_settings.follow": "New followers:",
+  "notifications.column_settings.mention": "Mentions:",
+  "notifications.column_settings.push": "Push notifications",
+  "notifications.column_settings.push_meta": "This device",
+  "notifications.column_settings.reblog": "Boosts:",
+  "notifications.column_settings.show": "Show in column",
+  "notifications.column_settings.sound": "Play sound",
+  "onboarding.done": "Done",
+  "onboarding.next": "Next",
+  "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
+  "onboarding.page_four.home": "The home timeline shows posts from people you follow.",
+  "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
+  "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
+  "onboarding.page_one.full_handle": "Your full handle",
+  "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.",
+  "onboarding.page_one.welcome": "Welcome to Mastodon!",
+  "onboarding.page_six.admin": "Your instance's admin is {admin}.",
+  "onboarding.page_six.almost_done": "Almost done...",
+  "onboarding.page_six.appetoot": "Bon Appetoot!",
+  "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
+  "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
+  "onboarding.page_six.guidelines": "community guidelines",
+  "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
+  "onboarding.page_six.various_app": "mobile apps",
+  "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
+  "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
+  "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
+  "onboarding.skip": "Skip",
+  "privacy.change": "Adjust status privacy",
+  "privacy.direct.long": "Post to mentioned users only",
+  "privacy.direct.short": "Direct",
+  "privacy.private.long": "Post to followers only",
+  "privacy.private.short": "Followers-only",
+  "privacy.public.long": "Post to public timelines",
+  "privacy.public.short": "Public",
+  "privacy.unlisted.long": "Do not show in public timelines",
+  "privacy.unlisted.short": "Unlisted",
+  "regeneration_indicator.label": "Loading…",
+  "regeneration_indicator.sublabel": "Your home feed is being prepared!",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
+  "reply_indicator.cancel": "Cancel",
+  "report.forward": "Forward to {target}",
+  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
+  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.placeholder": "Additional comments",
+  "report.submit": "Submit",
+  "report.target": "Report {target}",
+  "search.placeholder": "Search",
+  "search_popout.search_format": "Advanced search format",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
+  "search_popout.tips.hashtag": "hashtag",
+  "search_popout.tips.status": "status",
+  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+  "search_popout.tips.user": "user",
+  "search_results.accounts": "People",
+  "search_results.hashtags": "Hashtags",
+  "search_results.statuses": "Toots",
+  "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
+  "standalone.public_title": "A look inside...",
+  "status.block": "Block @{name}",
+  "status.cancel_reblog_private": "Unboost",
+  "status.cannot_reblog": "This post cannot be boosted",
+  "status.delete": "Delete",
+  "status.direct": "Direct message @{name}",
+  "status.embed": "Embed",
+  "status.favourite": "Favourite",
+  "status.load_more": "Load more",
+  "status.media_hidden": "Media hidden",
+  "status.mention": "Mention @{name}",
+  "status.more": "More",
+  "status.mute": "Mute @{name}",
+  "status.mute_conversation": "Mute conversation",
+  "status.open": "Expand this status",
+  "status.pin": "Pin on profile",
+  "status.pinned": "Pinned toot",
+  "status.reblog": "Boost",
+  "status.reblog_private": "Boost to original audience",
+  "status.reblogged_by": "{name} boosted",
+  "status.reply": "Reply",
+  "status.replyAll": "Reply to thread",
+  "status.report": "Report @{name}",
+  "status.sensitive_toggle": "Click to view",
+  "status.sensitive_warning": "Sensitive content",
+  "status.share": "Share",
+  "status.show_less": "Show less",
+  "status.show_less_all": "Show less for all",
+  "status.show_more": "Show more",
+  "status.show_more_all": "Show more for all",
+  "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
+  "tabs_bar.federated_timeline": "Federated",
+  "tabs_bar.home": "Home",
+  "tabs_bar.local_timeline": "Local",
+  "tabs_bar.notifications": "Notifications",
+  "tabs_bar.search": "Search",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "upload_area.title": "Drag & drop to upload",
+  "upload_button.label": "Add media",
+  "upload_form.description": "Describe for the visually impaired",
+  "upload_form.focus": "Crop",
+  "upload_form.undo": "Undo",
+  "upload_progress.label": "Uploading...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound"
+}
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 3b91c0d2c..82b44fe30 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "to mention author",
   "keyboard_shortcuts.reply": "to reply",
   "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "to start a brand new toot",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index cdf6f46a3..056fbfe8f 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "to mention author",
   "keyboard_shortcuts.reply": "to reply",
   "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "to start a brand new toot",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 261e5795e..1a7b58789 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "to mention author",
   "keyboard_shortcuts.reply": "to reply",
   "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "to start a brand new toot",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/locales/whitelist_el.json b/app/javascript/mastodon/locales/whitelist_el.json
new file mode 100644
index 000000000..0d4f101c7
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_el.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/app/javascript/mastodon/locales/whitelist_eu.json b/app/javascript/mastodon/locales/whitelist_eu.json
new file mode 100644
index 000000000..0d4f101c7
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_eu.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/app/javascript/mastodon/locales/whitelist_te.json b/app/javascript/mastodon/locales/whitelist_te.json
new file mode 100644
index 000000000..0d4f101c7
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_te.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index aba0bde83..a3a4de0af 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "提及嘟文作者",
   "keyboard_shortcuts.reply": "回复嘟文",
   "keyboard_shortcuts.search": "选择搜索框",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "发送新嘟文",
   "keyboard_shortcuts.unfocus": "取消输入",
   "keyboard_shortcuts.up": "在列表中让光标上移",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index b5ebd20fc..7719e08a6 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "提及作者",
   "keyboard_shortcuts.reply": "回覆",
   "keyboard_shortcuts.search": "把標示移動到搜索",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "新的推文",
   "keyboard_shortcuts.unfocus": "把標示移離文字輸入和搜索",
   "keyboard_shortcuts.up": "在列表往上移動",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 28d634600..84ff25e03 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -135,6 +135,7 @@
   "keyboard_shortcuts.mention": "到提到的作者",
   "keyboard_shortcuts.reply": "到回應",
   "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toot": "to start a brand new toot",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "to move up in the list",
diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js
index da9b8c420..84d4fc698 100644
--- a/app/javascript/mastodon/reducers/notifications.js
+++ b/app/javascript/mastodon/reducers/notifications.js
@@ -105,7 +105,7 @@ export default function notifications(state = initialState, action) {
     return expandNormalizedNotifications(state, action.notifications, action.next);
   case ACCOUNT_BLOCK_SUCCESS:
   case ACCOUNT_MUTE_SUCCESS:
-    return filterNotifications(state, action.relationship);
+    return action.relationship.muting_notifications ? filterNotifications(state, action.relationship) : state;
   case NOTIFICATIONS_CLEAR:
     return state.set('items', ImmutableList()).set('hasMore', false);
   case TIMELINE_DELETE:
diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js
index ad897bcc9..dd675d78f 100644
--- a/app/javascript/mastodon/reducers/timelines.js
+++ b/app/javascript/mastodon/reducers/timelines.js
@@ -34,7 +34,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial) =>
       mMap.update('items', ImmutableList(), oldIds => {
         const newIds = statuses.map(status => status.get('id'));
         const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;
-        const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) >= 0);
+        const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0);
 
         if (firstIndex < 0) {
           return (isPartial ? newIds.unshift(null) : newIds).concat(oldIds.skip(lastIndex));
diff --git a/app/javascript/mastodon/utils/__tests__/base64-test.js b/app/javascript/mastodon/utils/__tests__/base64-test.js
new file mode 100644
index 000000000..1b3260faa
--- /dev/null
+++ b/app/javascript/mastodon/utils/__tests__/base64-test.js
@@ -0,0 +1,10 @@
+import * as base64 from '../base64';
+
+describe('base64', () => {
+  describe('decode', () => {
+    it('returns a uint8 array', () => {
+      const arr = base64.decode('dGVzdA==');
+      expect(arr).toEqual(new Uint8Array([116, 101, 115, 116]));
+    });
+  });
+});
diff --git a/app/javascript/mastodon/utils/base64.js b/app/javascript/mastodon/utils/base64.js
new file mode 100644
index 000000000..8226e2c54
--- /dev/null
+++ b/app/javascript/mastodon/utils/base64.js
@@ -0,0 +1,10 @@
+export const decode = base64 => {
+  const rawData = window.atob(base64);
+  const outputArray = new Uint8Array(rawData.length);
+
+  for (let i = 0; i < rawData.length; ++i) {
+    outputArray[i] = rawData.charCodeAt(i);
+  }
+
+  return outputArray;
+};
diff --git a/app/javascript/mastodon/utils/resize_image.js b/app/javascript/mastodon/utils/resize_image.js
new file mode 100644
index 000000000..6442eda38
--- /dev/null
+++ b/app/javascript/mastodon/utils/resize_image.js
@@ -0,0 +1,66 @@
+const MAX_IMAGE_DIMENSION = 1280;
+
+const getImageUrl = inputFile => new Promise((resolve, reject) => {
+  if (window.URL && URL.createObjectURL) {
+    try {
+      resolve(URL.createObjectURL(inputFile));
+    } catch (error) {
+      reject(error);
+    }
+    return;
+  }
+
+  const reader = new FileReader();
+  reader.onerror = (...args) => reject(...args);
+  reader.onload  = ({ target }) => resolve(target.result);
+
+  reader.readAsDataURL(inputFile);
+});
+
+const loadImage = inputFile => new Promise((resolve, reject) => {
+  getImageUrl(inputFile).then(url => {
+    const img = new Image();
+
+    img.onerror = (...args) => reject(...args);
+    img.onload  = () => resolve(img);
+
+    img.src = url;
+  }).catch(reject);
+});
+
+export default inputFile => new Promise((resolve, reject) => {
+  if (!inputFile.type.match(/image.*/) || inputFile.type === 'image/gif') {
+    resolve(inputFile);
+    return;
+  }
+
+  loadImage(inputFile).then(img => {
+    const canvas = document.createElement('canvas');
+    const { width, height } = img;
+
+    let newWidth, newHeight;
+
+    if (width < MAX_IMAGE_DIMENSION && height < MAX_IMAGE_DIMENSION) {
+      resolve(inputFile);
+      return;
+    }
+
+    if (width > height) {
+      newHeight = height * MAX_IMAGE_DIMENSION / width;
+      newWidth  = MAX_IMAGE_DIMENSION;
+    } else if (height > width) {
+      newWidth  = width * MAX_IMAGE_DIMENSION / height;
+      newHeight = MAX_IMAGE_DIMENSION;
+    } else {
+      newWidth  = MAX_IMAGE_DIMENSION;
+      newHeight = MAX_IMAGE_DIMENSION;
+    }
+
+    canvas.width  = newWidth;
+    canvas.height = newHeight;
+
+    canvas.getContext('2d').drawImage(img, 0, 0, newWidth, newHeight);
+
+    canvas.toBlob(resolve, inputFile.type);
+  }).catch(reject);
+});
diff --git a/app/javascript/styles/contrast.scss b/app/javascript/styles/contrast.scss
new file mode 100644
index 000000000..5b43aecbe
--- /dev/null
+++ b/app/javascript/styles/contrast.scss
@@ -0,0 +1,3 @@
+@import 'contrast/variables';
+@import 'application';
+@import 'contrast/diff';
diff --git a/app/javascript/styles/contrast/diff.scss b/app/javascript/styles/contrast/diff.scss
new file mode 100644
index 000000000..eee9ecc3e
--- /dev/null
+++ b/app/javascript/styles/contrast/diff.scss
@@ -0,0 +1,14 @@
+// components.scss
+.compose-form {
+  .compose-form__modifiers {
+    .compose-form__upload {
+      &-description {
+        input {
+          &::placeholder {
+            opacity: 1.0;
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/app/javascript/styles/contrast/variables.scss b/app/javascript/styles/contrast/variables.scss
new file mode 100644
index 000000000..f6cadf029
--- /dev/null
+++ b/app/javascript/styles/contrast/variables.scss
@@ -0,0 +1,24 @@
+// Dependent colors
+$black: #000000;
+
+$classic-base-color: #282c37;
+$classic-primary-color: #9baec8;
+$classic-secondary-color: #d9e1e8;
+$classic-highlight-color: #2b90d9;
+
+$ui-base-color: $classic-base-color !default;
+$ui-primary-color: $classic-primary-color !default;
+$ui-secondary-color: $classic-secondary-color !default;
+
+// Differences
+$ui-highlight-color: #2b5fd9;
+
+$darker-text-color: lighten($ui-primary-color, 20%) !default;
+$dark-text-color: lighten($ui-primary-color, 12%) !default;
+$secondary-text-color: lighten($ui-secondary-color, 6%) !default;
+$highlight-text-color: $classic-highlight-color !default;
+$action-button-color: #8d9ac2;
+
+$inverted-text-color: $black !default;
+$lighter-text-color: darken($ui-base-color,6%) !default;
+$light-text-color: darken($ui-primary-color, 40%) !default;
diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss
index 0a09a38d2..c9c0e3081 100644
--- a/app/javascript/styles/mastodon/about.scss
+++ b/app/javascript/styles/mastodon/about.scss
@@ -225,7 +225,7 @@ $small-breakpoint: 960px;
     font-family: inherit;
     font-size: inherit;
     line-height: inherit;
-    color: transparentize($darker-text-color, 0.1);
+    color: lighten($darker-text-color, 10%);
   }
 
   h1 {
@@ -234,14 +234,14 @@ $small-breakpoint: 960px;
     line-height: 30px;
     font-weight: 500;
     margin-bottom: 20px;
-    color: $primary-text-color;
+    color: $secondary-text-color;
 
     small {
       font-family: 'mastodon-font-sans-serif', sans-serif;
       display: block;
       font-size: 18px;
       font-weight: 400;
-      color: opacify($darker-text-color, 0.1);
+      color: lighten($darker-text-color, 10%);
     }
   }
 
@@ -251,7 +251,7 @@ $small-breakpoint: 960px;
     line-height: 26px;
     font-weight: 500;
     margin-bottom: 20px;
-    color: $primary-text-color;
+    color: $secondary-text-color;
   }
 
   h3 {
@@ -260,7 +260,7 @@ $small-breakpoint: 960px;
     line-height: 24px;
     font-weight: 500;
     margin-bottom: 20px;
-    color: $primary-text-color;
+    color: $secondary-text-color;
   }
 
   h4 {
@@ -269,7 +269,7 @@ $small-breakpoint: 960px;
     line-height: 24px;
     font-weight: 500;
     margin-bottom: 20px;
-    color: $primary-text-color;
+    color: $secondary-text-color;
   }
 
   h5 {
@@ -278,7 +278,7 @@ $small-breakpoint: 960px;
     line-height: 24px;
     font-weight: 500;
     margin-bottom: 20px;
-    color: $primary-text-color;
+    color: $secondary-text-color;
   }
 
   h6 {
@@ -287,7 +287,7 @@ $small-breakpoint: 960px;
     line-height: 24px;
     font-weight: 500;
     margin-bottom: 20px;
-    color: $primary-text-color;
+    color: $secondary-text-color;
   }
 
   ul,
@@ -405,7 +405,7 @@ $small-breakpoint: 960px;
         font-size: 14px;
 
         &:hover {
-          color: $darker-text-color;
+          color: $secondary-text-color;
         }
       }
 
@@ -517,7 +517,7 @@ $small-breakpoint: 960px;
 
       span {
         &:last-child {
-          color: $darker-text-color;
+          color: $secondary-text-color;
         }
       }
 
@@ -559,7 +559,7 @@ $small-breakpoint: 960px;
         a,
         span {
           font-weight: 400;
-          color: opacify($darker-text-color, 0.1);
+          color: darken($darker-text-color, 10%);
         }
 
         a {
@@ -775,7 +775,7 @@ $small-breakpoint: 960px;
     }
 
     p a {
-      color: $darker-text-color;
+      color: $secondary-text-color;
     }
 
     h1 {
@@ -787,7 +787,7 @@ $small-breakpoint: 960px;
         color: $darker-text-color;
 
         span {
-          color: $darker-text-color;
+          color: $secondary-text-color;
         }
       }
     }
@@ -896,7 +896,7 @@ $small-breakpoint: 960px;
       }
 
       a {
-        color: $darker-text-color;
+        color: $secondary-text-color;
         text-decoration: none;
       }
     }
@@ -980,7 +980,7 @@ $small-breakpoint: 960px;
   .footer-links {
     padding-bottom: 50px;
     text-align: right;
-    color: $darker-text-color;
+    color: $dark-text-color;
 
     p {
       font-size: 14px;
@@ -995,7 +995,7 @@ $small-breakpoint: 960px;
   &__footer {
     margin-top: 10px;
     text-align: center;
-    color: $darker-text-color;
+    color: $dark-text-color;
 
     p {
       font-size: 14px;
diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss
index f9af6f288..c2d0de4b9 100644
--- a/app/javascript/styles/mastodon/accounts.scss
+++ b/app/javascript/styles/mastodon/accounts.scss
@@ -178,7 +178,7 @@
     font-size: 14px;
     line-height: 18px;
     padding: 0 15px;
-    color: $darker-text-color;
+    color: $secondary-text-color;
   }
 
   @media screen and (max-width: 480px) {
@@ -256,7 +256,7 @@
   .current {
     background: $simple-background-color;
     border-radius: 100px;
-    color: $lighter-text-color;
+    color: $inverted-text-color;
     cursor: default;
     margin: 0 10px;
   }
@@ -268,7 +268,7 @@
   .older,
   .newer {
     text-transform: uppercase;
-    color: $primary-text-color;
+    color: $secondary-text-color;
   }
 
   .older {
@@ -293,7 +293,7 @@
 
   .disabled {
     cursor: default;
-    color: opacify($lighter-text-color, 0.1);
+    color: lighten($inverted-text-color, 10%);
   }
 
   @media screen and (max-width: 700px) {
@@ -332,7 +332,7 @@
     width: 335px;
     background: $simple-background-color;
     border-radius: 4px;
-    color: $lighter-text-color;
+    color: $inverted-text-color;
     margin: 0 5px 10px;
     position: relative;
 
@@ -344,7 +344,7 @@
       overflow: hidden;
       height: 100px;
       border-radius: 4px 4px 0 0;
-      background-color: opacify($lighter-text-color, 0.04);
+      background-color: lighten($inverted-text-color, 4%);
       background-size: cover;
       background-position: center;
       position: relative;
@@ -422,7 +422,7 @@
     .account__header__content {
       padding: 10px 15px;
       padding-top: 15px;
-      color: transparentize($lighter-text-color, 0.1);
+      color: $lighter-text-color;
       word-wrap: break-word;
       overflow: hidden;
       text-overflow: ellipsis;
@@ -434,7 +434,7 @@
 .nothing-here {
   width: 100%;
   display: block;
-  color: $lighter-text-color;
+  color: $light-text-color;
   font-size: 14px;
   font-weight: 500;
   text-align: center;
@@ -493,7 +493,7 @@
 
       span {
         font-size: 14px;
-        color: $inverted-text-color;
+        color: $light-text-color;
       }
     }
 
@@ -508,7 +508,7 @@
 
   .account__header__content {
     font-size: 14px;
-    color: $darker-text-color;
+    color: $inverted-text-color;
   }
 }
 
@@ -586,7 +586,7 @@
     font-weight: 500;
     text-align: center;
     width: 94px;
-    color: opacify($darker-text-color, 0.1);
+    color: $secondary-text-color;
     background: rgba(darken($ui-base-color, 8%), 0.5);
   }
 
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 348f72078..a6cc8b62b 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -90,7 +90,7 @@
     padding-left: 25px;
 
     h2 {
-      color: $primary-text-color;
+      color: $secondary-text-color;
       font-size: 24px;
       line-height: 28px;
       font-weight: 400;
@@ -98,7 +98,7 @@
     }
 
     h3 {
-      color: $primary-text-color;
+      color: $secondary-text-color;
       font-size: 20px;
       line-height: 28px;
       font-weight: 400;
@@ -109,7 +109,7 @@
       text-transform: uppercase;
       font-size: 13px;
       font-weight: 500;
-      color: $primary-text-color;
+      color: $darker-text-color;
       padding-bottom: 8px;
       margin-bottom: 8px;
       border-bottom: 1px solid lighten($ui-base-color, 8%);
@@ -117,7 +117,7 @@
 
     h6 {
       font-size: 16px;
-      color: $primary-text-color;
+      color: $secondary-text-color;
       line-height: 28px;
       font-weight: 400;
     }
@@ -125,7 +125,7 @@
     & > p {
       font-size: 14px;
       line-height: 18px;
-      color: $darker-text-color;
+      color: $secondary-text-color;
       margin-bottom: 20px;
 
       strong {
@@ -141,14 +141,15 @@
     }
 
     hr {
-      margin: 20px 0;
+      width: 100%;
+      height: 0;
       border: 0;
-      background: transparent;
-      border-bottom: 1px solid $ui-base-color;
+      border-bottom: 1px solid rgba($ui-base-lighter-color, .6);
+      margin: 20px 0;
 
-      &.section-break {
-        margin: 30px 0;
-        border-bottom: 2px solid $ui-base-lighter-color;
+      &.spacer {
+        height: 1px;
+        border: 0;
       }
     }
 
@@ -291,7 +292,7 @@
     font-weight: 500;
     font-size: 14px;
     line-height: 18px;
-    color: $primary-text-color;
+    color: $secondary-text-color;
 
     @each $lang in $cjk-langs {
       &:lang(#{$lang}) {
@@ -335,34 +336,8 @@
   }
 }
 
-.report-note__comment {
-  margin-bottom: 20px;
-}
-
-.report-note__form {
-  margin-bottom: 20px;
-
-  .report-note__textarea {
-    box-sizing: border-box;
-    border: 0;
-    padding: 7px 4px;
-    margin-bottom: 10px;
-    font-size: 16px;
-    color: $inverted-text-color;
-    display: block;
-    width: 100%;
-    outline: 0;
-    font-family: inherit;
-    resize: vertical;
-  }
-
-  .report-note__buttons {
-    text-align: right;
-  }
-
-  .report-note__button {
-    margin: 0 0 5px 5px;
-  }
+.simple_form.new_report_note {
+  max-width: 100%;
 }
 
 .batch-form-box {
@@ -390,13 +365,6 @@
   }
 }
 
-.batch-checkbox,
-.batch-checkbox-all {
-  display: flex;
-  align-items: center;
-  margin-right: 5px;
-}
-
 .back-link {
   margin-bottom: 10px;
   font-size: 14px;
@@ -416,7 +384,7 @@
 }
 
 .log-entry {
-  margin-bottom: 8px;
+  margin-bottom: 20px;
   line-height: 20px;
 
   &__header {
@@ -452,7 +420,7 @@
   }
 
   &__timestamp {
-    color: $darker-text-color;
+    color: $dark-text-color;
   }
 
   &__extras {
@@ -469,7 +437,7 @@
   &__icon {
     font-size: 28px;
     margin-right: 10px;
-    color: $darker-text-color;
+    color: $dark-text-color;
   }
 
   &__icon__overlay {
@@ -496,7 +464,7 @@
   a,
   .username,
   .target {
-    color: $primary-text-color;
+    color: $secondary-text-color;
     text-decoration: none;
     font-weight: 500;
   }
@@ -506,7 +474,7 @@
   }
 
   .diff-neutral {
-    color: $darker-text-color;
+    color: $secondary-text-color;
   }
 
   .diff-new {
@@ -514,9 +482,12 @@
   }
 }
 
+a.name-tag,
 .name-tag {
   display: flex;
   align-items: center;
+  text-decoration: none;
+  color: $secondary-text-color;
 
   .avatar {
     display: block;
@@ -528,4 +499,52 @@
   .username {
     font-weight: 500;
   }
+
+  &.suspended {
+    .username {
+      text-decoration: line-through;
+      color: lighten($error-red, 12%);
+    }
+
+    .avatar {
+      filter: grayscale(100%);
+      opacity: 0.8;
+    }
+  }
+}
+
+.speech-bubble {
+  margin-bottom: 20px;
+  border-left: 4px solid $ui-highlight-color;
+
+  &.positive {
+    border-left-color: $success-green;
+  }
+
+  &.negative {
+    border-left-color: lighten($error-red, 12%);
+  }
+
+  &__bubble {
+    padding: 16px;
+    padding-left: 14px;
+    font-size: 15px;
+    line-height: 20px;
+    border-radius: 4px 4px 4px 0;
+    position: relative;
+    font-weight: 500;
+
+    a {
+      color: $darker-text-color;
+    }
+  }
+
+  &__owner {
+    padding: 8px;
+    padding-left: 12px;
+  }
+
+  time {
+    color: $dark-text-color;
+  }
 }
diff --git a/app/javascript/styles/mastodon/compact_header.scss b/app/javascript/styles/mastodon/compact_header.scss
index 83ac7a8d0..4980ab5f1 100644
--- a/app/javascript/styles/mastodon/compact_header.scss
+++ b/app/javascript/styles/mastodon/compact_header.scss
@@ -2,7 +2,7 @@
   h1 {
     font-size: 24px;
     line-height: 28px;
-    color: $primary-text-color;
+    color: $darker-text-color;
     font-weight: 500;
     margin-bottom: 20px;
     padding: 0 10px;
@@ -20,7 +20,7 @@
 
     small {
       font-weight: 400;
-      color: $darker-text-color;
+      color: $secondary-text-color;
     }
 
     img {
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index f0fde6666..a982585c3 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -31,7 +31,7 @@
   &:active,
   &:focus,
   &:hover {
-    background-color: lighten($ui-highlight-color, 4%);
+    background-color: lighten($ui-highlight-color, 10%);
     transition: all 200ms ease-out;
   }
 
@@ -83,7 +83,7 @@
   }
 
   &.button-secondary {
-    color: $ui-primary-color;
+    color: $darker-text-color;
     background: transparent;
     padding: 3px 15px;
     border: 1px solid $ui-primary-color;
@@ -92,7 +92,7 @@
     &:focus,
     &:hover {
       border-color: lighten($ui-primary-color, 4%);
-      color: lighten($ui-primary-color, 4%);
+      color: lighten($darker-text-color, 4%);
     }
   }
 
@@ -149,18 +149,18 @@
     &:hover,
     &:active,
     &:focus {
-      color: transparentize($lighter-text-color, 0.07);
+      color: darken($lighter-text-color, 7%);
     }
 
     &.disabled {
-      color: opacify($lighter-text-color, 0.07);
+      color: lighten($lighter-text-color, 7%);
     }
 
     &.active {
       color: $highlight-text-color;
 
       &.disabled {
-        color: opacify($lighter-text-color, 0.13);
+        color: lighten($highlight-text-color, 13%);
       }
     }
   }
@@ -193,12 +193,12 @@
   &:hover,
   &:active,
   &:focus {
-    color: opacify($lighter-text-color, 0.07);
+    color: darken($lighter-text-color, 7%);
     transition: color 200ms ease-out;
   }
 
   &.disabled {
-    color: transparentize($lighter-text-color, 0.2);
+    color: lighten($lighter-text-color, 20%);
     cursor: default;
   }
 
@@ -349,7 +349,7 @@
     box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
     background: $ui-secondary-color;
     border-radius: 0 0 4px 4px;
-    color: $lighter-text-color;
+    color: $inverted-text-color;
     font-size: 14px;
     padding: 6px;
 
@@ -457,7 +457,7 @@
 
         input {
           background: transparent;
-          color: $primary-text-color;
+          color: $secondary-text-color;
           border: 0;
           padding: 0;
           margin: 0;
@@ -471,8 +471,8 @@
           }
 
           &::placeholder {
-            opacity: 0.54;
-            color: $darker-text-color;
+            opacity: 0.75;
+            color: $secondary-text-color;
           }
         }
 
@@ -556,7 +556,6 @@
 }
 
 .emojione {
-  display: inline-block;
   font-size: inherit;
   vertical-align: middle;
   object-fit: contain;
@@ -588,7 +587,7 @@
 }
 
 .reply-indicator__display-name {
-  color: $lighter-text-color;
+  color: $inverted-text-color;
   display: block;
   max-width: 100%;
   line-height: 24px;
@@ -643,14 +642,14 @@
   }
 
   a {
-    color: $ui-secondary-color;
+    color: $secondary-text-color;
     text-decoration: none;
 
     &:hover {
       text-decoration: underline;
 
       .fa {
-        color: lighten($action-button-color, 7%);
+        color: lighten($dark-text-color, 7%);
       }
     }
 
@@ -665,7 +664,7 @@
     }
 
     .fa {
-      color: $action-button-color;
+      color: $dark-text-color;
     }
   }
 
@@ -702,7 +701,7 @@
   border-radius: 2px;
   background: transparent;
   border: 0;
-  color: $lighter-text-color;
+  color: $inverted-text-color;
   font-weight: 700;
   font-size: 11px;
   padding: 0 6px;
@@ -769,7 +768,7 @@
 
   &.light {
     .status__relative-time {
-      color: $lighter-text-color;
+      color: $light-text-color;
     }
 
     .status__display-name {
@@ -782,7 +781,7 @@
       }
 
       span {
-        color: $lighter-text-color;
+        color: $light-text-color;
       }
     }
 
@@ -816,13 +815,13 @@
 }
 
 .status__relative-time {
-  color: $darker-text-color;
+  color: $dark-text-color;
   float: right;
   font-size: 14px;
 }
 
 .status__display-name {
-  color: $darker-text-color;
+  color: $dark-text-color;
 }
 
 .status__info .status__display-name {
@@ -873,14 +872,14 @@
 
 .status__prepend {
   margin-left: 68px;
-  color: $darker-text-color;
+  color: $dark-text-color;
   padding: 8px 0;
   padding-bottom: 2px;
   font-size: 14px;
   position: relative;
 
   .status__display-name strong {
-    color: $darker-text-color;
+    color: $dark-text-color;
   }
 
   > span {
@@ -942,7 +941,7 @@
 
 .detailed-status__meta {
   margin-top: 15px;
-  color: $darker-text-color;
+  color: $dark-text-color;
   font-size: 14px;
   line-height: 18px;
 }
@@ -1006,6 +1005,15 @@
   padding: 10px;
   border-bottom: 1px solid lighten($ui-base-color, 8%);
 
+  &.compact {
+    padding: 0;
+    border-bottom: 0;
+
+    .account__avatar-wrapper {
+      margin-left: 0;
+    }
+  }
+
   .account__display-name {
     flex: 1 1 auto;
     display: block;
@@ -1029,7 +1037,6 @@
 .account__avatar {
   @include avatar-radius();
   position: relative;
-  cursor: pointer;
 
   &-inline {
     display: inline-block;
@@ -1038,6 +1045,10 @@
   }
 }
 
+a .account__avatar {
+  cursor: pointer;
+}
+
 .account__avatar-overlay {
   @include avatar-size(48px);
 
@@ -1079,7 +1090,7 @@
     }
 
     .account__header__username {
-      color: $darker-text-color;
+      color: $secondary-text-color;
     }
   }
 
@@ -1089,7 +1100,7 @@
   }
 
   .account__header__content {
-    color: $darker-text-color;
+    color: $secondary-text-color;
   }
 
   .account__header__display-name {
@@ -1117,7 +1128,7 @@
 .account__disclaimer {
   padding: 10px;
   border-top: 1px solid lighten($ui-base-color, 8%);
-  color: $darker-text-color;
+  color: $dark-text-color;
 
   strong {
     font-weight: 500;
@@ -1286,7 +1297,7 @@
 .status__display-name,
 .reply-indicator__display-name,
 .detailed-status__display-name,
-.account__display-name {
+a.account__display-name {
   &:hover strong {
     text-decoration: underline;
   }
@@ -1304,7 +1315,7 @@
 }
 
 .detailed-status__display-name {
-  color: $darker-text-color;
+  color: $secondary-text-color;
   display: block;
   line-height: 24px;
   margin-bottom: 15px;
@@ -1339,11 +1350,11 @@
 .muted {
   .status__content p,
   .status__content a {
-    color: $darker-text-color;
+    color: $dark-text-color;
   }
 
   .status__display-name strong {
-    color: $darker-text-color;
+    color: $dark-text-color;
   }
 
   .status__avatar {
@@ -1351,11 +1362,11 @@
   }
 
   a.status__content__spoiler-link {
-    background: $darker-text-color;
-    color: lighten($ui-base-color, 4%);
+    background: $ui-base-lighter-color;
+    color: $inverted-text-color;
 
     &:hover {
-      background: transparentize($darker-text-color, 0.07);
+      background: lighten($ui-base-lighter-color, 7%);
       text-decoration: none;
     }
   }
@@ -1366,7 +1377,7 @@
   padding: 8px 0;
   padding-bottom: 0;
   cursor: default;
-  color: $ui-primary-color;
+  color: $darker-text-color;
   font-size: 15px;
   position: relative;
 
@@ -1477,7 +1488,7 @@
   color: $darker-text-color;
 
   strong {
-    color: $primary-text-color;
+    color: $secondary-text-color;
   }
 
   a {
@@ -1591,7 +1602,7 @@
     &:hover,
     &:active {
       background: $ui-highlight-color;
-      color: $primary-text-color;
+      color: $secondary-text-color;
       outline: 0;
     }
   }
@@ -1644,7 +1655,7 @@
 
     &:hover {
       background: $ui-highlight-color;
-      color: $primary-text-color;
+      color: $secondary-text-color;
     }
   }
 }
@@ -1656,7 +1667,7 @@
 .static-content {
   padding: 10px;
   padding-top: 20px;
-  color: $darker-text-color;
+  color: $dark-text-color;
 
   h1 {
     font-size: 16px;
@@ -1743,7 +1754,7 @@
   display: block;
   flex: 1 1 auto;
   padding: 15px 5px 13px;
-  color: $ui-primary-color;
+  color: $darker-text-color;
   text-decoration: none;
   text-align: center;
   font-size: 16px;
@@ -2155,7 +2166,7 @@
 
 .column-subheading {
   background: $ui-base-color;
-  color: $darker-text-color;
+  color: $dark-text-color;
   padding: 8px 20px;
   font-size: 12px;
   font-weight: 500;
@@ -2178,11 +2189,11 @@
   flex: 1 0 auto;
 
   p {
-    color: $darker-text-color;
+    color: $secondary-text-color;
   }
 
   a {
-    color: opacify($darker-text-color, 0.07);
+    color: $dark-text-color;
   }
 }
 
@@ -2263,7 +2274,7 @@
   font-size: 14px;
   border: 1px solid lighten($ui-base-color, 8%);
   border-radius: 4px;
-  color: $darker-text-color;
+  color: $dark-text-color;
   margin-top: 14px;
   text-decoration: none;
   overflow: hidden;
@@ -2343,7 +2354,7 @@ a.status-card {
   display: block;
   font-weight: 500;
   margin-bottom: 5px;
-  color: $ui-primary-color;
+  color: $darker-text-color;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
@@ -2357,7 +2368,7 @@ a.status-card {
 }
 
 .status-card__description {
-  color: $ui-primary-color;
+  color: $darker-text-color;
 }
 
 .status-card__host {
@@ -2401,7 +2412,7 @@ a.status-card {
 
 .load-more {
   display: block;
-  color: $darker-text-color;
+  color: $dark-text-color;
   background-color: transparent;
   border: 0;
   font-size: inherit;
@@ -2425,7 +2436,7 @@ a.status-card {
   text-align: center;
   font-size: 16px;
   font-weight: 500;
-  color: opacify($darker-text-color, 0.07);
+  color: $dark-text-color;
   background: $ui-base-color;
   cursor: default;
   display: flex;
@@ -2465,7 +2476,7 @@ a.status-card {
     strong {
       display: block;
       margin-bottom: 10px;
-      color: $darker-text-color;
+      color: $dark-text-color;
     }
 
     span {
@@ -2553,13 +2564,13 @@ a.status-card {
 .column-header__button {
   background: lighten($ui-base-color, 4%);
   border: 0;
-  color: $ui-primary-color;
+  color: $darker-text-color;
   cursor: pointer;
   font-size: 16px;
   padding: 0 15px;
 
   &:hover {
-    color: lighten($ui-primary-color, 7%);
+    color: lighten($darker-text-color, 7%);
   }
 
   &.active {
@@ -2640,7 +2651,7 @@ a.status-card {
 }
 
 .loading-indicator {
-  color: $darker-text-color;
+  color: $dark-text-color;
   font-size: 12px;
   font-weight: 400;
   text-transform: uppercase;
@@ -2737,7 +2748,7 @@ a.status-card {
   &:active,
   &:focus {
     padding: 0;
-    color: transparentize($darker-text-color, 0.07);
+    color: lighten($darker-text-color, 8%);
   }
 }
 
@@ -2861,7 +2872,7 @@ a.status-card {
 
 .empty-column-indicator,
 .error-column {
-  color: $darker-text-color;
+  color: $dark-text-color;
   background: $ui-base-color;
   text-align: center;
   padding: 20px;
@@ -3063,7 +3074,7 @@ a.status-card {
   display: flex;
   align-items: center;
   justify-content: center;
-  color: $primary-text-color;
+  color: $secondary-text-color;
   font-size: 18px;
   font-weight: 500;
   border: 2px dashed $ui-base-lighter-color;
@@ -3161,7 +3172,7 @@ a.status-card {
 }
 
 .privacy-dropdown__option {
-  color: $lighter-text-color;
+  color: $inverted-text-color;
   padding: 10px;
   cursor: pointer;
   display: flex;
@@ -3283,7 +3294,7 @@ a.status-card {
     font-size: 18px;
     width: 18px;
     height: 18px;
-    color: $ui-secondary-color;
+    color: $secondary-text-color;
     cursor: default;
     pointer-events: none;
 
@@ -3319,7 +3330,7 @@ a.status-card {
 }
 
 .search-results__header {
-  color: $darker-text-color;
+  color: $dark-text-color;
   background: lighten($ui-base-color, 2%);
   border-bottom: 1px solid darken($ui-base-color, 4%);
   padding: 15px 10px;
@@ -3367,13 +3378,13 @@ a.status-card {
 .search-results__hashtag {
   display: block;
   padding: 10px;
-  color: darken($primary-text-color, 4%);
+  color: $secondary-text-color;
   text-decoration: none;
 
   &:hover,
   &:active,
   &:focus {
-    color: $primary-text-color;
+    color: lighten($secondary-text-color, 4%);
     text-decoration: underline;
   }
 }
@@ -3638,7 +3649,7 @@ a.status-card {
     &:hover,
     &:focus,
     &:active {
-      color: transparentize($lighter-text-color, 0.04);
+      color: darken($lighter-text-color, 4%);
       background-color: darken($ui-secondary-color, 16%);
     }
 
@@ -3732,7 +3743,7 @@ a.status-card {
     strong {
       font-weight: 500;
       background: $ui-base-color;
-      color: $primary-text-color;
+      color: $secondary-text-color;
       border-radius: 4px;
       font-size: 14px;
       padding: 3px 6px;
@@ -3792,7 +3803,7 @@ a.status-card {
 
   &__case {
     background: $ui-base-color;
-    color: $primary-text-color;
+    color: $secondary-text-color;
     font-weight: 500;
     padding: 10px;
     border-radius: 4px;
@@ -3809,7 +3820,7 @@ a.status-card {
 
   .figure {
     background: darken($ui-base-color, 8%);
-    color: $darker-text-color;
+    color: $secondary-text-color;
     margin-bottom: 20px;
     border-radius: 4px;
     padding: 10px;
@@ -3921,7 +3932,7 @@ a.status-card {
   }
 
   .status__content__spoiler-link {
-    color: lighten($ui-secondary-color, 8%);
+    color: lighten($secondary-text-color, 8%);
   }
 }
 
@@ -4026,6 +4037,10 @@ a.status-card {
   overflow-y: auto;
   overflow-x: hidden;
 
+  .status__content a {
+    color: $highlight-text-color;
+  }
+
   @media screen and (max-width: 480px) {
     max-height: 10vh;
   }
@@ -4151,7 +4166,7 @@ a.status-card {
     &:hover,
     &:focus,
     &:active {
-      color: transparentize($lighter-text-color, 0.04);
+      color: darken($lighter-text-color, 4%);
     }
   }
 }
@@ -4232,7 +4247,7 @@ a.status-card {
 
   &__icon {
     flex: 0 0 auto;
-    color: $darker-text-color;
+    color: $dark-text-color;
     padding: 8px 18px;
     cursor: default;
     border-right: 1px solid lighten($ui-base-color, 8%);
@@ -4262,7 +4277,7 @@ a.status-card {
 
     a {
       text-decoration: none;
-      color: $darker-text-color;
+      color: $dark-text-color;
       font-weight: 500;
 
       &:hover {
@@ -4281,7 +4296,7 @@ a.status-card {
     }
 
     .fa {
-      color: $darker-text-color;
+      color: $dark-text-color;
     }
   }
 }
@@ -4317,7 +4332,7 @@ a.status-card {
   cursor: zoom-in;
   display: block;
   text-decoration: none;
-  color: $ui-secondary-color;
+  color: $secondary-text-color;
   line-height: 0;
 
   &,
@@ -4431,6 +4446,8 @@ a.status-card {
     video {
       max-width: 100% !important;
       max-height: 100% !important;
+      width: 100% !important;
+      height: 100% !important;
     }
   }
 
@@ -4488,7 +4505,7 @@ a.status-card {
       &:hover,
       &:active,
       &:focus {
-        color: transparentize($darker-text-color, 0.07);
+        color: lighten($darker-text-color, 7%);
       }
     }
 
@@ -4693,7 +4710,7 @@ a.status-card {
     &:active,
     &:focus {
       outline: 0;
-      color: transparentize($darker-text-color, 0.07);
+      color: $secondary-text-color;
 
       &::before {
         content: "";
@@ -4733,7 +4750,7 @@ a.status-card {
     position: relative;
 
     &.active {
-      color: transparentize($darker-text-color, 0.07);
+      color: $secondary-text-color;
 
       &::before,
       &::after {
@@ -4768,12 +4785,12 @@ a.status-card {
   padding: 10px 14px;
   padding-bottom: 14px;
   margin-top: 10px;
-  color: $lighter-text-color;
+  color: $light-text-color;
   box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
 
   h4 {
     text-transform: uppercase;
-    color: $lighter-text-color;
+    color: $light-text-color;
     font-size: 13px;
     font-weight: 500;
     margin-bottom: 10px;
@@ -4805,7 +4822,7 @@ noscript {
   div {
     font-size: 14px;
     margin: 30px auto;
-    color: $primary-text-color;
+    color: $secondary-text-color;
     max-width: 400px;
 
     a {
@@ -4958,7 +4975,7 @@ noscript {
   &__message {
     position: relative;
     margin-left: 58px;
-    color: $darker-text-color;
+    color: $dark-text-color;
     padding: 8px 0;
     padding-top: 0;
     padding-bottom: 4px;
diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss
index 8df2902d2..9d5ab66a4 100644
--- a/app/javascript/styles/mastodon/containers.scss
+++ b/app/javascript/styles/mastodon/containers.scss
@@ -100,7 +100,7 @@
 
   .name {
     flex: 1 1 auto;
-    color: $darker-text-color;
+    color: $secondary-text-color;
     width: calc(100% - 88px);
 
     .username {
diff --git a/app/javascript/styles/mastodon/emoji_picker.scss b/app/javascript/styles/mastodon/emoji_picker.scss
index 3620a6f54..cf9547586 100644
--- a/app/javascript/styles/mastodon/emoji_picker.scss
+++ b/app/javascript/styles/mastodon/emoji_picker.scss
@@ -50,7 +50,7 @@
   cursor: pointer;
 
   &:hover {
-    color: opacify($lighter-text-color, 0.04);
+    color: darken($lighter-text-color, 4%);
   }
 }
 
@@ -184,7 +184,7 @@
   font-size: 14px;
   text-align: center;
   padding-top: 70px;
-  color: $lighter-text-color;
+  color: $light-text-color;
 
   .emoji-mart-category-label {
     display: none;
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 3a3b4c326..f97890187 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -248,7 +248,7 @@ code {
     }
 
     &:required:valid {
-      border-bottom-color: lighten($error-red, 12%);
+      border-bottom-color: $valid-value-color;
     }
 
     &:active,
@@ -266,7 +266,7 @@ code {
     input[type=text],
     input[type=email],
     input[type=password] {
-      border-bottom-color: lighten($error-red, 12%);
+      border-bottom-color: $valid-value-color;
     }
 
     .error {
@@ -356,7 +356,7 @@ code {
       padding: 7px 4px;
       padding-bottom: 9px;
       font-size: 16px;
-      color: $darker-text-color;
+      color: $dark-text-color;
       font-family: inherit;
       pointer-events: none;
       cursor: default;
@@ -446,7 +446,7 @@ code {
   }
 
   strong {
-    color: $primary-text-color;
+    color: $secondary-text-color;
     font-weight: 500;
 
     @each $lang in $cjk-langs {
@@ -483,7 +483,7 @@ code {
 
 .qr-alternative {
   margin-bottom: 20px;
-  color: $darker-text-color;
+  color: $secondary-text-color;
   flex: 150px;
 
   samp {
diff --git a/app/javascript/styles/mastodon/landing_strip.scss b/app/javascript/styles/mastodon/landing_strip.scss
index 651c06ced..86614b89b 100644
--- a/app/javascript/styles/mastodon/landing_strip.scss
+++ b/app/javascript/styles/mastodon/landing_strip.scss
@@ -45,7 +45,7 @@
   padding: 14px;
   border-radius: 4px;
   background: rgba(darken($ui-base-color, 7%), 0.8);
-  color: $darker-text-color;
+  color: $secondary-text-color;
   font-weight: 400;
   margin-bottom: 20px;
 
diff --git a/app/javascript/styles/mastodon/stream_entries.scss b/app/javascript/styles/mastodon/stream_entries.scss
index c39163ba8..281cbaf83 100644
--- a/app/javascript/styles/mastodon/stream_entries.scss
+++ b/app/javascript/styles/mastodon/stream_entries.scss
@@ -93,7 +93,7 @@
       display: block;
       max-width: 100%;
       padding-right: 25px;
-      color: $lighter-text-color;
+      color: $inverted-text-color;
     }
 
     .status__avatar {
@@ -134,7 +134,7 @@
 
       span {
         font-size: 14px;
-        color: $inverted-text-color;
+        color: $light-text-color;
       }
     }
 
@@ -191,7 +191,7 @@
 
         span {
           font-size: 14px;
-          color: $lighter-text-color;
+          color: $light-text-color;
         }
       }
     }
@@ -225,7 +225,7 @@
 
     .detailed-status__meta {
       margin-top: 15px;
-      color: $lighter-text-color;
+      color: $light-text-color;
       font-size: 14px;
       line-height: 18px;
 
@@ -270,7 +270,7 @@
     padding-left: (48px + 14px * 2);
     padding-bottom: 0;
     margin-bottom: -4px;
-    color: $lighter-text-color;
+    color: $light-text-color;
     font-size: 14px;
     position: relative;
 
@@ -280,7 +280,7 @@
     }
 
     .status__display-name.muted strong {
-      color: $lighter-text-color;
+      color: $light-text-color;
     }
   }
 
@@ -293,7 +293,7 @@
   }
 
   .more {
-    color: $classic-primary-color;
+    color: $darker-text-color;
     display: block;
     padding: 14px;
     text-align: center;
diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index c12d84f1c..fa876e603 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -11,6 +11,7 @@
     vertical-align: top;
     border-top: 1px solid $ui-base-color;
     text-align: left;
+    background: darken($ui-base-color, 4%);
   }
 
   & > thead > tr > th {
@@ -48,9 +49,38 @@
     }
   }
 
-  &.inline-table > tbody > tr:nth-child(odd) > td,
-  &.inline-table > tbody > tr:nth-child(odd) > th {
-    background: transparent;
+  &.inline-table {
+    & > tbody > tr:nth-child(odd) {
+      & > td,
+      & > th {
+        background: transparent;
+      }
+    }
+
+    & > tbody > tr:first-child {
+      & > td,
+      & > th {
+        border-top: 0;
+      }
+    }
+  }
+
+  &.batch-table {
+    & > thead > tr > th {
+      background: $ui-base-color;
+      border-top: 1px solid darken($ui-base-color, 8%);
+      border-bottom: 1px solid darken($ui-base-color, 8%);
+
+      &:first-child {
+        border-radius: 4px 0 0;
+        border-left: 1px solid darken($ui-base-color, 8%);
+      }
+
+      &:last-child {
+        border-radius: 0 4px 0 0;
+        border-right: 1px solid darken($ui-base-color, 8%);
+      }
+    }
   }
 }
 
@@ -63,6 +93,13 @@ samp {
   font-family: 'mastodon-font-monospace', monospace;
 }
 
+button.table-action-link {
+  background: transparent;
+  border: 0;
+  font: inherit;
+}
+
+button.table-action-link,
 a.table-action-link {
   text-decoration: none;
   display: inline-block;
@@ -79,4 +116,77 @@ a.table-action-link {
     font-weight: 400;
     margin-right: 5px;
   }
+
+  &:first-child {
+    padding-left: 0;
+  }
+}
+
+.batch-table {
+  &__toolbar,
+  &__row {
+    display: flex;
+
+    &__select {
+      box-sizing: border-box;
+      padding: 8px 16px;
+      cursor: pointer;
+      min-height: 100%;
+
+      input {
+        margin-top: 8px;
+      }
+    }
+
+    &__actions,
+    &__content {
+      padding: 8px 0;
+      padding-right: 16px;
+      flex: 1 1 auto;
+    }
+  }
+
+  &__toolbar {
+    border: 1px solid darken($ui-base-color, 8%);
+    background: $ui-base-color;
+    border-radius: 4px 0 0;
+    height: 47px;
+    align-items: center;
+
+    &__actions {
+      text-align: right;
+      padding-right: 16px - 5px;
+    }
+  }
+
+  &__row {
+    border: 1px solid darken($ui-base-color, 8%);
+    border-top: 0;
+    background: darken($ui-base-color, 4%);
+
+    &:hover {
+      background: darken($ui-base-color, 2%);
+    }
+
+    &:nth-child(even) {
+      background: $ui-base-color;
+
+      &:hover {
+        background: lighten($ui-base-color, 2%);
+      }
+    }
+
+    &__content {
+      padding-top: 12px;
+      padding-bottom: 16px;
+    }
+  }
+
+  .status__content {
+    padding-top: 0;
+
+    strong {
+      font-weight: 700;
+    }
+  }
 }
diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/variables.scss
index dc4e72a2e..cbefe35b4 100644
--- a/app/javascript/styles/mastodon/variables.scss
+++ b/app/javascript/styles/mastodon/variables.scss
@@ -17,12 +17,6 @@ $base-shadow-color: $black !default;
 $base-overlay-background: $black !default;
 $base-border-color: $white !default;
 $simple-background-color: $white !default;
-$primary-text-color: $white !default;
-$darker-text-color: rgba($primary-text-color, 0.7) !default;
-$highlight-text-color: $classic-highlight-color !default;
-$inverted-text-color: $black !default;
-$lighter-text-color: rgba($inverted-text-color, 0.7) !default;
-$action-button-color: #8d9ac2;
 $valid-value-color: $success-green !default;
 $error-value-color: $error-red !default;
 
@@ -31,7 +25,19 @@ $ui-base-color: $classic-base-color !default;                  // Darkest
 $ui-base-lighter-color: lighten($ui-base-color, 26%) !default; // Lighter darkest
 $ui-primary-color: $classic-primary-color !default;            // Lighter
 $ui-secondary-color: $classic-secondary-color !default;        // Lightest
-$ui-highlight-color: #2b5fd9;
+$ui-highlight-color: $classic-highlight-color !default;
+
+// Variables for texts
+$primary-text-color: $white !default;
+$darker-text-color: $ui-primary-color !default;
+$dark-text-color: $ui-base-lighter-color !default;
+$secondary-text-color: $ui-secondary-color !default;
+$highlight-text-color: $ui-highlight-color !default;
+$action-button-color: $ui-base-lighter-color !default;
+// For texts on inverted backgrounds
+$inverted-text-color: $ui-base-color !default;
+$lighter-text-color: $ui-base-lighter-color !default;
+$light-text-color: $ui-primary-color !default;
 
 // Language codes that uses CJK fonts
 $cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 9b00f0f52..5b97a6208 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -80,7 +80,7 @@ class ActivityPub::Activity
 
     # Only continue if the status is supposed to have
     # arrived in real-time
-    return unless @options[:override_timestamps] || status.within_realtime_window?
+    return unless status.within_realtime_window?
 
     distribute_to_followers(status)
   end
diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb
index c8a358195..8840a450c 100644
--- a/app/lib/activitypub/activity/announce.rb
+++ b/app/lib/activitypub/activity/announce.rb
@@ -15,7 +15,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
       account: @account,
       reblog: original_status,
       uri: @json['id'],
-      created_at: @options[:override_timestamps] ? nil : @json['published'],
+      created_at: @json['published'],
       visibility: original_status.visibility
     )
 
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index 45c0e91cb..edee2691f 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -47,7 +47,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
       text: text_from_content || '',
       language: detected_language,
       spoiler_text: @object['summary'] || '',
-      created_at: @options[:override_timestamps] ? nil : @object['published'],
+      created_at: @object['published'],
       reply: @object['inReplyTo'].present?,
       sensitive: @object['sensitive'] || false,
       visibility: visibility_from_audience,
@@ -61,12 +61,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     return if @object['tag'].nil?
 
     as_array(@object['tag']).each do |tag|
-      case tag['type']
-      when 'Hashtag'
+      if equals_or_includes?(tag['type'], 'Hashtag')
         process_hashtag tag, status
-      when 'Mention'
+      elsif equals_or_includes?(tag['type'], 'Mention')
         process_mention tag, status
-      when 'Emoji'
+      elsif equals_or_includes?(tag['type'], 'Emoji')
         process_emoji tag, status
       end
     end
@@ -235,11 +234,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
   end
 
   def supported_object_type?
-    SUPPORTED_TYPES.include?(@object['type'])
+    equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
   end
 
   def converted_object_type?
-    CONVERTED_TYPES.include?(@object['type'])
+    equals_or_includes_any?(@object['type'], CONVERTED_TYPES)
   end
 
   def skip_download?
diff --git a/app/lib/activitypub/activity/update.rb b/app/lib/activitypub/activity/update.rb
index 0134b4015..aa5907f03 100644
--- a/app/lib/activitypub/activity/update.rb
+++ b/app/lib/activitypub/activity/update.rb
@@ -1,11 +1,10 @@
 # frozen_string_literal: true
 
 class ActivityPub::Activity::Update < ActivityPub::Activity
+  SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
+
   def perform
-    case @object['type']
-    when 'Person'
-      update_account
-    end
+    update_account if equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
   end
 
   private
diff --git a/app/lib/entity_cache.rb b/app/lib/entity_cache.rb
new file mode 100644
index 000000000..2aa37389c
--- /dev/null
+++ b/app/lib/entity_cache.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'singleton'
+
+class EntityCache
+  include Singleton
+
+  MAX_EXPIRATION = 7.days.freeze
+
+  def mention(username, domain)
+    Rails.cache.fetch(to_key(:mention, username, domain), expires_in: MAX_EXPIRATION) { Account.select(:username, :domain, :url).find_remote(username, domain) }
+  end
+
+  def emoji(shortcodes, domain)
+    shortcodes   = [shortcodes] unless shortcodes.is_a?(Array)
+    cached       = Rails.cache.read_multi(*shortcodes.map { |shortcode| to_key(:emoji, shortcode, domain) })
+    uncached_ids = []
+
+    shortcodes.each do |shortcode|
+      uncached_ids << shortcode unless cached.key?(to_key(:emoji, shortcode, domain))
+    end
+
+    unless uncached_ids.empty?
+      uncached = CustomEmoji.where(shortcode: shortcodes, domain: domain, disabled: false).map { |item| [item.shortcode, item] }.to_h
+      uncached.each_value { |item| Rails.cache.write(to_key(:emoji, item.shortcode, domain), item, expires_in: MAX_EXPIRATION) }
+    end
+
+    shortcodes.map { |shortcode| cached[to_key(:emoji, shortcode, domain)] || uncached[shortcode] }.compact
+  end
+
+  def to_key(type, *ids)
+    "#{type}:#{ids.compact.map(&:downcase).join(':')}"
+  end
+end
diff --git a/app/lib/exceptions.rb b/app/lib/exceptions.rb
index e88e98eae..01346bfe5 100644
--- a/app/lib/exceptions.rb
+++ b/app/lib/exceptions.rb
@@ -6,6 +6,7 @@ module Mastodon
   class ValidationError < Error; end
   class HostValidationError < ValidationError; end
   class LengthValidationError < ValidationError; end
+  class DimensionsValidationError < ValidationError; end
   class RaceConditionError < Error; end
 
   class UnexpectedResponseError < Error
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 700fd61c4..3a2dcac68 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -145,10 +145,14 @@ class FeedManager
     redis.exists("subscribed:#{timeline_id}")
   end
 
+  def blocks_or_mutes?(receiver_id, account_ids, context)
+    Block.where(account_id: receiver_id, target_account_id: account_ids).any? ||
+      (context == :home ? Mute.where(account_id: receiver_id, target_account_id: account_ids).any? : Mute.where(account_id: receiver_id, target_account_id: account_ids, hide_notifications: true).any?)
+  end
+
   def filter_from_home?(status, receiver_id)
     return false if receiver_id == status.account_id
     return true  if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
-
     return true if keyword_filter?(status, receiver_id)
 
     check_for_mutes = [status.account_id]
@@ -158,9 +162,10 @@ class FeedManager
     return true if Mute.where(account_id: receiver_id, target_account_id: check_for_mutes).any?
 
     check_for_blocks = status.mentions.pluck(:account_id)
+    check_for_blocks.concat([status.account_id])
     check_for_blocks.concat([status.reblog.account_id]) if status.reblog?
 
-    return true if Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any?
+    return true if blocks_or_mutes?(receiver_id, check_for_blocks, :home)
 
     if status.reply? && !status.in_reply_to_account_id.nil?                                                                      # Filter out if it's a reply
       should_filter   = !Follow.where(account_id: receiver_id, target_account_id: status.in_reply_to_account_id).exists?         # and I'm not following the person it's a reply to
@@ -184,11 +189,13 @@ class FeedManager
   def filter_from_mentions?(status, receiver_id)
     return true if receiver_id == status.account_id
 
-    check_for_blocks = [status.account_id]
-    check_for_blocks.concat(status.mentions.pluck(:account_id))
+    # This filter is called from NotifyService, but already after the sender of
+    # the notification has been checked for mute/block. Therefore, it's not
+    # necessary to check the author of the toot for mute/block again
+    check_for_blocks = status.mentions.pluck(:account_id)
     check_for_blocks.concat([status.in_reply_to_account]) if status.reply? && !status.in_reply_to_account_id.nil?
 
-    should_filter   = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any?                                     # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked
+    should_filter   = blocks_or_mutes?(receiver_id, check_for_blocks, :mentions)                                                         # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked (or muted)
     should_filter ||= (status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists?) # of if the account is silenced and I'm not following them
     should_filter ||= keyword_filter?(status, receiver_id)                                                                               # or if the mention contains a muted keyword
 
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index 4124f1660..050c651ee 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -52,12 +52,8 @@ class Formatter
   end
 
   def simplified_format(account, **options)
-    html = if account.local?
-             linkify(account.note)
-           else
-             reformat(account.note)
-           end
-    html = encode_custom_emojis(html, CustomEmoji.from_text(account.note, account.domain)) if options[:custom_emojify]
+    html = account.local? ? linkify(account.note) : reformat(account.note)
+    html = encode_custom_emojis(html, account.emojis) if options[:custom_emojify]
     html.html_safe # rubocop:disable Rails/OutputSafety
   end
 
@@ -211,7 +207,7 @@ class Formatter
     username, domain = acct.split('@')
 
     domain  = nil if TagManager.instance.local_domain?(domain)
-    account = Account.find_remote(username, domain)
+    account = EntityCache.instance.mention(username, domain)
 
     account ? mention_html(account) : "@#{acct}"
   end
diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb
index 6235127b2..a24a0093c 100644
--- a/app/lib/ostatus/activity/creation.rb
+++ b/app/lib/ostatus/activity/creation.rb
@@ -39,7 +39,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
         reblog: cached_reblog,
         text: content,
         spoiler_text: content_warning,
-        created_at: @options[:override_timestamps] ? nil : published,
+        created_at: published,
         reply: thread?,
         language: content_language,
         visibility: visibility_scope,
@@ -61,7 +61,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
     Rails.logger.debug "Queuing remote status #{status.id} (#{id}) for distribution"
 
     LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text?
-    DistributionWorker.perform_async(status.id) if @options[:override_timestamps] || status.within_realtime_window?
+    DistributionWorker.perform_async(status.id) if status.within_realtime_window?
 
     status
   end
diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb
index 055b4649c..7c66f2066 100644
--- a/app/lib/ostatus/atom_serializer.rb
+++ b/app/lib/ostatus/atom_serializer.rb
@@ -364,8 +364,6 @@ class OStatus::AtomSerializer
       append_element(entry, 'category', nil, term: tag.name)
     end
 
-    append_element(entry, 'category', nil, term: 'nsfw') if status.sensitive?
-
     status.media_attachments.each do |media|
       append_element(entry, 'link', nil, rel: :enclosure, type: media.file_content_type, length: media.file_file_size, href: full_asset_url(media.file.url(:original, false)))
     end
diff --git a/app/lib/provider_discovery.rb b/app/lib/provider_discovery.rb
deleted file mode 100644
index 3bec7211b..000000000
--- a/app/lib/provider_discovery.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-class ProviderDiscovery < OEmbed::ProviderDiscovery
-  class << self
-    def get(url, **options)
-      provider = discover_provider(url, options)
-
-      options.delete(:html)
-
-      provider.get(url, options)
-    end
-
-    def discover_provider(url, **options)
-      format = options[:format]
-
-      html = if options[:html]
-               Nokogiri::HTML(options[:html])
-             else
-               Request.new(:get, url).perform do |res|
-                 raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html'
-                 Nokogiri::HTML(res.body_with_limit)
-               end
-             end
-
-      if format.nil? || format == :json
-        provider_endpoint ||= html.at_xpath('//link[@type="application/json+oembed"]')&.attribute('href')&.value
-        format ||= :json if provider_endpoint
-      end
-
-      if format.nil? || format == :xml
-        provider_endpoint ||= html.at_xpath('//link[@type="text/xml+oembed"]')&.attribute('href')&.value
-        format ||= :xml if provider_endpoint
-      end
-
-      raise OEmbed::NotFound, url if provider_endpoint.nil?
-      begin
-        provider_endpoint = Addressable::URI.parse(provider_endpoint)
-        provider_endpoint.query = nil
-        provider_endpoint = provider_endpoint.to_s
-      rescue Addressable::URI::InvalidURIError
-        raise OEmbed::NotFound, url
-      end
-
-      OEmbed::Provider.new(provider_endpoint, format)
-    end
-  end
-end
diff --git a/app/lib/request.rb b/app/lib/request.rb
index dca93a6e9..00f94dacf 100644
--- a/app/lib/request.rb
+++ b/app/lib/request.rb
@@ -9,11 +9,15 @@ class Request
   include RoutingHelper
 
   def initialize(verb, url, **options)
+    raise ArgumentError if url.blank?
+
     @verb    = verb
     @url     = Addressable::URI.parse(url).normalize
-    @options = options.merge(socket_class: Socket)
+    @options = options.merge(use_proxy? ? Rails.configuration.x.http_client_proxy : { socket_class: Socket })
     @headers = {}
 
+    raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if block_hidden_service?
+
     set_common_headers!
     set_digest! if options.key?(:body)
   end
@@ -99,6 +103,14 @@ class Request
     @http_client ||= HTTP.timeout(:per_operation, timeout).follow(max_hops: 2)
   end
 
+  def use_proxy?
+    Rails.configuration.x.http_client_proxy.present?
+  end
+
+  def block_hidden_service?
+    !Rails.configuration.x.access_to_hidden_service && /\.(onion|i2p)$/.match(@url.host)
+  end
+
   module ClientLimit
     def body_with_limit(limit = 1.megabyte)
       raise Mastodon::LengthValidationError if content_length.present? && content_length > limit
@@ -129,6 +141,7 @@ class Request
   class Socket < TCPSocket
     class << self
       def open(host, *args)
+        return super host, *args if thru_hidden_service? host
         outer_e = nil
         Addrinfo.foreach(host, nil, nil, :SOCK_STREAM) do |address|
           begin
@@ -142,6 +155,10 @@ class Request
       end
 
       alias new open
+
+      def thru_hidden_service?(host)
+        Rails.configuration.x.hidden_service_via_transparent_proxy && /\.(onion|i2p)$/.match(host)
+      end
     end
   end
 
diff --git a/app/lib/rss_builder.rb b/app/lib/rss_builder.rb
new file mode 100644
index 000000000..63ddba2e8
--- /dev/null
+++ b/app/lib/rss_builder.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+class RSSBuilder
+  class ItemBuilder
+    def initialize
+      @item = Ox::Element.new('item')
+    end
+
+    def title(str)
+      @item << (Ox::Element.new('title') << str)
+
+      self
+    end
+
+    def link(str)
+      @item << Ox::Element.new('guid').tap do |guid|
+        guid['isPermalink'] = 'true'
+        guid << str
+      end
+
+      @item << (Ox::Element.new('link') << str)
+
+      self
+    end
+
+    def pub_date(date)
+      @item << (Ox::Element.new('pubDate') << date.to_formatted_s(:rfc822))
+
+      self
+    end
+
+    def description(str)
+      @item << (Ox::Element.new('description') << str)
+
+      self
+    end
+
+    def enclosure(url, type, size)
+      @item << Ox::Element.new('enclosure').tap do |enclosure|
+        enclosure['url']    = url
+        enclosure['length'] = size
+        enclosure['type']   = type
+      end
+
+      self
+    end
+
+    def to_element
+      @item
+    end
+  end
+
+  def initialize
+    @document = Ox::Document.new(version: '1.0')
+    @channel  = Ox::Element.new('channel')
+
+    @document << (rss << @channel)
+  end
+
+  def title(str)
+    @channel << (Ox::Element.new('title') << str)
+
+    self
+  end
+
+  def link(str)
+    @channel << (Ox::Element.new('link') << str)
+
+    self
+  end
+
+  def image(str)
+    @channel << Ox::Element.new('image').tap do |image|
+      image << (Ox::Element.new('url') << str)
+      image << (Ox::Element.new('title') << '')
+      image << (Ox::Element.new('link') << '')
+    end
+
+    @channel << (Ox::Element.new('webfeeds:icon') << str)
+
+    self
+  end
+
+  def cover(str)
+    @channel << Ox::Element.new('webfeeds:cover').tap do |cover|
+      cover['image'] = str
+    end
+
+    self
+  end
+
+  def logo(str)
+    @channel << (Ox::Element.new('webfeeds:logo') << str)
+
+    self
+  end
+
+  def accent_color(str)
+    @channel << (Ox::Element.new('webfeeds:accentColor') << str)
+
+    self
+  end
+
+  def description(str)
+    @channel << (Ox::Element.new('description') << str)
+
+    self
+  end
+
+  def item
+    @channel << ItemBuilder.new.tap do |item|
+      yield item
+    end.to_element
+
+    self
+  end
+
+  def to_xml
+    ('<?xml version="1.0" encoding="UTF-8"?>' + Ox.dump(@document, effort: :tolerant)).force_encoding('UTF-8')
+  end
+
+  private
+
+  def rss
+    Ox::Element.new('rss').tap do |rss|
+      rss['version']        = '2.0'
+      rss['xmlns:webfeeds'] = 'http://webfeeds.org/rss/1.0'
+    end
+  end
+end
diff --git a/app/lib/status_filter.rb b/app/lib/status_filter.rb
index 41d4381e5..b6c80b801 100644
--- a/app/lib/status_filter.rb
+++ b/app/lib/status_filter.rb
@@ -3,9 +3,10 @@
 class StatusFilter
   attr_reader :status, :account
 
-  def initialize(status, account)
-    @status = status
-    @account = account
+  def initialize(status, account, preloaded_relations = {})
+    @status              = status
+    @account             = account
+    @preloaded_relations = preloaded_relations
   end
 
   def filtered?
@@ -24,15 +25,15 @@ class StatusFilter
   end
 
   def blocking_account?
-    account.blocking? status.account_id
+    @preloaded_relations[:blocking] ? @preloaded_relations[:blocking][status.account_id] : account.blocking?(status.account_id)
   end
 
   def blocking_domain?
-    account.domain_blocking? status.account_domain
+    @preloaded_relations[:domain_blocking_by_domain] ? @preloaded_relations[:domain_blocking_by_domain][status.account_domain] : account.domain_blocking?(status.account_domain)
   end
 
   def muting_account?
-    account.muting? status.account_id
+    @preloaded_relations[:muting] ? @preloaded_relations[:muting][status.account_id] : account.muting?(status.account_id)
   end
 
   def silenced_account?
@@ -44,7 +45,7 @@ class StatusFilter
   end
 
   def account_following_status_account?
-    account&.following? status.account_id
+    @preloaded_relations[:following] ? @preloaded_relations[:following][status.account_id] : account&.following?(status.account_id)
   end
 
   def blocked_by_policy?
@@ -52,6 +53,6 @@ class StatusFilter
   end
 
   def policy_allows_show?
-    StatusPolicy.new(account, status).show?
+    StatusPolicy.new(account, status, @preloaded_relations).show?
   end
 end
diff --git a/app/models/account.rb b/app/models/account.rb
index db2171102..c1ce1e99e 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -3,7 +3,7 @@
 #
 # Table name: accounts
 #
-#  id                      :integer          not null, primary key
+#  id                      :bigint(8)        not null, primary key
 #  username                :string           default(""), not null
 #  domain                  :string
 #  secret                  :string           default(""), not null
@@ -42,7 +42,7 @@
 #  followers_url           :string           default(""), not null
 #  protocol                :integer          default("ostatus"), not null
 #  memorial                :boolean          default(FALSE), not null
-#  moved_to_account_id     :integer
+#  moved_to_account_id     :bigint(8)
 #  featured_collection_url :string
 #  fields                  :jsonb
 #
@@ -120,6 +120,7 @@ class Account < ApplicationRecord
   scope :partitioned, -> { order(Arel.sql('row_number() over (partition by domain)')) }
   scope :silenced, -> { where(silenced: true) }
   scope :suspended, -> { where(suspended: true) }
+  scope :without_suspended, -> { where(suspended: false) }
   scope :recent, -> { reorder(id: :desc) }
   scope :alphabetic, -> { order(domain: :asc, username: :asc) }
   scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
@@ -275,6 +276,10 @@ class Account < ApplicationRecord
       @value   = attr['value']
       @errors  = {}
     end
+
+    def to_h
+      { name: @name, value: @value }
+    end
   end
 
   class << self
@@ -393,7 +398,7 @@ class Account < ApplicationRecord
   end
 
   def emojis
-    CustomEmoji.from_text(note, domain)
+    @emojis ||= CustomEmoji.from_text(note, domain)
   end
 
   before_create :generate_keys
@@ -408,9 +413,9 @@ class Account < ApplicationRecord
   end
 
   def generate_keys
-    return unless local?
+    return unless local? && !Rails.env.test?
 
-    keypair = OpenSSL::PKey::RSA.new(Rails.env.test? ? 512 : 2048)
+    keypair = OpenSSL::PKey::RSA.new(2048)
     self.private_key = keypair.to_pem
     self.public_key  = keypair.public_key.to_pem
   end
diff --git a/app/models/account_domain_block.rb b/app/models/account_domain_block.rb
index bc00b4f32..e352000c3 100644
--- a/app/models/account_domain_block.rb
+++ b/app/models/account_domain_block.rb
@@ -3,11 +3,11 @@
 #
 # Table name: account_domain_blocks
 #
-#  id         :integer          not null, primary key
+#  id         :bigint(8)        not null, primary key
 #  domain     :string
 #  created_at :datetime         not null
 #  updated_at :datetime         not null
-#  account_id :integer
+#  account_id :bigint(8)
 #
 
 class AccountDomainBlock < ApplicationRecord
diff --git a/app/models/account_moderation_note.rb b/app/models/account_moderation_note.rb
index 3ac9b1ac1..22e312bb2 100644
--- a/app/models/account_moderation_note.rb
+++ b/app/models/account_moderation_note.rb
@@ -3,10 +3,10 @@
 #
 # Table name: account_moderation_notes
 #
-#  id                :integer          not null, primary key
+#  id                :bigint(8)        not null, primary key
 #  content           :text             not null
-#  account_id        :integer          not null
-#  target_account_id :integer          not null
+#  account_id        :bigint(8)        not null
+#  target_account_id :bigint(8)        not null
 #  created_at        :datetime         not null
 #  updated_at        :datetime         not null
 #
diff --git a/app/models/admin/action_log.rb b/app/models/admin/action_log.rb
index 81f278e07..1d1db1b7a 100644
--- a/app/models/admin/action_log.rb
+++ b/app/models/admin/action_log.rb
@@ -3,11 +3,11 @@
 #
 # Table name: admin_action_logs
 #
-#  id               :integer          not null, primary key
-#  account_id       :integer
+#  id               :bigint(8)        not null, primary key
+#  account_id       :bigint(8)
 #  action           :string           default(""), not null
 #  target_type      :string
-#  target_id        :integer
+#  target_id        :bigint(8)
 #  recorded_changes :text             default(""), not null
 #  created_at       :datetime         not null
 #  updated_at       :datetime         not null
diff --git a/app/models/backup.rb b/app/models/backup.rb
index 5a7e6a14d..c2651313b 100644
--- a/app/models/backup.rb
+++ b/app/models/backup.rb
@@ -3,8 +3,8 @@
 #
 # Table name: backups
 #
-#  id                :integer          not null, primary key
-#  user_id           :integer
+#  id                :bigint(8)        not null, primary key
+#  user_id           :bigint(8)
 #  dump_file_name    :string
 #  dump_content_type :string
 #  dump_file_size    :integer
diff --git a/app/models/block.rb b/app/models/block.rb
index d6ecabd3b..df4a6bbac 100644
--- a/app/models/block.rb
+++ b/app/models/block.rb
@@ -3,11 +3,11 @@
 #
 # Table name: blocks
 #
-#  id                :integer          not null, primary key
+#  id                :bigint(8)        not null, primary key
 #  created_at        :datetime         not null
 #  updated_at        :datetime         not null
-#  account_id        :integer          not null
-#  target_account_id :integer          not null
+#  account_id        :bigint(8)        not null
+#  target_account_id :bigint(8)        not null
 #
 
 class Block < ApplicationRecord
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index 3830ba9b0..20fc74ba6 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -20,6 +20,10 @@ module AccountInteractions
       follow_mapping(Block.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
     end
 
+    def blocked_by_map(target_account_ids, account_id)
+      follow_mapping(Block.where(account_id: target_account_ids, target_account_id: account_id), :account_id)
+    end
+
     def muting_map(target_account_ids, account_id)
       Mute.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |mute, mapping|
         mapping[mute.target_account_id] = {
@@ -38,8 +42,12 @@ module AccountInteractions
 
     def domain_blocking_map(target_account_ids, account_id)
       accounts_map    = Account.where(id: target_account_ids).select('id, domain').map { |a| [a.id, a.domain] }.to_h
-      blocked_domains = AccountDomainBlock.where(account_id: account_id, domain: accounts_map.values).pluck(:domain)
-      accounts_map.map { |id, domain| [id, blocked_domains.include?(domain)] }.to_h
+      blocked_domains = domain_blocking_map_by_domain(accounts_map.values.compact, account_id)
+      accounts_map.map { |id, domain| [id, blocked_domains[domain]] }.to_h
+    end
+
+    def domain_blocking_map_by_domain(target_domains, account_id)
+      follow_mapping(AccountDomainBlock.where(account_id: account_id, domain: target_domains), :domain)
     end
 
     private
@@ -93,6 +101,7 @@ module AccountInteractions
     if mute.hide_notifications? != notifications
       mute.update!(hide_notifications: notifications)
     end
+    mute
   end
 
   def mute_conversation!(conversation)
diff --git a/app/models/concerns/attachmentable.rb b/app/models/concerns/attachmentable.rb
index 90ce88463..6f8489b89 100644
--- a/app/models/concerns/attachmentable.rb
+++ b/app/models/concerns/attachmentable.rb
@@ -1,10 +1,15 @@
 # frozen_string_literal: true
 
+require 'mime/types'
+
 module Attachmentable
   extend ActiveSupport::Concern
 
+  MAX_MATRIX_LIMIT = 16_777_216 # 4096x4096px or approx. 16MB
+
   included do
     before_post_process :set_file_extensions
+    before_post_process :check_image_dimensions
   end
 
   private
@@ -12,10 +17,31 @@ module Attachmentable
   def set_file_extensions
     self.class.attachment_definitions.each_key do |attachment_name|
       attachment = send(attachment_name)
+
       next if attachment.blank?
-      extension = Paperclip::Interpolations.content_type_extension(attachment, :original)
-      basename  = Paperclip::Interpolations.basename(attachment, :original)
-      attachment.instance_write :file_name, [basename, extension].delete_if(&:blank?).join('.')
+
+      attachment.instance_write :file_name, [Paperclip::Interpolations.basename(attachment, :original), appropriate_extension(attachment)].delete_if(&:blank?).join('.')
+    end
+  end
+
+  def check_image_dimensions
+    self.class.attachment_definitions.each_key do |attachment_name|
+      attachment = send(attachment_name)
+
+      next if attachment.blank? || !attachment.content_type.match?(/image.*/) || attachment.queued_for_write[:original].blank?
+
+      width, height = FastImage.size(attachment.queued_for_write[:original].path)
+
+      raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported" if width.present? && height.present? && (width * height >= MAX_MATRIX_LIMIT)
     end
   end
+
+  def appropriate_extension(attachment)
+    mime_type = MIME::Types[attachment.content_type]
+
+    extensions_for_mime_type = mime_type.empty? ? [] : mime_type.first.extensions
+    original_extension       = Paperclip::Interpolations.extension(attachment, :original)
+
+    extensions_for_mime_type.include?(original_extension) ? original_extension : extensions_for_mime_type.first
+  end
 end
diff --git a/app/models/concerns/cacheable.rb b/app/models/concerns/cacheable.rb
index 51451d260..d7524cdfd 100644
--- a/app/models/concerns/cacheable.rb
+++ b/app/models/concerns/cacheable.rb
@@ -3,14 +3,19 @@
 module Cacheable
   extend ActiveSupport::Concern
 
-  class_methods do
+  module ClassMethods
+    @cache_associated = []
+
     def cache_associated(*associations)
       @cache_associated = associations
     end
-  end
 
-  included do
-    scope :with_includes, -> { includes(@cache_associated) }
-    scope :cache_ids, -> { select(:id, :updated_at) }
+    def with_includes
+      includes(@cache_associated)
+    end
+
+    def cache_ids
+      select(:id, :updated_at)
+    end
   end
 end
diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb
index 3b8c507c3..7f1ef5191 100644
--- a/app/models/concerns/remotable.rb
+++ b/app/models/concerns/remotable.rb
@@ -38,7 +38,7 @@ module Remotable
 
             self[attribute_name] = url if has_attribute?(attribute_name)
           end
-        rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError => e
+        rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e
           Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}"
           nil
         end
diff --git a/app/models/concerns/status_threading_concern.rb b/app/models/concerns/status_threading_concern.rb
index fffc095ee..8e817be00 100644
--- a/app/models/concerns/status_threading_concern.rb
+++ b/app/models/concerns/status_threading_concern.rb
@@ -7,8 +7,8 @@ module StatusThreadingConcern
     find_statuses_from_tree_path(ancestor_ids(limit), account)
   end
 
-  def descendants(account = nil)
-    find_statuses_from_tree_path(descendant_ids, account)
+  def descendants(limit, account = nil, max_child_id = nil, since_child_id = nil, depth = nil)
+    find_statuses_from_tree_path(descendant_ids(limit, max_child_id, since_child_id, depth), account)
   end
 
   private
@@ -46,34 +46,46 @@ module StatusThreadingConcern
     SQL
   end
 
-  def descendant_ids
-    descendant_statuses.pluck(:id)
+  def descendant_ids(limit, max_child_id, since_child_id, depth)
+    descendant_statuses(limit, max_child_id, since_child_id, depth).pluck(:id)
   end
 
-  def descendant_statuses
-    Status.find_by_sql([<<-SQL.squish, id: id])
+  def descendant_statuses(limit, max_child_id, since_child_id, depth)
+    Status.find_by_sql([<<-SQL.squish, id: id, limit: limit, max_child_id: max_child_id, since_child_id: since_child_id, depth: depth])
       WITH RECURSIVE search_tree(id, path)
       AS (
         SELECT id, ARRAY[id]
         FROM statuses
-        WHERE in_reply_to_id = :id
+        WHERE in_reply_to_id = :id AND COALESCE(id < :max_child_id, TRUE) AND COALESCE(id > :since_child_id, TRUE)
         UNION ALL
         SELECT statuses.id, path || statuses.id
         FROM search_tree
         JOIN statuses ON statuses.in_reply_to_id = search_tree.id
-        WHERE NOT statuses.id = ANY(path)
+        WHERE COALESCE(array_length(path, 1) < :depth, TRUE) AND NOT statuses.id = ANY(path)
       )
       SELECT id
       FROM search_tree
       ORDER BY path
+      LIMIT :limit
     SQL
   end
 
   def find_statuses_from_tree_path(ids, account)
-    statuses = statuses_with_accounts(ids).to_a
+    statuses    = statuses_with_accounts(ids).to_a
+    account_ids = statuses.map(&:account_id).uniq
+    domains     = statuses.map(&:account_domain).compact.uniq
+
+    relations = if account.present?
+                  {
+                    blocking: Account.blocking_map(account_ids, account.id),
+                    blocked_by: Account.blocked_by_map(account_ids, account.id),
+                    muting: Account.muting_map(account_ids, account.id),
+                    following: Account.following_map(account_ids, account.id),
+                    domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id),
+                  }
+                end
 
-    # FIXME: n+1 bonanza
-    statuses.reject! { |status| filter_from_context?(status, account) }
+    statuses.reject! { |status| filter_from_context?(status, account, relations) }
 
     # Order ancestors/descendants by tree path
     statuses.sort_by! { |status| ids.index(status.id) }
@@ -83,7 +95,7 @@ module StatusThreadingConcern
     Status.where(id: ids).includes(:account)
   end
 
-  def filter_from_context?(status, account)
-    StatusFilter.new(status, account).filtered?
+  def filter_from_context?(status, account, relations)
+    StatusFilter.new(status, account, relations).filtered?
   end
 end
diff --git a/app/models/conversation.rb b/app/models/conversation.rb
index 08c1ce945..4dfaea889 100644
--- a/app/models/conversation.rb
+++ b/app/models/conversation.rb
@@ -3,7 +3,7 @@
 #
 # Table name: conversations
 #
-#  id         :integer          not null, primary key
+#  id         :bigint(8)        not null, primary key
 #  uri        :string
 #  created_at :datetime         not null
 #  updated_at :datetime         not null
diff --git a/app/models/conversation_mute.rb b/app/models/conversation_mute.rb
index 272eb81af..52c1a33e0 100644
--- a/app/models/conversation_mute.rb
+++ b/app/models/conversation_mute.rb
@@ -3,9 +3,9 @@
 #
 # Table name: conversation_mutes
 #
-#  id              :integer          not null, primary key
-#  conversation_id :integer          not null
-#  account_id      :integer          not null
+#  id              :bigint(8)        not null, primary key
+#  conversation_id :bigint(8)        not null
+#  account_id      :bigint(8)        not null
 #
 
 class ConversationMute < ApplicationRecord
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index 1ec21d1a0..b99ed01f0 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -3,7 +3,7 @@
 #
 # Table name: custom_emojis
 #
-#  id                 :integer          not null, primary key
+#  id                 :bigint(8)        not null, primary key
 #  shortcode          :string           default(""), not null
 #  domain             :string
 #  image_file_name    :string
@@ -40,6 +40,10 @@ class CustomEmoji < ApplicationRecord
 
   remotable_attachment :image, LIMIT
 
+  include Attachmentable
+
+  after_commit :remove_entity_cache
+
   def local?
     domain.nil?
   end
@@ -56,11 +60,17 @@ class CustomEmoji < ApplicationRecord
 
       return [] if shortcodes.empty?
 
-      where(shortcode: shortcodes, domain: domain, disabled: false)
+      EntityCache.instance.emoji(shortcodes, domain)
     end
 
     def search(shortcode)
       where('"custom_emojis"."shortcode" ILIKE ?', "%#{shortcode}%")
     end
   end
+
+  private
+
+  def remove_entity_cache
+    Rails.cache.delete(EntityCache.instance.to_key(:emoji, shortcode, domain))
+  end
 end
diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb
index aea8919af..93658793b 100644
--- a/app/models/domain_block.rb
+++ b/app/models/domain_block.rb
@@ -3,7 +3,7 @@
 #
 # Table name: domain_blocks
 #
-#  id           :integer          not null, primary key
+#  id           :bigint(8)        not null, primary key
 #  domain       :string           default(""), not null
 #  created_at   :datetime         not null
 #  updated_at   :datetime         not null
diff --git a/app/models/email_domain_block.rb b/app/models/email_domain_block.rb
index a104810d1..10490375b 100644
--- a/app/models/email_domain_block.rb
+++ b/app/models/email_domain_block.rb
@@ -3,7 +3,7 @@
 #
 # Table name: email_domain_blocks
 #
-#  id         :integer          not null, primary key
+#  id         :bigint(8)        not null, primary key
 #  domain     :string           default(""), not null
 #  created_at :datetime         not null
 #  updated_at :datetime         not null
diff --git a/app/models/favourite.rb b/app/models/favourite.rb
index fa1884b86..c998a67eb 100644
--- a/app/models/favourite.rb
+++ b/app/models/favourite.rb
@@ -3,11 +3,11 @@
 #
 # Table name: favourites
 #
-#  id         :integer          not null, primary key
+#  id         :bigint(8)        not null, primary key
 #  created_at :datetime         not null
 #  updated_at :datetime         not null
-#  account_id :integer          not null
-#  status_id  :integer          not null
+#  account_id :bigint(8)        not null
+#  status_id  :bigint(8)        not null
 #
 
 class Favourite < ApplicationRecord
diff --git a/app/models/follow.rb b/app/models/follow.rb
index 8e6fe537a..2ca42ff70 100644
--- a/app/models/follow.rb
+++ b/app/models/follow.rb
@@ -3,11 +3,11 @@
 #
 # Table name: follows
 #
-#  id                :integer          not null, primary key
+#  id                :bigint(8)        not null, primary key
 #  created_at        :datetime         not null
 #  updated_at        :datetime         not null
-#  account_id        :integer          not null
-#  target_account_id :integer          not null
+#  account_id        :bigint(8)        not null
+#  target_account_id :bigint(8)        not null
 #  show_reblogs      :boolean          default(TRUE), not null
 #
 
diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb
index cde26ceed..d559a8f62 100644
--- a/app/models/follow_request.rb
+++ b/app/models/follow_request.rb
@@ -3,11 +3,11 @@
 #
 # Table name: follow_requests
 #
-#  id                :integer          not null, primary key
+#  id                :bigint(8)        not null, primary key
 #  created_at        :datetime         not null
 #  updated_at        :datetime         not null
-#  account_id        :integer          not null
-#  target_account_id :integer          not null
+#  account_id        :bigint(8)        not null
+#  target_account_id :bigint(8)        not null
 #  show_reblogs      :boolean          default(TRUE), not null
 #
 
diff --git a/app/models/import.rb b/app/models/import.rb
index fdb4c6b80..55e970b0d 100644
--- a/app/models/import.rb
+++ b/app/models/import.rb
@@ -3,7 +3,7 @@
 #
 # Table name: imports
 #
-#  id                :integer          not null, primary key
+#  id                :bigint(8)        not null, primary key
 #  type              :integer          not null
 #  approved          :boolean          default(FALSE), not null
 #  created_at        :datetime         not null
@@ -12,7 +12,7 @@
 #  data_content_type :string
 #  data_file_size    :integer
 #  data_updated_at   :datetime
-#  account_id        :integer          not null
+#  account_id        :bigint(8)        not null
 #
 
 class Import < ApplicationRecord
diff --git a/app/models/invite.rb b/app/models/invite.rb
index 4ba5432d2..2250e588e 100644
--- a/app/models/invite.rb
+++ b/app/models/invite.rb
@@ -3,8 +3,8 @@
 #
 # Table name: invites
 #
-#  id         :integer          not null, primary key
-#  user_id    :integer          not null
+#  id         :bigint(8)        not null, primary key
+#  user_id    :bigint(8)        not null
 #  code       :string           default(""), not null
 #  expires_at :datetime
 #  max_uses   :integer
diff --git a/app/models/list.rb b/app/models/list.rb
index a2ec7e84a..c9c94fca1 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -3,8 +3,8 @@
 #
 # Table name: lists
 #
-#  id         :integer          not null, primary key
-#  account_id :integer          not null
+#  id         :bigint(8)        not null, primary key
+#  account_id :bigint(8)        not null
 #  title      :string           default(""), not null
 #  created_at :datetime         not null
 #  updated_at :datetime         not null
diff --git a/app/models/list_account.rb b/app/models/list_account.rb
index da46cf032..87b498224 100644
--- a/app/models/list_account.rb
+++ b/app/models/list_account.rb
@@ -3,10 +3,10 @@
 #
 # Table name: list_accounts
 #
-#  id         :integer          not null, primary key
-#  list_id    :integer          not null
-#  account_id :integer          not null
-#  follow_id  :integer          not null
+#  id         :bigint(8)        not null, primary key
+#  list_id    :bigint(8)        not null
+#  account_id :bigint(8)        not null
+#  follow_id  :bigint(8)        not null
 #
 
 class ListAccount < ApplicationRecord
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 3b16944ce..62abc876e 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -3,8 +3,8 @@
 #
 # Table name: media_attachments
 #
-#  id                :integer          not null, primary key
-#  status_id         :integer
+#  id                :bigint(8)        not null, primary key
+#  status_id         :bigint(8)
 #  file_file_name    :string
 #  file_content_type :string
 #  file_file_size    :integer
@@ -15,12 +15,10 @@
 #  shortcode         :string
 #  type              :integer          default("image"), not null
 #  file_meta         :json
-#  account_id        :integer
+#  account_id        :bigint(8)
 #  description       :text
 #
 
-require 'mime/types'
-
 class MediaAttachment < ApplicationRecord
   self.inheritance_column = nil
 
@@ -90,6 +88,8 @@ class MediaAttachment < ApplicationRecord
   validates_attachment_size :file, less_than: LIMIT
   remotable_attachment :file, LIMIT
 
+  include Attachmentable
+
   validates :account, presence: true
   validates :description, length: { maximum: 420 }, if: :local?
 
@@ -247,13 +247,4 @@ class MediaAttachment < ApplicationRecord
       bitrate: movie.bitrate,
     }
   end
-
-  def appropriate_extension
-    mime_type = MIME::Types[file.content_type]
-
-    extensions_for_mime_type = mime_type.empty? ? [] : mime_type.first.extensions
-    original_extension       = Paperclip::Interpolations.extension(file, :original)
-
-    extensions_for_mime_type.include?(original_extension) ? original_extension : extensions_for_mime_type.first
-  end
 end
diff --git a/app/models/mention.rb b/app/models/mention.rb
index f864bf8e1..8ab886b18 100644
--- a/app/models/mention.rb
+++ b/app/models/mention.rb
@@ -3,11 +3,11 @@
 #
 # Table name: mentions
 #
-#  id         :integer          not null, primary key
-#  status_id  :integer
+#  id         :bigint(8)        not null, primary key
+#  status_id  :bigint(8)
 #  created_at :datetime         not null
 #  updated_at :datetime         not null
-#  account_id :integer
+#  account_id :bigint(8)
 #
 
 class Mention < ApplicationRecord
diff --git a/app/models/mute.rb b/app/models/mute.rb
index ebb3818c7..639120f7d 100644
--- a/app/models/mute.rb
+++ b/app/models/mute.rb
@@ -3,12 +3,12 @@
 #
 # Table name: mutes
 #
-#  id                 :integer          not null, primary key
+#  id                 :bigint(8)        not null, primary key
 #  created_at         :datetime         not null
 #  updated_at         :datetime         not null
 #  hide_notifications :boolean          default(TRUE), not null
-#  account_id         :integer          not null
-#  target_account_id  :integer          not null
+#  account_id         :bigint(8)        not null
+#  target_account_id  :bigint(8)        not null
 #
 
 class Mute < ApplicationRecord
diff --git a/app/models/notification.rb b/app/models/notification.rb
index 0b0f01aa8..4f6ec8e8e 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -3,13 +3,13 @@
 #
 # Table name: notifications
 #
-#  id              :integer          not null, primary key
-#  activity_id     :integer          not null
+#  id              :bigint(8)        not null, primary key
+#  activity_id     :bigint(8)        not null
 #  activity_type   :string           not null
 #  created_at      :datetime         not null
 #  updated_at      :datetime         not null
-#  account_id      :integer          not null
-#  from_account_id :integer          not null
+#  account_id      :bigint(8)        not null
+#  from_account_id :bigint(8)        not null
 #
 
 class Notification < ApplicationRecord
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index 0c82f06ce..a792b352b 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -3,7 +3,7 @@
 #
 # Table name: preview_cards
 #
-#  id                 :integer          not null, primary key
+#  id                 :bigint(8)        not null, primary key
 #  url                :string           default(""), not null
 #  title              :string           default(""), not null
 #  description        :string           default(""), not null
@@ -34,7 +34,7 @@ class PreviewCard < ApplicationRecord
 
   has_and_belongs_to_many :statuses
 
-  has_attached_file :image, styles: { original: { geometry: '400x400>', file_geometry_parser: FastGeometryParser } }, convert_options: { all: '-quality 80 -strip' }
+  has_attached_file :image, styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 80 -strip' }
 
   include Attachmentable
 
@@ -52,6 +52,23 @@ class PreviewCard < ApplicationRecord
     save!
   end
 
+  class << self
+    private
+
+    def image_styles(f)
+      styles = {
+        original: {
+          geometry: '400x400>',
+          file_geometry_parser: FastGeometryParser,
+          convert_options: '-coalesce -strip',
+        },
+      }
+
+      styles[:original][:format] = 'jpg' if f.instance.image_content_type == 'image/gif'
+      styles
+    end
+  end
+
   private
 
   def extract_dimensions
diff --git a/app/models/report.rb b/app/models/report.rb
index 5b90c7bce..efe385b2d 100644
--- a/app/models/report.rb
+++ b/app/models/report.rb
@@ -3,16 +3,16 @@
 #
 # Table name: reports
 #
-#  id                         :integer          not null, primary key
-#  status_ids                 :integer          default([]), not null, is an Array
+#  id                         :bigint(8)        not null, primary key
+#  status_ids                 :bigint(8)        default([]), not null, is an Array
 #  comment                    :text             default(""), not null
 #  action_taken               :boolean          default(FALSE), not null
 #  created_at                 :datetime         not null
 #  updated_at                 :datetime         not null
-#  account_id                 :integer          not null
-#  action_taken_by_account_id :integer
-#  target_account_id          :integer          not null
-#  assigned_account_id        :integer
+#  account_id                 :bigint(8)        not null
+#  action_taken_by_account_id :bigint(8)
+#  target_account_id          :bigint(8)        not null
+#  assigned_account_id        :bigint(8)
 #
 
 class Report < ApplicationRecord
diff --git a/app/models/report_note.rb b/app/models/report_note.rb
index 6d9dec80a..54b416577 100644
--- a/app/models/report_note.rb
+++ b/app/models/report_note.rb
@@ -3,10 +3,10 @@
 #
 # Table name: report_notes
 #
-#  id         :integer          not null, primary key
+#  id         :bigint(8)        not null, primary key
 #  content    :text             not null
-#  report_id  :integer          not null
-#  account_id :integer          not null
+#  report_id  :bigint(8)        not null
+#  account_id :bigint(8)        not null
 #  created_at :datetime         not null
 #  updated_at :datetime         not null
 #
diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb
index d364f03df..34d25c83d 100644
--- a/app/models/session_activation.rb
+++ b/app/models/session_activation.rb
@@ -3,15 +3,15 @@
 #
 # Table name: session_activations
 #
-#  id                       :integer          not null, primary key
+#  id                       :bigint(8)        not null, primary key
 #  session_id               :string           not null
 #  created_at               :datetime         not null
 #  updated_at               :datetime         not null
 #  user_agent               :string           default(""), not null
 #  ip                       :inet
-#  access_token_id          :integer
-#  user_id                  :integer          not null
-#  web_push_subscription_id :integer
+#  access_token_id          :bigint(8)
+#  user_id                  :bigint(8)        not null
+#  web_push_subscription_id :bigint(8)
 #
 
 class SessionActivation < ApplicationRecord
diff --git a/app/models/setting.rb b/app/models/setting.rb
index df93590ce..033d09fd5 100644
--- a/app/models/setting.rb
+++ b/app/models/setting.rb
@@ -3,13 +3,13 @@
 #
 # Table name: settings
 #
-#  id         :integer          not null, primary key
+#  id         :bigint(8)        not null, primary key
 #  var        :string           not null
 #  value      :text
 #  thing_type :string
 #  created_at :datetime
 #  updated_at :datetime
-#  thing_id   :integer
+#  thing_id   :bigint(8)
 #
 
 class Setting < RailsSettings::Base
diff --git a/app/models/site_upload.rb b/app/models/site_upload.rb
index 641128adf..14d683767 100644
--- a/app/models/site_upload.rb
+++ b/app/models/site_upload.rb
@@ -3,7 +3,7 @@
 #
 # Table name: site_uploads
 #
-#  id                :integer          not null, primary key
+#  id                :bigint(8)        not null, primary key
 #  var               :string           default(""), not null
 #  file_file_name    :string
 #  file_content_type :string
diff --git a/app/models/status.rb b/app/models/status.rb
index 952661169..44238ca6b 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -3,13 +3,13 @@
 #
 # Table name: statuses
 #
-#  id                     :integer          not null, primary key
+#  id                     :bigint(8)        not null, primary key
 #  uri                    :string
 #  text                   :text             default(""), not null
 #  created_at             :datetime         not null
 #  updated_at             :datetime         not null
-#  in_reply_to_id         :integer
-#  reblog_of_id           :integer
+#  in_reply_to_id         :bigint(8)
+#  reblog_of_id           :bigint(8)
 #  url                    :string
 #  sensitive              :boolean          default(FALSE), not null
 #  visibility             :integer          default("public"), not null
@@ -18,11 +18,11 @@
 #  favourites_count       :integer          default(0), not null
 #  reblogs_count          :integer          default(0), not null
 #  language               :string
-#  conversation_id        :integer
+#  conversation_id        :bigint(8)
 #  local                  :boolean
-#  account_id             :integer          not null
-#  application_id         :integer
-#  in_reply_to_account_id :integer
+#  account_id             :bigint(8)        not null
+#  application_id         :bigint(8)
+#  in_reply_to_account_id :bigint(8)
 #  local_only             :boolean
 #  full_status_text       :text             default(""), not null
 #
@@ -62,6 +62,7 @@ class Status < ApplicationRecord
   validates :uri, uniqueness: true, presence: true, unless: :local?
   validates :text, presence: true, unless: -> { with_media? || reblog? }
   validates_with StatusLengthValidator
+  validates_with DisallowedHashtagsValidator
   validates :reblog, uniqueness: { scope: :account }, if: :reblog?
 
   default_scope { recent }
@@ -164,7 +165,7 @@ class Status < ApplicationRecord
   end
 
   def emojis
-    CustomEmoji.from_text([spoiler_text, text].join(' '), account.domain)
+    @emojis ||= CustomEmoji.from_text([spoiler_text, text].join(' '), account.domain)
   end
 
   after_create_commit :store_uri, if: :local?
diff --git a/app/models/status_pin.rb b/app/models/status_pin.rb
index d3a98d8bd..afc76bded 100644
--- a/app/models/status_pin.rb
+++ b/app/models/status_pin.rb
@@ -3,9 +3,9 @@
 #
 # Table name: status_pins
 #
-#  id         :integer          not null, primary key
-#  account_id :integer          not null
-#  status_id  :integer          not null
+#  id         :bigint(8)        not null, primary key
+#  account_id :bigint(8)        not null
+#  status_id  :bigint(8)        not null
 #  created_at :datetime         not null
 #  updated_at :datetime         not null
 #
diff --git a/app/models/stream_entry.rb b/app/models/stream_entry.rb
index 36fe487dc..dd383eb81 100644
--- a/app/models/stream_entry.rb
+++ b/app/models/stream_entry.rb
@@ -3,13 +3,13 @@
 #
 # Table name: stream_entries
 #
-#  id            :integer          not null, primary key
-#  activity_id   :integer
+#  id            :bigint(8)        not null, primary key
+#  activity_id   :bigint(8)
 #  activity_type :string
 #  created_at    :datetime         not null
 #  updated_at    :datetime         not null
 #  hidden        :boolean          default(FALSE), not null
-#  account_id    :integer
+#  account_id    :bigint(8)
 #
 
 class StreamEntry < ApplicationRecord
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
index ea1173160..79b81828d 100644
--- a/app/models/subscription.rb
+++ b/app/models/subscription.rb
@@ -3,7 +3,7 @@
 #
 # Table name: subscriptions
 #
-#  id                          :integer          not null, primary key
+#  id                          :bigint(8)        not null, primary key
 #  callback_url                :string           default(""), not null
 #  secret                      :string
 #  expires_at                  :datetime
@@ -12,7 +12,7 @@
 #  updated_at                  :datetime         not null
 #  last_successful_delivery_at :datetime
 #  domain                      :string
-#  account_id                  :integer          not null
+#  account_id                  :bigint(8)        not null
 #
 
 class Subscription < ApplicationRecord
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 9fa9405d7..8b1b02412 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -3,7 +3,7 @@
 #
 # Table name: tags
 #
-#  id         :integer          not null, primary key
+#  id         :bigint(8)        not null, primary key
 #  name       :string           default(""), not null
 #  created_at :datetime         not null
 #  updated_at :datetime         not null
diff --git a/app/models/user.rb b/app/models/user.rb
index 803eb8a33..24beb77b2 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -3,7 +3,7 @@
 #
 # Table name: users
 #
-#  id                        :integer          not null, primary key
+#  id                        :bigint(8)        not null, primary key
 #  email                     :string           default(""), not null
 #  created_at                :datetime         not null
 #  updated_at                :datetime         not null
@@ -30,10 +30,10 @@
 #  last_emailed_at           :datetime
 #  otp_backup_codes          :string           is an Array
 #  filtered_languages        :string           default([]), not null, is an Array
-#  account_id                :integer          not null
+#  account_id                :bigint(8)        not null
 #  disabled                  :boolean          default(FALSE), not null
 #  moderator                 :boolean          default(FALSE), not null
-#  invite_id                 :integer
+#  invite_id                 :bigint(8)
 #  remember_token            :string
 #
 
diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb
index 5aee92d27..1736106f7 100644
--- a/app/models/web/push_subscription.rb
+++ b/app/models/web/push_subscription.rb
@@ -3,7 +3,7 @@
 #
 # Table name: web_push_subscriptions
 #
-#  id         :integer          not null, primary key
+#  id         :bigint(8)        not null, primary key
 #  endpoint   :string           not null
 #  key_p256dh :string           not null
 #  key_auth   :string           not null
diff --git a/app/models/web/setting.rb b/app/models/web/setting.rb
index 0a5129d17..99588d26c 100644
--- a/app/models/web/setting.rb
+++ b/app/models/web/setting.rb
@@ -3,11 +3,11 @@
 #
 # Table name: web_settings
 #
-#  id         :integer          not null, primary key
+#  id         :bigint(8)        not null, primary key
 #  data       :json
 #  created_at :datetime         not null
 #  updated_at :datetime         not null
-#  user_id    :integer          not null
+#  user_id    :bigint(8)        not null
 #
 
 class Web::Setting < ApplicationRecord
diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb
index 307876856..96cdee8c7 100644
--- a/app/policies/status_policy.rb
+++ b/app/policies/status_policy.rb
@@ -1,6 +1,12 @@
 # frozen_string_literal: true
 
 class StatusPolicy < ApplicationPolicy
+  def initialize(current_account, record, preloaded_relations = {})
+    super(current_account, record)
+
+    @preloaded_relations = preloaded_relations
+  end
+
   def index?
     staff?
   end
@@ -9,16 +15,20 @@ class StatusPolicy < ApplicationPolicy
     return false if local_only? && current_account.nil?
 
     if direct?
-      owned? || record.mentions.where(account: current_account).exists?
+      owned? || mention_exists?
     elsif private?
-      owned? || current_account&.following?(author) || record.mentions.where(account: current_account).exists?
+      owned? || following_author? || mention_exists?
     else
-      current_account.nil? || !author.blocking?(current_account)
+      current_account.nil? || !author_blocking?
     end
   end
 
   def reblog?
-    !direct? && (!private? || owned?) && show?
+    !direct? && (!private? || owned?) && show? && !blocking_author?
+  end
+
+  def favourite?
+    show? && !blocking_author?
   end
 
   def destroy?
@@ -45,6 +55,34 @@ class StatusPolicy < ApplicationPolicy
     record.private_visibility?
   end
 
+  def mention_exists?
+    return false if current_account.nil?
+
+    if record.mentions.loaded?
+      record.mentions.any? { |mention| mention.account_id == current_account.id }
+    else
+      record.mentions.where(account: current_account).exists?
+    end
+  end
+
+  def blocking_author?
+    return false if current_account.nil?
+
+    @preloaded_relations[:blocking] ? @preloaded_relations[:blocking][author.id] : current_account.blocking?(author)
+  end
+
+  def author_blocking?
+    return false if current_account.nil?
+
+    @preloaded_relations[:blocked_by] ? @preloaded_relations[:blocked_by][author.id] : author.blocking?(current_account)
+  end
+
+  def following_author?
+    return false if current_account.nil?
+
+    @preloaded_relations[:following] ? @preloaded_relations[:following][author.id] : current_account.following?(author)
+  end
+
   def author
     record.account
   end
diff --git a/app/serializers/rest/credential_account_serializer.rb b/app/serializers/rest/credential_account_serializer.rb
index 870d8b71f..56857cba8 100644
--- a/app/serializers/rest/credential_account_serializer.rb
+++ b/app/serializers/rest/credential_account_serializer.rb
@@ -5,10 +5,12 @@ class REST::CredentialAccountSerializer < REST::AccountSerializer
 
   def source
     user = object.user
+
     {
       privacy: user.setting_default_privacy,
       sensitive: user.setting_default_sensitive,
       note: object.note,
+      fields: object.fields.map(&:to_h),
     }
   end
 end
diff --git a/app/serializers/rss/account_serializer.rb b/app/serializers/rss/account_serializer.rb
new file mode 100644
index 000000000..bde360a41
--- /dev/null
+++ b/app/serializers/rss/account_serializer.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+class RSS::AccountSerializer
+  include ActionView::Helpers::NumberHelper
+  include StreamEntriesHelper
+  include RoutingHelper
+
+  def render(account, statuses)
+    builder = RSSBuilder.new
+
+    builder.title("#{display_name(account)} (@#{account.local_username_and_domain})")
+           .description(account_description(account))
+           .link(TagManager.instance.url_for(account))
+           .logo(full_asset_url(asset_pack_path('logo.svg')))
+           .accent_color('2b90d9')
+
+    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(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, length: media.file.size)
+        end
+      end
+    end
+
+    builder.to_xml
+  end
+
+  def self.render(account, statuses)
+    new.render(account, statuses)
+  end
+end
diff --git a/app/serializers/rss/tag_serializer.rb b/app/serializers/rss/tag_serializer.rb
new file mode 100644
index 000000000..7680a8da5
--- /dev/null
+++ b/app/serializers/rss/tag_serializer.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+class RSS::TagSerializer
+  include ActionView::Helpers::NumberHelper
+  include ActionView::Helpers::SanitizeHelper
+  include StreamEntriesHelper
+  include RoutingHelper
+
+  def render(tag, statuses)
+    builder = RSSBuilder.new
+
+    builder.title("##{tag.name}")
+           .description(strip_tags(I18n.t('about.about_hashtag_html', hashtag: tag.name)))
+           .link(tag_url(tag))
+           .logo(full_asset_url(asset_pack_path('logo.svg')))
+           .accent_color('2b90d9')
+
+    statuses.each do |status|
+      builder.item do |item|
+        item.title(status.title)
+            .link(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, length: media.file.size)
+        end
+      end
+    end
+
+    builder.to_xml
+  end
+
+  def self.render(tag, statuses)
+    new.render(tag, statuses)
+  end
+end
diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb
index 3860a9cbd..7edbd9b47 100644
--- a/app/services/account_search_service.rb
+++ b/app/services/account_search_service.rb
@@ -65,9 +65,9 @@ class AccountSearchService < BaseService
   def exact_match
     @_exact_match ||= begin
       if domain_is_local?
-        search_from.find_local(query_username)
+        search_from.without_suspended.find_local(query_username)
       else
-        search_from.find_remote(query_username, query_domain)
+        search_from.without_suspended.find_remote(query_username, query_domain)
       end
     end
   end
diff --git a/app/services/activitypub/fetch_featured_collection_service.rb b/app/services/activitypub/fetch_featured_collection_service.rb
index 40714e980..6a137b520 100644
--- a/app/services/activitypub/fetch_featured_collection_service.rb
+++ b/app/services/activitypub/fetch_featured_collection_service.rb
@@ -4,6 +4,8 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
   include JsonLdHelper
 
   def call(account)
+    return if account.featured_collection_url.blank?
+
     @account = account
     @json    = fetch_resource(@account.featured_collection_url, true)
 
diff --git a/app/services/activitypub/fetch_remote_account_service.rb b/app/services/activitypub/fetch_remote_account_service.rb
index 5024853ca..867e70876 100644
--- a/app/services/activitypub/fetch_remote_account_service.rb
+++ b/app/services/activitypub/fetch_remote_account_service.rb
@@ -56,6 +56,6 @@ class ActivityPub::FetchRemoteAccountService < BaseService
   end
 
   def expected_type?
-    SUPPORTED_TYPES.include?(@json['type'])
+    equals_or_includes_any?(@json['type'], SUPPORTED_TYPES)
   end
 end
diff --git a/app/services/activitypub/fetch_remote_key_service.rb b/app/services/activitypub/fetch_remote_key_service.rb
index 41837d462..505baccd4 100644
--- a/app/services/activitypub/fetch_remote_key_service.rb
+++ b/app/services/activitypub/fetch_remote_key_service.rb
@@ -43,7 +43,7 @@ class ActivityPub::FetchRemoteKeyService < BaseService
   end
 
   def person?
-    ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES.include?(@json['type'])
+    equals_or_includes_any?(@json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
   end
 
   def public_key?
@@ -55,6 +55,6 @@ class ActivityPub::FetchRemoteKeyService < BaseService
   end
 
   def confirmed_owner?
-    ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES.include?(@owner['type']) && value_or_id(@owner['publicKey']) == @json['id']
+    equals_or_includes_any?(@owner['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) && value_or_id(@owner['publicKey']) == @json['id']
   end
 end
diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb
index 503c175d8..930fbad1f 100644
--- a/app/services/activitypub/fetch_remote_status_service.rb
+++ b/app/services/activitypub/fetch_remote_status_service.rb
@@ -42,7 +42,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService
   end
 
   def expected_type?
-    (ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES).include? @json['type']
+    equals_or_includes_any?(@json['type'], ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES)
   end
 
   def needs_update(actor)
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index da32f9615..f67ebb443 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -201,10 +201,7 @@ class ActivityPub::ProcessAccountService < BaseService
     return if @json['tag'].blank?
 
     as_array(@json['tag']).each do |tag|
-      case tag['type']
-      when 'Emoji'
-        process_emoji tag
-      end
+      process_emoji tag if equals_or_includes?(tag['type'], 'Emoji')
     end
   end
 
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index 0f77556dc..510b80c82 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -1,7 +1,5 @@
 # frozen_string_literal: true
 
-require 'sidekiq-bulk'
-
 class FanOutOnWriteService < BaseService
   # Push a status into home and mentions feeds
   # @param [Status] status
diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb
index 44df3ed13..bc2d1547a 100644
--- a/app/services/favourite_service.rb
+++ b/app/services/favourite_service.rb
@@ -8,7 +8,7 @@ class FavouriteService < BaseService
   # @param [Status] status
   # @return [Favourite]
   def call(account, status)
-    authorize_with account, status, :show?
+    authorize_with account, status, :favourite?
 
     favourite = Favourite.find_by(account: account, status: status)
 
diff --git a/app/services/fetch_atom_service.rb b/app/services/fetch_atom_service.rb
index 0444baf74..550e75f33 100644
--- a/app/services/fetch_atom_service.rb
+++ b/app/services/fetch_atom_service.rb
@@ -42,7 +42,7 @@ class FetchAtomService < BaseService
     elsif ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(response.mime_type)
       body = response.body_with_limit
       json = body_to_json(body)
-      if supported_context?(json) && ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES.include?(json['type']) && json['inbox'].present?
+      if supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) && json['inbox'].present?
         [json['id'], { prefetched_body: body, id: true }, :activitypub]
       elsif supported_context?(json) && expected_type?(json)
         [json['id'], { prefetched_body: body, id: true }, :activitypub]
@@ -62,7 +62,7 @@ class FetchAtomService < BaseService
   end
 
   def expected_type?(json)
-    (ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES).include? json['type']
+    equals_or_includes_any?(json['type'], ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES)
   end
 
   def process_html(response)
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index d5920a417..77d4aa538 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -85,42 +85,40 @@ class FetchLinkCardService < BaseService
   end
 
   def attempt_oembed
-    embed = OEmbed::Providers.get(@url, html: @html)
+    embed = FetchOEmbedService.new.call(@url, html: @html)
 
-    return false unless embed.respond_to?(:type)
+    return false if embed.nil?
 
-    @card.type          = embed.type
-    @card.title         = embed.respond_to?(:title)         ? embed.title         : ''
-    @card.author_name   = embed.respond_to?(:author_name)   ? embed.author_name   : ''
-    @card.author_url    = embed.respond_to?(:author_url)    ? embed.author_url    : ''
-    @card.provider_name = embed.respond_to?(:provider_name) ? embed.provider_name : ''
-    @card.provider_url  = embed.respond_to?(:provider_url)  ? embed.provider_url  : ''
+    @card.type          = embed[:type]
+    @card.title         = embed[:title]         || ''
+    @card.author_name   = embed[:author_name]   || ''
+    @card.author_url    = embed[:author_url]    || ''
+    @card.provider_name = embed[:provider_name] || ''
+    @card.provider_url  = embed[:provider_url]  || ''
     @card.width         = 0
     @card.height        = 0
 
     case @card.type
     when 'link'
-      @card.image_remote_url = embed.thumbnail_url if embed.respond_to?(:thumbnail_url)
+      @card.image_remote_url = embed[:thumbnail_url] if embed[:thumbnail_url].present?
     when 'photo'
-      return false unless embed.respond_to?(:url)
+      return false if embed[:url].blank?
 
-      @card.embed_url        = embed.url
-      @card.image_remote_url = embed.url
-      @card.width            = embed.width.presence  || 0
-      @card.height           = embed.height.presence || 0
+      @card.embed_url        = embed[:url]
+      @card.image_remote_url = embed[:url]
+      @card.width            = embed[:width].presence  || 0
+      @card.height           = embed[:height].presence || 0
     when 'video'
-      @card.width            = embed.width.presence  || 0
-      @card.height           = embed.height.presence || 0
-      @card.html             = Formatter.instance.sanitize(embed.html, Sanitize::Config::MASTODON_OEMBED)
-      @card.image_remote_url = embed.thumbnail_url if embed.respond_to?(:thumbnail_url)
+      @card.width            = embed[:width].presence  || 0
+      @card.height           = embed[:height].presence || 0
+      @card.html             = Formatter.instance.sanitize(embed[:html], Sanitize::Config::MASTODON_OEMBED)
+      @card.image_remote_url = embed[:thumbnail_url] if embed[:thumbnail_url].present?
     when 'rich'
       # Most providers rely on <script> tags, which is a no-no
       return false
     end
 
     @card.save_with_optional_image!
-  rescue OEmbed::NotFound
-    false
   end
 
   def attempt_opengraph
diff --git a/app/services/fetch_oembed_service.rb b/app/services/fetch_oembed_service.rb
new file mode 100644
index 000000000..998228517
--- /dev/null
+++ b/app/services/fetch_oembed_service.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+class FetchOEmbedService
+  attr_reader :url, :options, :format, :endpoint_url
+
+  def call(url, options = {})
+    @url     = url
+    @options = options
+
+    discover_endpoint!
+    fetch!
+  end
+
+  private
+
+  def discover_endpoint!
+    return if html.nil?
+
+    @format = @options[:format]
+    page    = Nokogiri::HTML(html)
+
+    if @format.nil? || @format == :json
+      @endpoint_url ||= page.at_xpath('//link[@type="application/json+oembed"]')&.attribute('href')&.value
+      @format       ||= :json if @endpoint_url
+    end
+
+    if @format.nil? || @format == :xml
+      @endpoint_url ||= page.at_xpath('//link[@type="text/xml+oembed"]')&.attribute('href')&.value
+      @format       ||= :xml if @endpoint_url
+    end
+
+    return if @endpoint_url.blank?
+
+    @endpoint_url = Addressable::URI.parse(@endpoint_url).to_s
+  rescue Addressable::URI::InvalidURIError
+    @endpoint_url = nil
+  end
+
+  def fetch!
+    return if @endpoint_url.blank?
+
+    body = Request.new(:get, @endpoint_url).perform do |res|
+      res.code != 200 ? nil : res.body_with_limit
+    end
+
+    validate(parse_for_format(body)) unless body.nil?
+  rescue Oj::ParseError, Ox::ParseError
+    nil
+  end
+
+  def parse_for_format(body)
+    case @format
+    when :json
+      Oj.load(body, mode: :strict)&.with_indifferent_access
+    when :xml
+      Ox.load(body, mode: :hash_no_attrs)&.with_indifferent_access&.dig(:oembed)
+    end
+  end
+
+  def validate(oembed)
+    oembed if oembed[:version] == '1.0' && oembed[:type].present?
+  end
+
+  def html
+    return @html if defined?(@html)
+
+    @html = @options[:html] || Request.new(:get, @url).perform do |res|
+      res.code != 200 || res.mime_type != 'text/html' ? nil : res.body_with_limit
+    end
+  end
+end
diff --git a/app/services/mute_service.rb b/app/services/mute_service.rb
index 547b2efa1..c6122a152 100644
--- a/app/services/mute_service.rb
+++ b/app/services/mute_service.rb
@@ -3,8 +3,13 @@
 class MuteService < BaseService
   def call(account, target_account, notifications: nil)
     return if account.id == target_account.id
+
     mute = account.mute!(target_account, notifications: notifications)
-    BlockWorker.perform_async(account.id, target_account.id)
+    if mute.hide_notifications?
+      BlockWorker.perform_async(account.id, target_account.id)
+    else
+      FeedManager.instance.clear_from_timeline(account, target_account)
+    end
     mute
   end
 end
diff --git a/app/services/process_hashtags_service.rb b/app/services/process_hashtags_service.rb
index 990e01a4b..5b45c865f 100644
--- a/app/services/process_hashtags_service.rb
+++ b/app/services/process_hashtags_service.rb
@@ -7,7 +7,5 @@ class ProcessHashtagsService < BaseService
     tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |tag|
       status.tags << Tag.where(name: tag).first_or_initialize(name: tag)
     end
-
-    status.update(sensitive: true) if tags.include?('nsfw')
   end
 end
diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb
index dc8df4a9a..2ed6698cf 100644
--- a/app/services/process_mentions_service.rb
+++ b/app/services/process_mentions_service.rb
@@ -10,55 +10,61 @@ class ProcessMentionsService < BaseService
   def call(status)
     return unless status.local?
 
+    @status  = status
+    mentions = []
+
     status.text = status.text.gsub(Account::MENTION_RE) do |match|
-      username, domain  = $1.split('@')
+      username, domain  = Regexp.last_match(1).split('@')
       mentioned_account = Account.find_remote(username, domain)
 
-      if mention_undeliverable?(status, mentioned_account)
+      if mention_undeliverable?(mentioned_account)
         begin
-          mentioned_account = resolve_account_service.call($1)
+          mentioned_account = resolve_account_service.call(Regexp.last_match(1))
         rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::UnexpectedResponseError
           mentioned_account = nil
         end
       end
 
-      next match if mention_undeliverable?(status, mentioned_account)
+      next match if mention_undeliverable?(mentioned_account)
+
+      mentions << mentioned_account.mentions.where(status: status).first_or_create(status: status)
 
-      mentioned_account.mentions.where(status: status).first_or_create(status: status)
       "@#{mentioned_account.acct}"
     end
 
     status.save!
 
-    status.mentions.includes(:account).each do |mention|
-      create_notification(status, mention)
-    end
+    mentions.each { |mention| create_notification(mention) }
   end
 
   private
 
-  def mention_undeliverable?(status, mentioned_account)
-    mentioned_account.nil? || (!mentioned_account.local? && mentioned_account.ostatus? && status.stream_entry.hidden?)
+  def mention_undeliverable?(mentioned_account)
+    mentioned_account.nil? || (!mentioned_account.local? && mentioned_account.ostatus? && @status.stream_entry.hidden?)
   end
 
-  def create_notification(status, mention)
+  def create_notification(mention)
     mentioned_account = mention.account
 
     if mentioned_account.local?
-      NotifyService.new.call(mentioned_account, mention)
-    elsif mentioned_account.ostatus? && !status.stream_entry.hidden?
-      NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, mentioned_account.id)
+      LocalNotificationWorker.perform_async(mention.id)
+    elsif mentioned_account.ostatus? && !@status.stream_entry.hidden?
+      NotificationWorker.perform_async(ostatus_xml, @status.account_id, mentioned_account.id)
     elsif mentioned_account.activitypub?
-      ActivityPub::DeliveryWorker.perform_async(build_json(mention.status), mention.status.account_id, mentioned_account.inbox_url)
+      ActivityPub::DeliveryWorker.perform_async(activitypub_json, mention.status.account_id, mentioned_account.inbox_url)
     end
   end
 
-  def build_json(status)
-    Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
-      status,
+  def ostatus_xml
+    @ostatus_xml ||= stream_entry_to_xml(@status.stream_entry)
+  end
+
+  def activitypub_json
+    @activitypub_json ||= Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(
+      @status,
       serializer: ActivityPub::ActivitySerializer,
       adapter: ActivityPub::Adapter
-    ).as_json).sign!(status.account))
+    ).as_json).sign!(@status.account))
   end
 
   def resolve_account_service
diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb
index 8cba88f01..de8d1151d 100644
--- a/app/services/resolve_account_service.rb
+++ b/app/services/resolve_account_service.rb
@@ -189,7 +189,7 @@ class ResolveAccountService < BaseService
     return @actor_json if defined?(@actor_json)
 
     json        = fetch_resource(actor_url, false)
-    @actor_json = supported_context?(json) && ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES.include?(json['type']) ? json : nil
+    @actor_json = supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) ? json : nil
   end
 
   def atom
diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb
index c19b568cb..a068c1ed8 100644
--- a/app/services/resolve_url_service.rb
+++ b/app/services/resolve_url_service.rb
@@ -16,10 +16,9 @@ class ResolveURLService < BaseService
   private
 
   def process_url
-    case type
-    when 'Application', 'Group', 'Organization', 'Person', 'Service'
+    if equals_or_includes_any?(type, %w(Application Group Organization Person Service))
       FetchRemoteAccountService.new.call(atom_url, body, protocol)
-    when 'Note', 'Article', 'Image', 'Video'
+    elsif equals_or_includes_any?(type, %w(Note Article Image Video))
       FetchRemoteStatusService.new.call(atom_url, body, protocol)
     end
   end
diff --git a/app/validators/disallowed_hashtags_validator.rb b/app/validators/disallowed_hashtags_validator.rb
new file mode 100644
index 000000000..22c027b0f
--- /dev/null
+++ b/app/validators/disallowed_hashtags_validator.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class DisallowedHashtagsValidator < ActiveModel::Validator
+  def validate(status)
+    return unless status.local? && !status.reblog?
+
+    tags = Extractor.extract_hashtags(status.text)
+    tags.keep_if { |tag| disallowed_hashtags.include? tag.downcase }
+
+    status.errors.add(:text, I18n.t('statuses.disallowed_hashtags', tags: tags.join(', '), count: tags.size)) unless tags.empty?
+  end
+
+  private
+
+  def disallowed_hashtags
+    return @disallowed_hashtags if @disallowed_hashtags
+
+    @disallowed_hashtags = Setting.disallowed_hashtags.nil? ? [] : Setting.disallowed_hashtags
+    @disallowed_hashtags = @disallowed_hashtags.split(' ') if @disallowed_hashtags.is_a? String
+    @disallowed_hashtags = @disallowed_hashtags.map(&:downcase)
+  end
+end
diff --git a/app/views/admin/action_logs/_action_log.html.haml b/app/views/admin/action_logs/_action_log.html.haml
index ec90961cb..f059814bd 100644
--- a/app/views/admin/action_logs/_action_log.html.haml
+++ b/app/views/admin/action_logs/_action_log.html.haml
@@ -1,4 +1,4 @@
-%li.log-entry
+.log-entry
   .log-entry__header
     .log-entry__avatar
       = image_tag action_log.account.avatar.url(:original), alt: '', width: 40, height: 40, class: 'avatar'
diff --git a/app/views/admin/action_logs/index.html.haml b/app/views/admin/action_logs/index.html.haml
index bb6d7b5d7..a4d3871a9 100644
--- a/app/views/admin/action_logs/index.html.haml
+++ b/app/views/admin/action_logs/index.html.haml
@@ -1,7 +1,6 @@
 - content_for :page_title do
   = t('admin.action_logs.title')
 
-%ul
-  = render @action_logs
+= render @action_logs
 
 = paginate @action_logs
diff --git a/app/views/admin/report_notes/_report_note.html.haml b/app/views/admin/report_notes/_report_note.html.haml
index 1f621e0d3..d34dc3d15 100644
--- a/app/views/admin/report_notes/_report_note.html.haml
+++ b/app/views/admin/report_notes/_report_note.html.haml
@@ -1,9 +1,7 @@
-%li
-  %h4
-    = report_note.account.acct
-    %div{ style: 'float: right' }
-      %time.formatted{ datetime: report_note.created_at.iso8601, title: l(report_note.created_at) }
-        = l report_note.created_at
-      = table_link_to 'trash', t('admin.reports.notes.delete'), admin_report_note_path(report_note), method: :delete if can?(:destroy, report_note)
-  %div{ class: 'report-note__comment' }
+.speech-bubble
+  .speech-bubble__bubble
     = simple_format(h(report_note.content))
+  .speech-bubble__owner
+    = admin_account_link_to report_note.account
+    %time.formatted{ datetime: report_note.created_at.iso8601 }= l report_note.created_at
+    = table_link_to 'trash', t('admin.reports.notes.delete'), admin_report_note_path(report_note), method: :delete if can?(:destroy, report_note)
diff --git a/app/views/admin/reports/_account.html.haml b/app/views/admin/reports/_account.html.haml
new file mode 100644
index 000000000..22b7a0861
--- /dev/null
+++ b/app/views/admin/reports/_account.html.haml
@@ -0,0 +1,19 @@
+- size ||= 36
+
+.account.compact
+  .account__wrapper
+    - if account.nil?
+      .account__display-name
+        .account__avatar-wrapper
+          .account__avatar{ style: "background-image: url(#{full_asset_url('avatars/original/missing.png', skip_pipeline: true)}); width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px" }
+        %span.display-name
+          %strong= t 'about.contact_missing'
+          %span.display-name__account= t 'about.contact_unavailable'
+    - else
+      = link_to TagManager.instance.url_for(account), class: 'account__display-name' do
+        .account__avatar-wrapper
+          .account__avatar{ style: "background-image: url(#{account.avatar.url}); width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px" }
+        %span.display-name
+          %bdi
+            %strong.display-name__html.emojify= display_name(account)
+          %span.display-name__account @#{account.acct}
diff --git a/app/views/admin/reports/_account_details.html.haml b/app/views/admin/reports/_account_details.html.haml
deleted file mode 100644
index a8af39bef..000000000
--- a/app/views/admin/reports/_account_details.html.haml
+++ /dev/null
@@ -1,20 +0,0 @@
-.table-wrapper
-  %table.table
-    %tbody
-      %tr
-        %td= t('admin.reports.account.created_reports')
-        %td= link_to pluralize(account.reports.count, t('admin.reports.account.report')), admin_reports_path(account_id: account.id)
-      %tr
-        %td= t('admin.reports.account.targeted_reports')
-        %td= link_to pluralize(account.targeted_reports.count, t('admin.reports.account.report')), admin_reports_path(target_account_id: account.id)
-      %tr
-        %td= t('admin.reports.account.moderation_notes')
-        %td= link_to pluralize(account.targeted_moderation_notes.count, t('admin.reports.account.note')), admin_reports_path(target_account_id: account.id)
-      - if account.silenced? || account.suspended?
-        %tr
-          %td= t('admin.reports.account.moderation.title')
-          %td
-            - if account.silenced?
-              %p= t('admin.reports.account.moderation.silenced')
-            - if account.suspended?
-              %p= t('admin.reports.account.moderation.suspended')
diff --git a/app/views/admin/reports/_action_log.html.haml b/app/views/admin/reports/_action_log.html.haml
new file mode 100644
index 000000000..024078eb9
--- /dev/null
+++ b/app/views/admin/reports/_action_log.html.haml
@@ -0,0 +1,6 @@
+.speech-bubble.positive
+  .speech-bubble__bubble
+    = t("admin.action_logs.actions.#{action_log.action}_#{action_log.target_type.underscore}", name: content_tag(:span, action_log.account.username, class: 'username'), target: content_tag(:span, log_target(action_log), class: 'target')).html_safe
+  .speech-bubble__owner
+    = admin_account_link_to(action_log.account)
+    %time.formatted{ datetime: action_log.created_at.iso8601 }= l action_log.created_at
diff --git a/app/views/admin/reports/_report.html.haml b/app/views/admin/reports/_report.html.haml
index 84db00ad5..d6c881955 100644
--- a/app/views/admin/reports/_report.html.haml
+++ b/app/views/admin/reports/_report.html.haml
@@ -2,9 +2,9 @@
   %td.id
     = "##{report.id}"
   %td.target
-    = link_to report.target_account.acct, admin_account_path(report.target_account.id)
+    = admin_account_link_to report.target_account
   %td.reporter
-    = link_to report.account.acct, admin_account_path(report.account.id)
+    = admin_account_link_to report.account
   %td
     %div{ title: report.comment }
       = truncate(report.comment, length: 30, separator: ' ')
@@ -21,6 +21,6 @@
     - if report.assigned_account.nil?
       \-
     - else
-      = link_to report.assigned_account.acct, admin_account_path(report.assigned_account.id)
+      = admin_account_link_to report.assigned_account
   %td
     = table_link_to 'circle', t('admin.reports.view'), admin_report_path(report)
diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml
new file mode 100644
index 000000000..137609539
--- /dev/null
+++ b/app/views/admin/reports/_status.html.haml
@@ -0,0 +1,28 @@
+.batch-table__row
+  %label.batch-table__row__select.batch-checkbox
+    = f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id
+  .batch-table__row__content
+    .status__content><
+      - unless status.spoiler_text.blank?
+        %p><
+          %strong= Formatter.instance.format_spoiler(status)
+
+      = Formatter.instance.format(status)
+
+    - unless status.media_attachments.empty?
+      - if status.media_attachments.first.video?
+        - video = status.media_attachments.first
+        = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 610, height: 343, inline: true
+      - else
+        = react_component :media_gallery, height: 343, sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }
+
+    .detailed-status__meta
+      = link_to TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener' do
+        %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
+      ·
+      = fa_visibility_icon(status)
+      = t("statuses.visibilities.#{status.visibility}")
+      - if status.sensitive?
+        ·
+        = fa_icon('eye-slash fw')
+        = t('stream_entries.sensitive_content')
diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml
index c3baaf6be..44a531f2c 100644
--- a/app/views/admin/reports/index.html.haml
+++ b/app/views/admin/reports/index.html.haml
@@ -8,20 +8,17 @@
       %li= filter_link_to t('admin.reports.unresolved'), resolved: nil
       %li= filter_link_to t('admin.reports.resolved'), resolved: '1'
 
-= form_tag do
-
-  .table-wrapper
-    %table.table
-      %thead
-        %tr
-          -# %th
-          %th= t('admin.reports.id')
-          %th= t('admin.reports.target')
-          %th= t('admin.reports.reported_by')
-          %th= t('admin.reports.report_contents')
-          %th= t('admin.reports.assigned')
-          %th
-      %tbody
-        = render @reports
+.table-wrapper
+  %table.table
+    %thead
+      %tr
+        %th= t('admin.reports.id')
+        %th= t('admin.reports.target')
+        %th= t('admin.reports.reported_by')
+        %th= t('admin.reports.report_contents')
+        %th= t('admin.reports.assigned')
+        %th
+    %tbody
+      = render @reports
 
 = paginate @reports
diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml
index 60a8cab8e..cbfbdcfa9 100644
--- a/app/views/admin/reports/show.html.haml
+++ b/app/views/admin/reports/show.html.haml
@@ -11,16 +11,28 @@
   - else
     = link_to t('admin.reports.mark_as_unresolved'), admin_report_path(@report, outcome: 'reopen'), method: :put, class: 'button'
 
+%hr.spacer
+
 .table-wrapper
   %table.table.inline-table
     %tbody
       %tr
+        %th= t('admin.reports.reported_account')
+        %td= admin_account_link_to @report.target_account
+        %td= table_link_to 'flag', pluralize(@report.target_account.targeted_reports.count, t('admin.reports.account.report')), admin_reports_path(target_account_id: @report.target_account.id)
+        %td= table_link_to 'file', pluralize(@report.target_account.targeted_moderation_notes.count, t('admin.reports.account.note')), admin_reports_path(target_account_id: @report.target_account.id)
+      %tr
+        %th= t('admin.reports.reported_by')
+        %td= admin_account_link_to @report.account
+        %td= table_link_to 'flag', pluralize(@report.account.targeted_reports.count, t('admin.reports.account.report')), admin_reports_path(target_account_id: @report.account.id)
+        %td= table_link_to 'file', pluralize(@report.account.targeted_moderation_notes.count, t('admin.reports.account.note')), admin_reports_path(target_account_id: @report.account.id)
+      %tr
         %th= t('admin.reports.created_at')
-        %td{colspan: 2}
+        %td{ colspan: 3 }
           %time.formatted{ datetime: @report.created_at.iso8601 }
       %tr
         %th= t('admin.reports.updated_at')
-        %td{colspan: 2}
+        %td{ colspan: 3 }
           %time.formatted{ datetime: @report.updated_at.iso8601 }
       %tr
         %th= t('admin.reports.status')
@@ -29,14 +41,14 @@
             = t('admin.reports.resolved')
           - else
             = t('admin.reports.unresolved')
-        %td{style: "text-align: right; overflow: hidden;"}
+        %td{ colspan: 2 }
           - if @report.action_taken?
             = table_link_to 'envelope-open', t('admin.reports.reopen'), admin_report_path(@report, outcome: 'reopen'), method: :put
       - if !@report.action_taken_by_account.nil?
         %tr
           %th= t('admin.reports.action_taken_by')
-          %td{colspan: 2}
-            = @report.action_taken_by_account.acct
+          %td{ colspan: 3 }
+            = admin_account_link_to @report.action_taken_by_account
       - else
         %tr
           %th= t('admin.reports.assigned')
@@ -44,78 +56,55 @@
             - if @report.assigned_account.nil?
               \-
             - else
-              = link_to @report.assigned_account.acct, admin_account_path(@report.assigned_account.id)
-          %td{style: "text-align: right"}
+              = admin_account_link_to @report.assigned_account
+          %td
             - if @report.assigned_account != current_user.account
               = table_link_to 'user', t('admin.reports.assign_to_self'), admin_report_path(@report, outcome: 'assign_to_self'), method: :put
+          %td
             - if !@report.assigned_account.nil?
               = table_link_to 'trash', t('admin.reports.unassign'), admin_report_path(@report, outcome: 'unassign'), method: :put
 
-%hr{ class: "section-break"}/
-
-.report-accounts
-  .report-accounts__item
-    %h3= t('admin.reports.reported_account')
-    = render 'authorize_follows/card', account: @report.target_account, admin: true
-    = render 'admin/reports/account_details', account: @report.target_account
-  .report-accounts__item
-    %h3= t('admin.reports.reported_by')
-    = render 'authorize_follows/card', account: @report.account, admin: true
-    = render 'admin/reports/account_details', account: @report.account
-
-%h3= t('admin.reports.comment.label')
+%hr.spacer
 
-= simple_format(@report.comment.presence || t('admin.reports.comment.none'))
+.speech-bubble
+  .speech-bubble__bubble= simple_format(@report.comment.presence || t('admin.reports.comment.none'))
+  .speech-bubble__owner
+    = admin_account_link_to @report.account
+    %time.formatted{ datetime: @report.created_at.iso8601 }
 
 - unless @report.statuses.empty?
-  %hr/
-
-  %h3= t('admin.reports.statuses')
+  %hr.spacer/
 
   = form_for(@form, url: admin_report_reported_statuses_path(@report.id)) do |f|
-    .batch-form-box
-      .batch-checkbox-all
-        = check_box_tag :batch_checkbox_all, nil, false
-      = f.select :action, Form::StatusBatch::ACTION_TYPE.map{|action| [t("admin.statuses.batch.#{action}"), action]}
-      = f.submit t('admin.statuses.execute'), data: { confirm: t('admin.reports.are_you_sure') }, class: 'button'
-      .media-spoiler-toggle-buttons
-        .media-spoiler-show-button.button= t('admin.statuses.media.show')
-        .media-spoiler-hide-button.button= t('admin.statuses.media.hide')
-    - @report.statuses.each do |status|
-      .report-status{ data: { id: status.id } }
-        .batch-checkbox
-          = f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id
-        .activity-stream.activity-stream-headless
-          .entry= render 'stream_entries/simple_status', status: status
-        .report-status__actions
-          - unless status.media_attachments.empty?
-            = link_to admin_report_reported_status_path(@report, status, status: { sensitive: !status.sensitive }), method: :put, class: 'icon-button nsfw-button', title: t("admin.reports.nsfw.#{!status.sensitive}") do
-              = fa_icon status.sensitive? ? 'eye' : 'eye-slash'
-          = link_to admin_report_reported_status_path(@report, status), method: :delete, class: 'icon-button trash-button', title: t('admin.reports.delete'), data: { confirm: t('admin.reports.are_you_sure') }, remote: true do
-            = fa_icon 'trash'
-
-%hr{ class: "section-break"}/
+    .batch-table
+      .batch-table__toolbar
+        %label.batch-table__toolbar__select.batch-checkbox-all
+          = check_box_tag :batch_checkbox_all, nil, false
+        .batch-table__toolbar__actions
+          = f.button safe_join([fa_icon('eye-slash'), t('admin.statuses.batch.nsfw_on')]), name: :nsfw_on, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
+          = f.button safe_join([fa_icon('eye'), t('admin.statuses.batch.nsfw_off')]), name: :nsfw_off, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
+          = f.button safe_join([fa_icon('trash'), t('admin.statuses.batch.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
+      .batch-table__body
+        = render partial: 'admin/reports/status', collection: @report.statuses, locals: { f: f }
 
-%h3= t('admin.reports.notes.label')
+%hr.spacer/
 
-- if @report_notes.length > 0
-  %ul
-    = render @report_notes
+- @report_notes.each do |item|
+  - if item.is_a?(Admin::ActionLog)
+    = render partial: 'action_log', locals: { action_log: item }
+  - elsif item.is_a?(ReportNote)
+    = render item
 
-%h4= t('admin.reports.notes.new_label')
-= form_for @report_note, url: admin_report_notes_path, html: { class: 'report-note__form' } do |f|
+= simple_form_for @report_note, url: admin_report_notes_path do |f|
   = render 'shared/error_messages', object: @report_note
-  = f.text_area :content, placeholder: t('admin.reports.notes.placeholder'), rows: 6, class: 'report-note__textarea'
-  = f.hidden_field :report_id
-  %div{ class: 'report-note__buttons' }
-    - if @report.unresolved?
-      = f.submit t('admin.reports.notes.create_and_resolve'), name: :create_and_resolve, class: 'button report-note__button'
-    - else
-      = f.submit t('admin.reports.notes.create_and_unresolve'), name: :create_and_unresolve, class: 'button report-note__button'
-    = f.submit t('admin.reports.notes.create'), class: 'button report-note__button'
+  = f.input :report_id, as: :hidden
 
-- if @report_history.length > 0
-  %h3= t('admin.reports.history')
+  .field-group
+    = f.input :content, placeholder: t('admin.reports.notes.placeholder'), rows: 6
 
-  %ul
-    = render @report_history
+  .actions
+    - if @report.unresolved?
+      = f.button :button, t('admin.reports.notes.create_and_resolve'), name: :create_and_resolve, type: :submit
+    - else
+      = f.button :button, t('admin.reports.notes.create_and_unresolve'), name: :create_and_unresolve, type: :submit
+    = f.button :button, t('admin.reports.notes.create'), type: :submit
diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml
index e8a81656c..789de47d1 100644
--- a/app/views/home/index.html.haml
+++ b/app/views/home/index.html.haml
@@ -1,4 +1,9 @@
 - content_for :header_tags do
+  = preload_link_tag asset_pack_path('features/getting_started.js'), crossorigin: 'anonymous'
+  = preload_link_tag asset_pack_path('features/compose.js'), crossorigin: 'anonymous'
+  = preload_link_tag asset_pack_path('features/home_timeline.js'), crossorigin: 'anonymous'
+  = preload_link_tag asset_pack_path('features/notifications.js'), crossorigin: 'anonymous'
+
   %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
 
diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml
index e1122d5a2..afc66d148 100644
--- a/app/views/stream_entries/_detailed_status.html.haml
+++ b/app/views/stream_entries/_detailed_status.html.haml
@@ -22,11 +22,11 @@
   - if !status.media_attachments.empty?
     - if status.media_attachments.first.video?
       - video = status.media_attachments.first
-      %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 670, height: 380, detailed: true, inline: true) }}
+      = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 670, height: 380, detailed: true, inline: true
     - else
-      %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}
+      = react_component :media_gallery, height: 380, sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }
   - elsif status.preview_cards.first
-    %div{ data: { component: 'Card', props: Oj.dump('maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_cards.first, serializer: REST::PreviewCardSerializer).as_json) }}
+    = react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_cards.first, serializer: REST::PreviewCardSerializer).as_json
 
   .detailed-status__meta
     %data.dt-published{ value: status.created_at.to_time.iso8601 }
diff --git a/app/views/stream_entries/_more.html.haml b/app/views/stream_entries/_more.html.haml
new file mode 100644
index 000000000..9b1dfe4a7
--- /dev/null
+++ b/app/views/stream_entries/_more.html.haml
@@ -0,0 +1,2 @@
+= link_to url, class: 'more light'  do
+  = t('statuses.show_more')
diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml
index 2ad1f5120..cc2b6abe8 100644
--- a/app/views/stream_entries/_simple_status.html.haml
+++ b/app/views/stream_entries/_simple_status.html.haml
@@ -20,9 +20,10 @@
         %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
     .e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }<
       = Formatter.instance.format(status, custom_emojify: true)
-      - unless status.media_attachments.empty?
-        - if status.media_attachments.first.video?
-          - video = status.media_attachments.first
-          %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 610, height: 343) }}
-        - else
-          %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 343, sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}
+
+  - unless status.media_attachments.empty?
+    - if status.media_attachments.first.video?
+      - video = status.media_attachments.first
+      = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 610, height: 343, inline: true
+    - else
+      = react_component :media_gallery, height: 343, sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }
diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml
index 2d0dafcb7..9764bc74d 100644
--- a/app/views/stream_entries/_status.html.haml
+++ b/app/views/stream_entries/_status.html.haml
@@ -5,19 +5,19 @@
   is_successor    ||= false
   direct_reply_id ||= false
   parent_id       ||= false
-  is_direct_parent = direct_reply_id == status.id
-  is_direct_child  = parent_id == status.in_reply_to_id
-  centered ||= include_threads && !is_predecessor && !is_successor
-  h_class       = microformats_h_class(status, is_predecessor, is_successor, include_threads)
-  style_classes = style_classes(status, is_predecessor, is_successor, include_threads)
-  mf_classes    = microformats_classes(status, is_direct_parent, is_direct_child)
-  entry_classes = h_class + ' ' + mf_classes + ' ' + style_classes
+  is_direct_parent  = direct_reply_id == status.id
+  is_direct_child   = parent_id == status.in_reply_to_id
+  centered        ||= include_threads && !is_predecessor && !is_successor
+  h_class           = microformats_h_class(status, is_predecessor, is_successor, include_threads)
+  style_classes     = style_classes(status, is_predecessor, is_successor, include_threads)
+  mf_classes        = microformats_classes(status, is_direct_parent, is_direct_child)
+  entry_classes     = h_class + ' ' + mf_classes + ' ' + style_classes
 
 - if status.reply? && include_threads
   - if @next_ancestor
     .entry{ class: entry_classes }
-      = link_to short_account_status_url(@next_ancestor.account.username, @next_ancestor), class: 'more light'  do
-        = t('statuses.show_more')
+      = render 'stream_entries/more', url: TagManager.instance.url_for(@next_ancestor)
+
   = render partial: 'stream_entries/status', collection: @ancestors, as: :status, locals: { is_predecessor: true, direct_reply_id: status.in_reply_to_id }
 
 .entry{ class: entry_classes }
@@ -40,4 +40,15 @@
   = render (centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status'), status: status.proper
 
 - if include_threads
-  = render partial: 'stream_entries/status', collection: @descendants, as: :status, locals: { is_successor: true, parent_id: status.id }
+  - if @since_descendant_thread_id
+    .entry{ class: entry_classes }
+      = render 'stream_entries/more', url: short_account_status_url(status.account.username, status, max_descendant_thread_id: @since_descendant_thread_id + 1)
+  - @descendant_threads.each do |thread|
+    = render partial: 'stream_entries/status', collection: thread[:statuses], as: :status, locals: { is_successor: true, parent_id: status.id }
+
+    - if thread[:next_status]
+      .entry{ class: entry_classes }
+        = render 'stream_entries/more', url: TagManager.instance.url_for(thread[:next_status])
+  - if @next_descendant_thread
+    .entry{ class: entry_classes }
+      = render 'stream_entries/more', url: short_account_status_url(status.account.username, status, since_descendant_thread_id: @max_descendant_thread_id - 1)
diff --git a/app/views/well_known/host_meta/show.xml.ruby b/app/views/well_known/host_meta/show.xml.ruby
index 07d026471..0a6bdc322 100644
--- a/app/views/well_known/host_meta/show.xml.ruby
+++ b/app/views/well_known/host_meta/show.xml.ruby
@@ -1,5 +1,13 @@
-Nokogiri::XML::Builder.new do |xml|
-  xml.XRD(xmlns: 'http://docs.oasis-open.org/ns/xri/xrd-1.0') do
-    xml.Link(rel: 'lrdd', type: 'application/xrd+xml', template: @webfinger_template)
+doc = Ox::Document.new(version: '1.0')
+
+doc << Ox::Element.new('XRD').tap do |xrd|
+  xrd['xmlns'] = 'http://docs.oasis-open.org/ns/xri/xrd-1.0'
+
+  xrd << Ox::Element.new('Link').tap do |link|
+    link['rel']      = 'lrdd'
+    link['type']     = 'application/xrd+xml'
+    link['template'] = @webfinger_template
   end
-end.to_xml
+end
+
+('<?xml version="1.0" encoding="UTF-8"?>' + Ox.dump(doc, effort: :tolerant)).force_encoding('UTF-8')
diff --git a/app/views/well_known/webfinger/show.xml.ruby b/app/views/well_known/webfinger/show.xml.ruby
index 0c7289d6a..4352a24e9 100644
--- a/app/views/well_known/webfinger/show.xml.ruby
+++ b/app/views/well_known/webfinger/show.xml.ruby
@@ -1,13 +1,44 @@
-Nokogiri::XML::Builder.new do |xml|
-  xml.XRD(xmlns: 'http://docs.oasis-open.org/ns/xri/xrd-1.0') do
-    xml.Subject @account.to_webfinger_s
-    xml.Alias short_account_url(@account)
-    xml.Alias account_url(@account)
-    xml.Link(rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(@account))
-    xml.Link(rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(@account, format: 'atom'))
-    xml.Link(rel: 'self', type: 'application/activity+json', href: account_url(@account))
-    xml.Link(rel: 'salmon', href: api_salmon_url(@account.id))
-    xml.Link(rel: 'magic-public-key', href: "data:application/magic-public-key,#{@account.magic_key}")
-    xml.Link(rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_follow_url}?acct={uri}")
-  end
-end.to_xml
+doc = Ox::Document.new(version: '1.0')
+
+doc << Ox::Element.new('XRD').tap do |xrd|
+  xrd['xmlns'] = 'http://docs.oasis-open.org/ns/xri/xrd-1.0'
+
+  xrd << (Ox::Element.new('Subject') << @account.to_webfinger_s)
+  xrd << (Ox::Element.new('Alias') << short_account_url(@account))
+  xrd << (Ox::Element.new('Alias') << account_url(@account))
+
+  xrd << Ox::Element.new('Link').tap do |link|
+    link['rel']      = 'http://webfinger.net/rel/profile-page'
+    link['type']     = 'text/html'
+    link['href']     = short_account_url(@account)
+  end
+
+  xrd << Ox::Element.new('Link').tap do |link|
+    link['rel']      = 'http://schemas.google.com/g/2010#updates-from'
+    link['type']     = 'application/atom+xml'
+    link['href']     = account_url(@account, format: 'atom')
+  end
+
+  xrd << Ox::Element.new('Link').tap do |link|
+    link['rel']      = 'self'
+    link['type']     = 'application/activity+json'
+    link['href']     = account_url(@account)
+  end
+
+  xrd << Ox::Element.new('Link').tap do |link|
+    link['rel']      = 'salmon'
+    link['href']     = api_salmon_url(@account.id)
+  end
+
+  xrd << Ox::Element.new('Link').tap do |link|
+    link['rel']      = 'magic-public-key'
+    link['href']     = "data:application/magic-public-key,#{@account.magic_key}"
+  end
+
+  xrd << Ox::Element.new('Link').tap do |link|
+    link['rel']      = 'http://ostatus.org/schema/1.0/subscribe'
+    link['template'] = "#{authorize_follow_url}?acct={uri}"
+  end
+end
+
+('<?xml version="1.0" encoding="UTF-8"?>' + Ox.dump(doc, effort: :tolerant)).force_encoding('UTF-8')
diff --git a/app/workers/activitypub/processing_worker.rb b/app/workers/activitypub/processing_worker.rb
index 0e2e0eddd..bb9adf64b 100644
--- a/app/workers/activitypub/processing_worker.rb
+++ b/app/workers/activitypub/processing_worker.rb
@@ -6,6 +6,6 @@ class ActivityPub::ProcessingWorker
   sidekiq_options backtrace: true
 
   def perform(account_id, body)
-    ActivityPub::ProcessCollectionService.new.call(body, Account.find(account_id), override_timestamps: true)
+    ActivityPub::ProcessCollectionService.new.call(body, Account.find(account_id))
   end
 end
diff --git a/app/workers/local_notification_worker.rb b/app/workers/local_notification_worker.rb
new file mode 100644
index 000000000..748270563
--- /dev/null
+++ b/app/workers/local_notification_worker.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class LocalNotificationWorker
+  include Sidekiq::Worker
+
+  def perform(mention_id)
+    mention = Mention.find(mention_id)
+    NotifyService.new.call(mention.account, mention)
+  rescue ActiveRecord::RecordNotFound
+    true
+  end
+end
diff --git a/app/workers/processing_worker.rb b/app/workers/processing_worker.rb
index 978c3aba2..5df404bcc 100644
--- a/app/workers/processing_worker.rb
+++ b/app/workers/processing_worker.rb
@@ -6,6 +6,6 @@ class ProcessingWorker
   sidekiq_options backtrace: true
 
   def perform(account_id, body)
-    ProcessFeedService.new.call(body, Account.find(account_id), override_timestamps: true)
+    ProcessFeedService.new.call(body, Account.find(account_id))
   end
 end
diff --git a/app/workers/scheduler/backup_cleanup_scheduler.rb b/app/workers/scheduler/backup_cleanup_scheduler.rb
index 7a9d4f894..5ab16c057 100644
--- a/app/workers/scheduler/backup_cleanup_scheduler.rb
+++ b/app/workers/scheduler/backup_cleanup_scheduler.rb
@@ -1,5 +1,4 @@
 # frozen_string_literal: true
-require 'sidekiq-scheduler'
 
 class Scheduler::BackupCleanupScheduler
   include Sidekiq::Worker
diff --git a/app/workers/scheduler/doorkeeper_cleanup_scheduler.rb b/app/workers/scheduler/doorkeeper_cleanup_scheduler.rb
index 6488798cd..bab4ae886 100644
--- a/app/workers/scheduler/doorkeeper_cleanup_scheduler.rb
+++ b/app/workers/scheduler/doorkeeper_cleanup_scheduler.rb
@@ -1,5 +1,4 @@
 # frozen_string_literal: true
-require 'sidekiq-scheduler'
 
 class Scheduler::DoorkeeperCleanupScheduler
   include Sidekiq::Worker
diff --git a/app/workers/scheduler/email_scheduler.rb b/app/workers/scheduler/email_scheduler.rb
index 24d0c0ebe..36866061b 100644
--- a/app/workers/scheduler/email_scheduler.rb
+++ b/app/workers/scheduler/email_scheduler.rb
@@ -1,5 +1,4 @@
 # frozen_string_literal: true
-require 'sidekiq-scheduler'
 
 class Scheduler::EmailScheduler
   include Sidekiq::Worker
diff --git a/app/workers/scheduler/feed_cleanup_scheduler.rb b/app/workers/scheduler/feed_cleanup_scheduler.rb
index 23fa7672b..42cf14128 100644
--- a/app/workers/scheduler/feed_cleanup_scheduler.rb
+++ b/app/workers/scheduler/feed_cleanup_scheduler.rb
@@ -1,5 +1,4 @@
 # frozen_string_literal: true
-require 'sidekiq-scheduler'
 
 class Scheduler::FeedCleanupScheduler
   include Sidekiq::Worker
diff --git a/app/workers/scheduler/ip_cleanup_scheduler.rb b/app/workers/scheduler/ip_cleanup_scheduler.rb
index a33ca031e..613a5e336 100644
--- a/app/workers/scheduler/ip_cleanup_scheduler.rb
+++ b/app/workers/scheduler/ip_cleanup_scheduler.rb
@@ -1,5 +1,4 @@
 # frozen_string_literal: true
-require 'sidekiq-scheduler'
 
 class Scheduler::IpCleanupScheduler
   include Sidekiq::Worker
diff --git a/app/workers/scheduler/media_cleanup_scheduler.rb b/app/workers/scheduler/media_cleanup_scheduler.rb
index ce32ce314..c35686fcb 100644
--- a/app/workers/scheduler/media_cleanup_scheduler.rb
+++ b/app/workers/scheduler/media_cleanup_scheduler.rb
@@ -1,5 +1,4 @@
 # frozen_string_literal: true
-require 'sidekiq-scheduler'
 
 class Scheduler::MediaCleanupScheduler
   include Sidekiq::Worker
diff --git a/app/workers/scheduler/subscriptions_cleanup_scheduler.rb b/app/workers/scheduler/subscriptions_cleanup_scheduler.rb
index 3b9211e81..af2ae3120 100644
--- a/app/workers/scheduler/subscriptions_cleanup_scheduler.rb
+++ b/app/workers/scheduler/subscriptions_cleanup_scheduler.rb
@@ -1,7 +1,5 @@
 # frozen_string_literal: true
 
-require 'sidekiq-scheduler'
-
 class Scheduler::SubscriptionsCleanupScheduler
   include Sidekiq::Worker
 
diff --git a/app/workers/scheduler/subscriptions_scheduler.rb b/app/workers/scheduler/subscriptions_scheduler.rb
index 469a3d2a6..dc16e85c2 100644
--- a/app/workers/scheduler/subscriptions_scheduler.rb
+++ b/app/workers/scheduler/subscriptions_scheduler.rb
@@ -1,8 +1,5 @@
 # frozen_string_literal: true
 
-require 'sidekiq-scheduler'
-require 'sidekiq-bulk'
-
 class Scheduler::SubscriptionsScheduler
   include Sidekiq::Worker
 
diff --git a/app/workers/scheduler/user_cleanup_scheduler.rb b/app/workers/scheduler/user_cleanup_scheduler.rb
index a8f8fbd83..245536cea 100644
--- a/app/workers/scheduler/user_cleanup_scheduler.rb
+++ b/app/workers/scheduler/user_cleanup_scheduler.rb
@@ -1,5 +1,4 @@
 # frozen_string_literal: true
-require 'sidekiq-scheduler'
 
 class Scheduler::UserCleanupScheduler
   include Sidekiq::Worker
diff --git a/app/workers/soft_block_domain_followers_worker.rb b/app/workers/soft_block_domain_followers_worker.rb
index ce76683c5..85445c7fb 100644
--- a/app/workers/soft_block_domain_followers_worker.rb
+++ b/app/workers/soft_block_domain_followers_worker.rb
@@ -1,7 +1,5 @@
 # frozen_string_literal: true
 
-require 'sidekiq-bulk'
-
 class SoftBlockDomainFollowersWorker
   include Sidekiq::Worker
 
diff --git a/config/application.rb b/config/application.rb
index fdb534343..77da5cc2e 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -42,8 +42,10 @@ module Mastodon
       :bg,
       :ca,
       :de,
+      :el,
       :eo,
       :es,
+      :eu,
       :fa,
       :fi,
       :fr,
@@ -68,6 +70,7 @@ module Mastodon
       :sr,
       :'sr-Latn',
       :sv,
+      :te,
       :th,
       :tr,
       :uk,
diff --git a/config/deploy.rb b/config/deploy.rb
index 180dd1c2a..e0cd60f54 100644
--- a/config/deploy.rb
+++ b/config/deploy.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-lock '3.10.1'
+lock '3.10.2'
 
 set :repo_url, ENV.fetch('REPO', 'https://github.com/tootsuite/mastodon.git')
 set :branch, ENV.fetch('BRANCH', 'master')
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 2c8471ddd..16c0ef941 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -52,7 +52,7 @@ Rails.application.configure do
   config.log_tags = [:request_id]
 
   # Use a different cache store in production.
-  config.cache_store = :redis_store, ENV['REDIS_URL'], REDIS_CACHE_PARAMS
+  config.cache_store = :redis_store, ENV['CACHE_REDIS_URL'], REDIS_CACHE_PARAMS
 
   # Ignore bad email addresses and do not raise email delivery errors.
   # Set this to true and configure the email server for immediate delivery to raise delivery errors.
diff --git a/config/initializers/http_client_proxy.rb b/config/initializers/http_client_proxy.rb
new file mode 100644
index 000000000..f5026d59e
--- /dev/null
+++ b/config/initializers/http_client_proxy.rb
@@ -0,0 +1,24 @@
+Rails.application.configure do
+  config.x.http_client_proxy = {}
+  if ENV['http_proxy'].present?
+    proxy = URI.parse(ENV['http_proxy'])
+    raise "Unsupported proxy type: #{proxy.scheme}" unless %w(http https).include? proxy.scheme
+    raise "No proxy host" unless proxy.host
+
+    host = proxy.host
+    host = host[1...-1] if host[0] == '[' #for IPv6 address
+    config.x.http_client_proxy[:proxy] = { proxy_address: host, proxy_port: proxy.port, proxy_username: proxy.user, proxy_password: proxy.password }.compact
+  end
+
+  config.x.access_to_hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
+  config.x.hidden_service_via_transparent_proxy = ENV['HIDDEN_SERVICE_VIA_TRANSPARENT_PROXY'] == 'true'
+end
+
+module Goldfinger
+  def self.finger(uri, opts = {})
+    to_hidden = /\.(onion|i2p)(:\d+)?$/.match(uri)
+    raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if !Rails.configuration.x.access_to_hidden_service && to_hidden
+    opts = opts.merge(Rails.configuration.x.http_client_proxy).merge(ssl: !to_hidden)
+    Goldfinger::Client.new(uri, opts).finger
+  end
+end
diff --git a/config/initializers/json_ld.rb b/config/initializers/json_ld.rb
deleted file mode 100644
index 2ddc7352d..000000000
--- a/config/initializers/json_ld.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../lib/json_ld/identity'
-require_relative '../../lib/json_ld/security'
-require_relative '../../lib/json_ld/activitystreams'
diff --git a/config/initializers/oembed.rb b/config/initializers/oembed.rb
deleted file mode 100644
index 208e586cb..000000000
--- a/config/initializers/oembed.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../app/lib/provider_discovery'
-OEmbed::Providers.register_fallback(ProviderDiscovery)
diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb
index b35452f04..0ca0a7e7f 100644
--- a/config/initializers/rack_attack.rb
+++ b/config/initializers/rack_attack.rb
@@ -53,6 +53,10 @@ class Rack::Attack
     req.ip if req.api_request?
   end
 
+  throttle('throttle_media', limit: 30, period: 30.minutes) do |req|
+    req.authenticated_user_id if req.post? && req.path.start_with?('/api/v1/media')
+  end
+
   throttle('protected_paths', limit: 25, period: 5.minutes) do |req|
     req.ip if req.post? && req.path =~ PROTECTED_PATHS_REGEX
   end
diff --git a/config/locales/activerecord.eu.yml b/config/locales/activerecord.eu.yml
new file mode 100644
index 000000000..7b0ebe0b0
--- /dev/null
+++ b/config/locales/activerecord.eu.yml
@@ -0,0 +1,9 @@
+---
+eu:
+  activerecord:
+    errors:
+      models:
+        account:
+          attributes:
+            username:
+              invalid: letrak, zenbakiak eta gidoi baxuak besterik ez
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index 8b9a6688a..e9ca3038e 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -151,7 +151,7 @@ ar:
         memorialize_account: لقد قام %{name} بتحويل حساب %{target} إلى صفحة تذكارية
         promote_user: "%{name} قام بترقية المستخدم %{target}"
         reset_password_user: "%{name} لقد قام بإعادة تعيين الكلمة السرية الخاصة بـ %{target}"
-        resolve_report: قام %{name} بإلغاء التقرير المُرسَل مِن طرف %{target}
+        resolve_report: قام %{name} بحل التقرير %{target}
         silence_account: لقد قام %{name} بكتم حساب %{target}
         suspend_account: لقد قام %{name} بتعليق حساب %{target}
         unsilence_account: لقد قام %{name} بإلغاء الكتم عن حساب %{target}
@@ -240,7 +240,6 @@ ar:
       action_taken_by: تم اتخاذ الإجراء مِن طرف
       are_you_sure: هل أنت متأكد ؟
       comment:
-        label: تعليق
         none: لا شيء
       delete: حذف
       id: معرّف ID
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 61daddc66..063003218 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -4,6 +4,7 @@ ca:
     about_hashtag_html: Aquests són toots públics etiquetats amb <strong>#%{hashtag}</strong>. Pots interactuar amb ells si tens un compte a qualsevol lloc del fediverse.
     about_mastodon_html: Mastodon és una xarxa social basada en protocols web oberts i en programari lliure i de codi obert. Està descentralitzat com el correu electrònic.
     about_this: Quant a
+    administered_by: 'Administrat per:'
     closed_registrations: Actualment, el registre està tancat en aquesta instància. Malgrat això! Pots trobar una altra instància per fer-te un compte i obtenir accés a la mateixa xarxa des d'allà.
     contact: Contacte
     contact_missing: No configurat
@@ -60,7 +61,15 @@ ca:
       destroyed_msg: Nota de moderació destruïda amb èxit!
     accounts:
       are_you_sure: N'estàs segur?
+      avatar: Avatar
       by_domain: Domini
+      change_email:
+        changed_msg: El correu electrònic del compte s'ha canviat correctament!
+        current_email: Correu electrònic actual
+        label: Canviar l'adreça de correu
+        new_email: Nou correu
+        submit: Canviar adreça de correu
+        title: Canviar adreça de correu de %{username}
       confirm: Confirma
       confirmed: Confirmat
       demote: Degrada
@@ -108,6 +117,7 @@ ca:
       public: Públic
       push_subscription_expires: La subscripció PuSH expira
       redownload: Actualitza l'avatar
+      remove_avatar: Eliminar avatar
       reset: Reinicialitza
       reset_password: Restableix la contrasenya
       resubscribe: Torna a subscriure
@@ -128,6 +138,7 @@ ca:
       statuses: Estats
       subscribe: Subscriu
       title: Comptes
+      unconfirmed_email: Correu electrònic sense confirmar
       undo_silenced: Deixa de silenciar
       undo_suspension: Desfés la suspensió
       unsubscribe: Cancel·la la subscripció
@@ -135,6 +146,8 @@ ca:
       web: Web
     action_logs:
       actions:
+        assigned_to_self_report: "%{name} han assignat l'informe %{target} a ells mateixos"
+        change_email_user: "%{name} ha canviat l'adreça de correu electrònic del usuari %{target}"
         confirm_user: "%{name} ha confirmat l'adreça de correu electrònic de l'usuari %{target}"
         create_custom_emoji: "%{name} ha pujat un nou emoji %{target}"
         create_domain_block: "%{name} ha blocat el domini %{target}"
@@ -150,10 +163,13 @@ ca:
         enable_user: "%{name} ha activat l'accés per a l'usuari %{target}"
         memorialize_account: "%{name} ha convertit el compte %{target} en una pàgina de memorial"
         promote_user: "%{name} ha promogut l'usuari %{target}"
+        remove_avatar_user: "%{name} ha eliminat l'avatar de %{target}"
+        reopen_report: "%{name} ha reobert l'informe %{target}"
         reset_password_user: "%{name} ha restablert la contrasenya de l'usuari %{target}"
-        resolve_report: "%{name} ha descartat l'informe %{target}"
+        resolve_report: "%{name} ha resolt l'informe %{target}"
         silence_account: "%{name} ha silenciat el compte de %{target}"
         suspend_account: "%{name} ha suspès el compte de %{target}"
+        unassigned_report: "%{name} ha des-assignat  l'informe %{target}"
         unsilence_account: "%{name} ha silenciat el compte de %{target}"
         unsuspend_account: "%{name} ha llevat la suspensió del compte de %{target}"
         update_custom_emoji: "%{name} ha actualitzat l'emoji %{target}"
@@ -239,29 +255,48 @@ ca:
         expired: Caducat
         title: Filtre
       title: Convida
+    report_notes:
+      created_msg: La nota del informe s'ha creat correctament!
+      destroyed_msg: La nota del informe s'ha esborrat correctament!
     reports:
+      account:
+        note: nota
+        report: informe
       action_taken_by: Mesures adoptades per
       are_you_sure: N'estàs segur?
+      assign_to_self: Assignar-me
+      assigned: Moderador assignat
       comment:
-        label: Comentari
         none: Cap
+      created_at: Reportat
       delete: Suprimeix
       id: ID
       mark_as_resolved: Marca com a resolt
+      mark_as_unresolved: Marcar sense resoldre
+      notes:
+        create: Afegir nota
+        create_and_resolve: Resoldre amb nota
+        create_and_unresolve: Reobrir amb nota
+        delete: Esborrar
+        placeholder: Descriu les accions que s'han pres o qualsevol altra actualització d'aquest informe…
       nsfw:
         'false': Mostra els fitxers multimèdia adjunts
         'true': Amaga els fitxers multimèdia adjunts
+      reopen: Reobrir informe
       report: 'Informe #%{id}'
       report_contents: Contingut
       reported_account: Compte reportat
       reported_by: Reportat per
       resolved: Resolt
+      resolved_msg: Informe resolt amb èxit!
       silence_account: Silencia el compte
       status: Estat
       suspend_account: Suspèn el compte
       target: Objectiu
       title: Informes
+      unassign: Treure assignació
       unresolved: No resolt
+      updated_at: Actualitzat
       view: Visualització
     settings:
       activity_api_enabled:
@@ -319,8 +354,8 @@ ca:
       back_to_account: Torna a la pàgina del compte
       batch:
         delete: Suprimeix
-        nsfw_off: NSFW OFF
-        nsfw_on: NSFW ON
+        nsfw_off: Marcar com a no sensible
+        nsfw_on: Marcar com a sensible
       execute: Executa
       failed_to_execute: No s'ha pogut executar
       media:
@@ -382,6 +417,7 @@ ca:
     security: Seguretat
     set_new_password: Estableix una contrasenya nova
   authorize_follow:
+    already_following: Ja estàs seguint aquest compte
     error: Malauradament, ha ocorregut un error cercant el compte remot
     follow: Segueix
     follow_request: 'Has enviat una sol·licitud de seguiment a:'
@@ -474,6 +510,7 @@ ca:
       '21600': 6 hores
       '3600': 1 hora
       '43200': 12 hores
+      '604800': 1 setmana
       '86400': 1 dia
     expires_in_prompt: Mai
     generate: Genera
@@ -577,6 +614,10 @@ ca:
     missing_resource: No s'ha pogut trobar la URL de redirecció necessaria per al compte
     proceed: Comença a seguir
     prompt: 'Seguiràs a:'
+  remote_unfollow:
+    error: Error
+    title: Títol
+    unfollowed: Sense seguir
   sessions:
     activity: Última activitat
     browser: Navegador
@@ -643,6 +684,9 @@ ca:
         one: "%{count} vídeo"
         other: "%{count} vídeos"
     content_warning: 'Avís de contingut: %{warning}'
+    disallowed_hashtags:
+      one: 'conté una etiqueta no permesa: %{tags}'
+      other: 'conté les etiquetes no permeses: %{tags}'
     open_in_web: Obre en la web
     over_character_limit: Límit de caràcters de %{max} superat
     pin_errors:
@@ -665,6 +709,83 @@ ca:
     reblogged: ha impulsat
     sensitive_content: Contingut sensible
   terms:
+    body_html: |
+      <h2>Privacy Policy</h2>
+      <h3 id="collect">Quina informació recollim?</h3>
+
+      <ul>
+        <li><em>Informació bàsica del compte</em>: Si et registres en aquest servidor, se´t pot demanar que introdueixis un nom d'usuari, una adreça de correu electrònic i una contrasenya. També pots introduir informació de perfil addicional, com ara un nom de visualització i una biografia, i carregar una imatge de perfil i de capçalera. El nom d'usuari, el nom de visualització, la biografia, la imatge de perfil i la imatge de capçalera sempre apareixen públicament.</li>
+        <li><em>Publicacions, seguiment i altra informació pública</em>: La llista de persones que segueixes s'enumeren públicament i el mateix passa amb els teus seguidors. Quan envies un missatge, la data i l'hora s'emmagatzemen, així com l'aplicació que va enviar el missatge. Els missatges poden contenir multimèdia, com ara imatges i vídeos. Els toots públics i no llistats estan disponibles públicament. En quan tinguis un toot en el teu perfil, aquest també és informació pública. Les teves entrades es lliuren als teus seguidors que en alguns casos significa que es lliuren a diferents servidors en els quals s'hi emmagatzemen còpies. Quan suprimeixes publicacions, també es lliuraran als teus seguidors. L'acció d'impulsar o marcar com a favorit una publicació sempre és pública.</li>
+        <li><em>Toots directes i per a només seguidors</em>: Totes les publicacions s'emmagatzemen i processen al servidor. Els toots per a només seguidors només es lliuren als teus seguidors i als usuaris que s'esmenten en ells i els toots directes només es lliuren als usuaris esmentats. En alguns casos, significa que es lliuren a diferents servidors i s'hi emmagatzemen còpies. Fem un esforç de bona fe per limitar l'accés a aquestes publicacions només a les persones autoritzades, però és possible que altres servidors no ho facin. Per tant, és important revisar els servidors als quals pertanyen els teus seguidors. Pots canviar la opció de aprovar o rebutjar els nous seguidors manualment a la configuració.  <em>Tingues en compte que els operadors del servidor i qualsevol servidor receptor poden visualitzar aquests missatges</em> i els destinataris poden fer una captura de pantalla, copiar-los o tornar-los a compartir.  <em>No comparteixis cap informació perillosa a Mastodon.</em></li>
+        <li><em>IPs i altres metadades</em>: Quan inicies sessió registrem l'adreça IP en que l'has iniciat, així com el nom de l'aplicació o navegador. Totes les sessions registrades estan disponibles per a la teva revisió i revocació a la configuració. L'última adreça IP utilitzada s'emmagatzema durant un màxim de 12 mesos. També podrem conservar els registres que inclouen l'adreça IP de cada sol·licitud al nostre servidor.</li>
+      </ul>
+
+      <hr class="spacer" />
+
+      <h3 id="use">Per a què utilitzem la teva informació?</h3>
+
+      <p>Qualsevol de la informació que recopilem de tu es pot utilitzar de la manera següent:</p>
+
+      <ul>
+        <li>Per proporcionar la funcionalitat bàsica de Mastodon. Només pots interactuar amb el contingut d'altres persones i publicar el teu propi contingut quan hàgis iniciat la sessió. Per exemple, pots seguir altres persones per veure les publicacions combinades a la teva pròpia línia de temps personalitzada.</li>
+        <li>Per ajudar a la moderació de la comunitat, per exemple comparar la teva adreça IP amb altres conegudes per determinar l'evasió de prohibicions o altres infraccions.</li>
+        <li>L'adreça electrònica que ens proporciones pot utilitzar-se per enviar-te informació, notificacions sobre altres persones que interactuen amb el teu contingut o t'envien missatges, i per respondre a les consultes i / o altres sol·licituds o preguntes.</li>
+      </ul>
+
+      <hr class="spacer" />
+
+      <h3 id="protect">Com protegim la teva informació</h3>
+
+      <p>Implementem diverses mesures per mantenir la seguretat quan introdueixes, envies o accedeixes a la teva informació personal. Entre altres mesures, la sessió del teu navegador així com el trànsit entre les teves aplicacions i l'API estan protegides amb SSL i la teva contrasenya es codifica utilitzant un algoritme de direcció única. Pots habilitar l'autenticació de dos factors per a garantir l'accés segur al teu compte.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="data-retention">Quina és la nostra política de retenció de dades?</h3>
+
+      <p>Farem un esforç de bona fe per:</p>
+
+      <ul>
+        <li>Conservar els registres del servidor que continguin l'adreça IP de totes les sol·licituds que rebi, tenint em compte que aquests registres es mantenen no més de 90 dies.</li>
+        <li>Conservar les adreces IP associades als usuaris registrats no més de 12 mesos.</li>
+      </ul>
+
+      <p>Pots sol·licitar i descarregar un arxiu del teu contingut incloses les publicacions, els fitxers adjunts multimèdia, la imatge de perfil i la imatge de capçalera.</p>
+
+      <p>Pots eliminar el teu compte de forma irreversible en qualsevol moment.</p>
+
+      <hr class="spacer"/>
+
+      <h3 id="cookies">Utilitzem cookies?</h3>
+
+      <p>Sí. Les cookies són petits fitxers que un lloc o el proveïdor de serveis transfereix al disc dur del teu ordinador a través del navegador web (si ho permet). Aquestes galetes permeten al lloc reconèixer el teu navegador i, si tens un compte registrat, associar-lo al teu compte registrat.</p>
+
+      <p>Utilitzem cookies per entendre i guardar les teves preferències per a futures visites.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="disclose">Revelem informació a terceres parts?</h3>
+
+      <p>No venem, comercialitzem ni transmetem a tercers la teva informació d'identificació personal. Això no inclou tercers de confiança que ens ajuden a operar el nostre lloc, a dur a terme el nostre servei o a servir-te, sempre que aquestes parts acceptin mantenir confidencial aquesta informació. També podem publicar la teva informació quan creiem que l'alliberament és apropiat per complir amb la llei, fer complir les polítiques del nostre lloc o protegir els nostres drets o altres drets, propietat o seguretat.</p>
+
+      <p>Els altres servidors de la teva xarxa poden descarregar contingut públic. Els teus toots públics i per a només seguidors es lliuren als servidors on resideixen els teus seguidors i els missatges directes s'envien als servidors dels destinataris, sempre que aquests seguidors o destinataris resideixin en un servidor diferent d'aquest.</p>
+
+      <p>Quan autoritzes una aplicació a utilitzar el teu compte, segons l'abast dels permisos que aprovis, pot accedir a la teva informació de perfil pública, a la teva llista de seguits, als teus seguidors, a les teves llistes, a totes les teves publicacions i als teus favorits. Les aplicacions mai no poden accedir a la teva adreça de correu electrònic o contrasenya.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="coppa">Compliment de la Llei de protecció de la privacitat en línia dels nens</h3>
+
+      <p>El nostre lloc, productes i serveis estan dirigits a persones que tenen almenys 13 anys. Si aquest servidor es troba als EUA, i tens menys de 13 anys, segons els requisits de COPPA (<a href="https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act">Children's Online Privacy Protection Act</a>) no utilitzis aquest lloc.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="changes">Canvis a la nostra política de privacitat</h3>
+
+      <p>Si decidim canviar la nostra política de privadesa, publicarem aquests canvis en aquesta pàgina.</p>
+
+      <p> Aquest document és CC-BY-SA. Actualitzat per darrera vegada el 7 de Març del 2018.</p>
+
+      <p>Originalment adaptat des del <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>.</p>
     title: "%{instance} Condicions del servei i política de privadesa"
   time:
     formats:
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 6233d299e..6b2c08735 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -4,6 +4,7 @@ de:
     about_hashtag_html: Dies sind öffentliche Beiträge, die mit <strong>#%{hashtag}</strong> getaggt wurden. Wenn du ein Konto irgendwo im Fediversum besitzt, kannst du mit ihnen interagieren.
     about_mastodon_html: Mastodon ist ein soziales Netzwerk. Es basiert auf offenen Web-Protokollen und freier, quelloffener Software. Es ist dezentral (so wie E-Mail!).
     about_this: Über diese Instanz
+    administered_by: 'Administriert von:'
     closed_registrations: Die Registrierung auf dieser Instanz ist momentan geschlossen. Aber du kannst dein Konto auch auf einer anderen Instanz erstellen! Von dort hast du genauso Zugriff auf das Mastodon-Netzwerk.
     contact: Kontakt
     contact_missing: Nicht angegeben
@@ -60,7 +61,15 @@ de:
       destroyed_msg: Moderationsnotiz erfolgreich gelöscht!
     accounts:
       are_you_sure: Bist du sicher?
+      avatar: Profilbild
       by_domain: Domäne
+      change_email:
+        changed_msg: E-Mail-Adresse des Kontos erfolgreich geändert!
+        current_email: Aktuelle E-Mail-Adresse
+        label: E-Mail-Adresse ändern
+        new_email: Neue E-Mail-Adresse
+        submit: E-Mail-Adresse ändern
+        title: E-Mail-Adresse für %{username} ändern
       confirm: Bestätigen
       confirmed: Bestätigt
       demote: Degradieren
@@ -75,9 +84,9 @@ de:
       enabled: Freigegeben
       feed_url: Feed-URL
       followers: Folger
-      followers_url: Followers URL
+      followers_url: URL des Folgenden
       follows: Folgt
-      inbox_url: Inbox URL
+      inbox_url: Posteingangs-URL
       ip: IP-Adresse
       location:
         all: Alle
@@ -100,7 +109,7 @@ de:
         alphabetic: Alphabetisch
         most_recent: Neueste
         title: Sortierung
-      outbox_url: Outbox URL
+      outbox_url: Postausgangs-URL
       perform_full_suspension: Vollständige Sperre durchführen
       profile_url: Profil-URL
       promote: Befördern
@@ -108,6 +117,7 @@ de:
       public: Öffentlich
       push_subscription_expires: PuSH-Abonnement läuft aus
       redownload: Avatar neu laden
+      remove_avatar: Profilbild entfernen
       reset: Zurücksetzen
       reset_password: Passwort zurücksetzen
       resubscribe: Wieder abonnieren
@@ -128,6 +138,7 @@ de:
       statuses: Beiträge
       subscribe: Abonnieren
       title: Konten
+      unconfirmed_email: Unbestätigte E-Mail-Adresse
       undo_silenced: Stummschaltung zurücknehmen
       undo_suspension: Sperre zurücknehmen
       unsubscribe: Abbestellen
@@ -135,6 +146,8 @@ de:
       web: Web
     action_logs:
       actions:
+        assigned_to_self_report: "%{name} hat sich die Meldung %{target} selbst zugewiesen"
+        change_email_user: "%{name} hat die E-Mail-Adresse des Nutzers %{target} geändert"
         confirm_user: "%{name} hat die E-Mail-Adresse von %{target} bestätigt"
         create_custom_emoji: "%{name} hat neues Emoji %{target} hochgeladen"
         create_domain_block: "%{name} hat die Domain %{target} blockiert"
@@ -150,10 +163,13 @@ de:
         enable_user: "%{name} hat die Anmeldung für den Benutzer %{target} aktiviert"
         memorialize_account: "%{name} hat %{target}s Profil in eine Gedenkseite umgewandelt"
         promote_user: "%{name} hat %{target} befördert"
+        remove_avatar_user: "%{name} hat das Profilbild von %{target} entfernt"
+        reopen_report: "%{name} hat die Meldung %{target} wieder geöffnet"
         reset_password_user: "%{name} hat das Passwort für den Benutzer %{target} zurückgesetzt"
-        resolve_report: "%{name} hat die Meldung %{target} abgelehnt"
+        resolve_report: "%{name} hat die Meldung %{target} bearbeitet"
         silence_account: "%{name} hat %{target}s Account stummgeschaltet"
         suspend_account: "%{name} hat %{target}s Account gesperrt"
+        unassigned_report: "%{name} hat die Zuweisung der Meldung %{target} entfernt"
         unsilence_account: "%{name} hat die Stummschaltung von %{target}s Account aufgehoben"
         unsuspend_account: "%{name} hat die Sperrung von %{target}s Account aufgehoben"
         update_custom_emoji: "%{name} hat das %{target} Emoji aktualisiert"
@@ -177,7 +193,7 @@ de:
       new:
         title: Eigenes Emoji hinzufügen
       overwrite: Überschreiben
-      shortcode: Shortcode
+      shortcode: Kürzel
       shortcode_hint: Mindestens 2 Zeichen, nur Buchstaben, Ziffern und Unterstriche
       title: Eigene Emojis
       unlisted: Ungelistet
@@ -239,29 +255,48 @@ de:
         expired: Ausgelaufen
         title: Filter
       title: Einladungen
+    report_notes:
+      created_msg: Meldungs-Kommentar erfolgreich erstellt!
+      destroyed_msg: Meldungs-Kommentar erfolgreich gelöscht!
     reports:
+      account:
+        note: Notiz
+        report: Meldung
       action_taken_by: Maßnahme ergriffen durch
       are_you_sure: Bist du dir sicher?
+      assign_to_self: Mir zuweisen
+      assigned: Zugewiesener Moderator
       comment:
-        label: Kommentar
         none: Kein
+      created_at: Gemeldet
       delete: Löschen
       id: ID
       mark_as_resolved: Als gelöst markieren
+      mark_as_unresolved: Als ungelöst markieren
+      notes:
+        create: Kommentar hinzufügen
+        create_and_resolve: Mit Kommentar lösen
+        create_and_unresolve: Mit Kommentar wieder öffnen
+        delete: Löschen
+        placeholder: Beschreibe, welche Maßnahmen ergriffen wurden oder andere Neuigkeiten zu dieser Meldung…
       nsfw:
         'false': Medienanhänge wieder anzeigen
         'true': Medienanhänge verbergen
+      reopen: Meldung wieder öffnen
       report: 'Meldung #%{id}'
       report_contents: Inhalt
       reported_account: Gemeldetes Konto
       reported_by: Gemeldet von
       resolved: Gelöst
+      resolved_msg: Meldung erfolgreich gelöst!
       silence_account: Konto stummschalten
       status: Status
       suspend_account: Konto sperren
       target: Ziel
       title: Meldungen
+      unassign: Zuweisung entfernen
       unresolved: Ungelöst
+      updated_at: Aktualisiert
       view: Ansehen
     settings:
       activity_api_enabled:
@@ -319,8 +354,8 @@ de:
       back_to_account: Zurück zum Konto
       batch:
         delete: Löschen
-        nsfw_off: NSFW aus
-        nsfw_on: NSFW ein
+        nsfw_off: Als nicht heikel markieren
+        nsfw_on: Als heikel markieren
       execute: Ausführen
       failed_to_execute: Ausführen fehlgeschlagen
       media:
@@ -382,6 +417,7 @@ de:
     security: Sicherheit
     set_new_password: Neues Passwort setzen
   authorize_follow:
+    already_following: Du folgst diesem Konto bereits
     error: Das Profil konnte nicht geladen werden
     follow: Folgen
     follow_request: 'Du hast eine Folgeanfrage gesendet an:'
@@ -429,7 +465,7 @@ de:
     archive_takeout:
       date: Datum
       download: Dein Archiv herunterladen
-      hint_html: Du kannst ein Archiv deiner <strong>Beiträge und hochgeladenen Medien</strong> anfragen. Die exportieren Daten werden im ActivityPub-Format gespeichert, dass lesbar mit jeder Software ist, die das Format unterstützt.
+      hint_html: Du kannst ein Archiv deiner <strong>Beiträge und hochgeladenen Medien</strong> anfragen. Die exportierten Daten werden im ActivityPub-Format gespeichert, welches mit jeder Software lesbar ist die das Format unterstützt.
       in_progress: Stelle dein Archiv zusammen...
       request: Dein Archiv anfragen
       size: Größe
@@ -474,6 +510,7 @@ de:
       '21600': 6 Stunden
       '3600': 1 Stunde
       '43200': 12 Stunden
+      '604800': 1 Woche
       '86400': 1 Tag
     expires_in_prompt: Nie
     generate: Generieren
@@ -577,6 +614,10 @@ de:
     missing_resource: Die erforderliche Weiterleitungs-URL für dein Konto konnte nicht gefunden werden
     proceed: Weiter
     prompt: 'Du wirst dieser Person folgen:'
+  remote_unfollow:
+    error: Fehler
+    title: Titel
+    unfollowed: Entfolgt
   sessions:
     activity: Letzte Aktivität
     browser: Browser
@@ -634,6 +675,18 @@ de:
     two_factor_authentication: Zwei-Faktor-Auth
     your_apps: Deine Anwendungen
   statuses:
+    attached:
+      description: 'Angehängt: %{attached}'
+      image:
+        one: "%{count} Bild"
+        other: "%{count} Bilder"
+      video:
+        one: "%{count} Video"
+        other: "%{count} Videos"
+    content_warning: 'Inhaltswarnung: %{warning}'
+    disallowed_hashtags:
+      one: 'Enthält den unerlaubten Hashtag: %{tags}'
+      other: 'Enthält die unerlaubten Hashtags: %{tags}'
     open_in_web: Im Web öffnen
     over_character_limit: Zeichenlimit von %{max} überschritten
     pin_errors:
@@ -655,6 +708,11 @@ de:
     pinned: Angehefteter Beitrag
     reblogged: teilte
     sensitive_content: Heikle Inhalte
+  terms:
+    title: "%{instance} Nutzungsbedingungen und Datenschutzerklärung"
+  themes:
+    contrast: Hoher Kontrast
+    default: Mastodon
   time:
     formats:
       default: "%d.%m.%Y %H:%M"
diff --git a/config/locales/devise.de.yml b/config/locales/devise.de.yml
index 77243ba15..0d33af6f1 100644
--- a/config/locales/devise.de.yml
+++ b/config/locales/devise.de.yml
@@ -3,8 +3,8 @@ de:
   devise:
     confirmations:
       confirmed: Deine E-Mail-Adresse wurde bestätigt.
-      send_instructions: Du erhältst in wenigen Minuten eine E-Mail. Darin wird erklärt, wie du deine E-Mail-Adresse bestätigen kannst. Schau bitte auch in deinen Spam-Ordner!
-      send_paranoid_instructions: Falls deine E-Mail-Adresse in unserer Datenbank hinterlegt ist, erhältst du in wenigen Minuten eine E-Mail. Darin wird erklärt, wie du deine E-Mail-Adresse bestätigen kannst. Schau bitte auch in deinen Spam-Ordner!
+      send_instructions: Du wirst in wenigen Minuten eine E-Mail erhalten. Darin wird erklärt, wie du deine E-Mail-Adresse bestätigen kannst. Schau bitte auch in deinem Spam-Ordner nach, wenn du diese E-Mail nicht erhalten hast.
+      send_paranoid_instructions: Falls deine E-Mail-Adresse in unserer Datenbank hinterlegt ist, wirst du in wenigen Minuten eine E-Mail erhalten. Darin wird erklärt, wie du deine E-Mail-Adresse bestätigen kannst. Schau bitte auch in deinem Spam-Ordner nach, wenn du diese E-Mail nicht erhalten hast.
     failure:
       already_authenticated: Du bist bereits angemeldet.
       inactive: Dein Konto wurde noch nicht aktiviert.
@@ -73,10 +73,10 @@ de:
   errors:
     messages:
       already_confirmed: wurde bereits bestätigt, bitte versuche dich anzumelden
-      confirmation_period_expired: muss innerhalb %{period} bestätigt werden, bitte fordere einen neuen Link an.
-      expired: ist abgelaufen, bitte neu anfordern.
-      not_found: wurde nicht gefunden.
+      confirmation_period_expired: muss innerhalb %{period} bestätigt werden, bitte fordere einen neuen Link an
+      expired: ist abgelaufen, bitte neu anfordern
+      not_found: nicht gefunden
       not_locked: ist nicht gesperrt
       not_saved:
-        one: 'Konnte %{resource} nicht speichern: ein Fehler.'
-        other: 'Konnte %{resource} nicht speichern: %{count} Fehler.'
+        one: '1 Fehler hat verhindert, dass %{resource} gespeichert wurde:'
+        other: "%{count} Fehler verhinderten, dass %{resource} gespeichert wurde:"
diff --git a/config/locales/devise.eu.yml b/config/locales/devise.eu.yml
new file mode 100644
index 000000000..215b72e52
--- /dev/null
+++ b/config/locales/devise.eu.yml
@@ -0,0 +1,5 @@
+---
+eu:
+  devise:
+    failure:
+      already_authenticated: Saioa hasi duzu jada.
diff --git a/config/locales/devise.it.yml b/config/locales/devise.it.yml
index e1ba7bb22..0c5d8963c 100644
--- a/config/locales/devise.it.yml
+++ b/config/locales/devise.it.yml
@@ -17,11 +17,32 @@ it:
       unconfirmed: Devi confermare il tuo indirizzo email per continuare.
     mailer:
       confirmation_instructions:
+        action: Verifica indirizzo email
+        explanation: Hai creato un account su %{host} con questo indirizzo email. Sei lonatno solo un clic dall'attivarlo. Se non sei stato tu, per favore ignora questa email.
+        extra_html: Per favore controlla<a href="%{terms_path}">le regole dell'istanza</a> e <a href="%{policy_path}">i nostri termini di servizio</a>.
         subject: 'Mastodon: Istruzioni di conferma per %{instance}'
+        title: Verifica indirizzo email
+      email_changed:
+        explanation: 'L''indirizzo email del tuo account sta per essere cambiato in:'
+        extra: Se non hai cambiato la tua email, è probabile che qualcuno abbia accesso al tuo account. Cambia immediatamente la tua password e contatta l'amministratore dell'istanza se sei bloccato fuori dal tuo account.
+        subject: 'Mastodon: Email cambiata'
+        title: Nuovo indirizzo email
       password_change:
+        explanation: La password del tuo account è stata cambiata.
+        extra: Se non hai cambiato la password, è probabile che qualcuno abbia accesso al tuo account. Cambia immediatamente la tua password e contatta l'amministratore dell'istanza se sei bloccato fuori dal tuo account.
         subject: 'Mastodon: Password modificata'
+        title: Password cambiata
+      reconfirmation_instructions:
+        explanation: Conferma il nuovo indirizzo per cambiare la tua email.
+        extra: Se questo cambiamento non è stato chiesto da te, ignora questa email. L'indirizzo email per l'account Mastodon non verrà cambiato finché non accedi dal link qui sopra.
+        subject: 'Mastodon: Email di conferma per %{instance}'
+        title: Verifica indirizzo email
       reset_password_instructions:
+        action: Cambia password
+        explanation: Hai richiesto una nuova password per il tuo account.
+        extra: Se questo cambiamento non è stato chiesto da te, ignora questa email. La tua password non verrà cambiata finché non accedi tramite il link qui sopra e ne crei una nuova.
         subject: 'Mastodon: Istruzioni per il reset della password'
+        title: Ripristino password
       unlock_instructions:
         subject: 'Mastodon: Istruzioni di sblocco'
     omniauth_callbacks:
diff --git a/config/locales/doorkeeper.de.yml b/config/locales/doorkeeper.de.yml
index d7d98c6d6..670f5ec2a 100644
--- a/config/locales/doorkeeper.de.yml
+++ b/config/locales/doorkeeper.de.yml
@@ -29,7 +29,7 @@ de:
       edit:
         title: Anwendung bearbeiten
       form:
-        error: Hoppla! Bitte überprüfe das Formular auf Fehler!
+        error: Hoppla! Bitte überprüfe das Formular auf mögliche Fehler
       help:
         native_redirect_uri: "%{native_redirect_uri} für lokale Tests benutzen"
         redirect_uri: Bitte benutze eine Zeile pro URI
@@ -59,7 +59,7 @@ de:
       error:
         title: Ein Fehler ist aufgetreten
       new:
-        able_to: 'Sie wird folgende Befugnisse haben:'
+        able_to: Es wird in der Lage sein zu
         prompt: Die Anwendung %{client_name} verlangt Zugriff auf dein Konto
         title: Autorisierung erforderlich
       show:
@@ -83,7 +83,7 @@ de:
         invalid_grant: Die beigefügte Autorisierung ist ungültig, abgelaufen, wurde widerrufen, einem anderen Client ausgestellt oder der Weiterleitungs-URI stimmt nicht mit der Autorisierungs-Anfrage überein.
         invalid_redirect_uri: Der beigefügte Weiterleitungs-URI ist ungültig.
         invalid_request: Die Anfrage enthält ein nicht-unterstütztes Argument, ein Parameter fehlt, oder sie ist anderweitig fehlerhaft.
-        invalid_resource_owner: Die angegebenen Zugangsdaten für den »resource owner« sind ungültig, oder dieses Profil existiert nicht.
+        invalid_resource_owner: Die angegebenen Zugangsdaten für den Ressourcenbesitzer sind ungültig oder der Ressourcenbesitzer kann nicht gefunden werden
         invalid_scope: Die angeforderte Befugnis ist ungültig, unbekannt oder fehlerhaft.
         invalid_token:
           expired: Der Zugriffs-Token ist abgelaufen
diff --git a/config/locales/doorkeeper.eu.yml b/config/locales/doorkeeper.eu.yml
new file mode 100644
index 000000000..a51b1dc8b
--- /dev/null
+++ b/config/locales/doorkeeper.eu.yml
@@ -0,0 +1,6 @@
+---
+eu:
+  activerecord:
+    attributes:
+      doorkeeper/application:
+        name: Aplikazioaren izena
diff --git a/config/locales/doorkeeper.it.yml b/config/locales/doorkeeper.it.yml
index e5a2d3f6e..50b2c9780 100644
--- a/config/locales/doorkeeper.it.yml
+++ b/config/locales/doorkeeper.it.yml
@@ -3,8 +3,10 @@ it:
   activerecord:
     attributes:
       doorkeeper/application:
-        name: Nome
+        name: Nome applicazione
         redirect_uri: URI di reindirizzamento
+        scopes: Scopi
+        website: Sito web applicazione
     errors:
       models:
         doorkeeper/application:
@@ -33,9 +35,13 @@ it:
         redirect_uri: Usa una riga per URI
         scopes: Dividi gli scopes con spazi. Lascia vuoto per utilizzare gli scopes di default.
       index:
+        application: Applicazione
         callback_url: Callback URL
+        delete: Elimina
         name: Nome
         new: Nuova applicazione
+        scopes: Scopes
+        show: Mostra
         title: Le tue applicazioni
       new:
         title: Nuova applicazione
@@ -43,7 +49,7 @@ it:
         actions: Azioni
         application_id: Id applicazione
         callback_urls: Callback urls
-        scopes: Scopes
+        scopes: Scopi
         secret: Secret
         title: 'Applicazione: %{name}'
     authorizations:
@@ -57,7 +63,7 @@ it:
         prompt: L'applicazione %{client_name} richiede l'accesso al tuo account
         title: Autorizzazione richiesta
       show:
-        title: Copy this authorization code and paste it to the application.
+        title: Copia questo codice di autorizzazione e incollalo nell'applicazione.
     authorized_applications:
       buttons:
         revoke: Disabilita
@@ -67,7 +73,7 @@ it:
         application: Applicazione
         created_at: Autorizzato
         date_format: "%d-%m-%Y %H:%M:%S"
-        scopes: Scopes
+        scopes: Scopi
         title: Applicazioni autorizzate
     errors:
       messages:
@@ -104,7 +110,7 @@ it:
       admin:
         nav:
           applications: Applicazioni
-          oauth2_provider: OAuth2 Provider
+          oauth2_provider: Provider OAuth2
       application:
         title: Autorizzazione OAuth richiesta
     scopes:
diff --git a/config/locales/doorkeeper.zh-HK.yml b/config/locales/doorkeeper.zh-HK.yml
index 4f46a416a..6eddcc27b 100644
--- a/config/locales/doorkeeper.zh-HK.yml
+++ b/config/locales/doorkeeper.zh-HK.yml
@@ -12,10 +12,10 @@ zh-HK:
         doorkeeper/application:
           attributes:
             redirect_uri:
-              fragment_present: URI 不可包含 "#fragment" 部份
-              invalid_uri: 必需有正確的 URI.
-              relative_uri: 必需為完整 URI.
-              secured_uri: 必需使用有 HTTPS/SSL 加密的 URI.
+              fragment_present: URI 不可包含 "#fragment" 部份。
+              invalid_uri: 必需有正確的 URI。
+              relative_uri: 必需為完整 URI。
+              secured_uri: 必需使用有 HTTPS/SSL 加密的 URI。
   doorkeeper:
     applications:
       buttons:
@@ -33,7 +33,7 @@ zh-HK:
       help:
         native_redirect_uri: 使用 %{native_redirect_uri} 作局部測試
         redirect_uri: 每行輸入一個 URI
-        scopes: 請用半形空格分開權限範圍 (scope)。留空表示使用預設的權限範圍
+        scopes: 請用半形空格分開權限範圍 (scope)。留空表示使用預設的權限範圍。
       index:
         application: 應用
         callback_url: 回傳網址
@@ -83,7 +83,7 @@ zh-HK:
         invalid_grant: 授權申請 (authorization grant) 不正確、過期、已被取消,或者無法對應授權請求 (authorization request) 內的轉接 URI,或者屬於別的用戶程式。
         invalid_redirect_uri: 不正確的轉接網址。
         invalid_request: 請求缺少了必要的參數、包含了不支援的參數、或者其他輸入錯誤。
-        invalid_resource_owner: 資源擁有者的登入資訊錯誤、或者無法找到該資源擁有者。
+        invalid_resource_owner: 資源擁有者的登入資訊錯誤、或者無法找到該資源擁有者
         invalid_scope: 請求的權限範圍 (scope) 不正確、未有定義、或者輸入錯誤。
         invalid_token:
           expired: access token 已經過期
@@ -94,7 +94,7 @@ zh-HK:
         temporarily_unavailable: 認證伺服器由於臨時負荷過重或者維護,目前未能處理請求。
         unauthorized_client: 用戶程式無權用此方法 (method) 請行這個請求。
         unsupported_grant_type: 授權伺服器不支援這個授權類型 (grant type)。
-        unsupported_response_type: 授權伺服器不支援這個回應類型 (response type).
+        unsupported_response_type: 授權伺服器不支援這個回應類型 (response type)。
     flash:
       applications:
         create:
diff --git a/config/locales/el.yml b/config/locales/el.yml
new file mode 100644
index 000000000..8741635e1
--- /dev/null
+++ b/config/locales/el.yml
@@ -0,0 +1,40 @@
+---
+el:
+  about:
+    about_mastodon_html: Το Mastodon είναι ένα κοινωνικό δίκτυο που βασίζεται σε ανοιχτά δικτυακά πρωτόκολλα και ελεύθερο λογισμικό ανοιχτού κώδικα. Είναι αποκεντρωμένο όπως το e-mail.
+    about_this: Σχετικά
+    administered_by: 'Διαχειρίζεται από:'
+    closed_registrations: Αυτή τη στιγμή οι εγγραφές σε αυτό τον διακομιστή είναι κλειστές. Αλλά! Μπορείς να βρεις έναν άλλο διακομιστή για να ανοίξεις λογαριασμό και να έχεις πρόσβαση από εκεί στο ίδιο ακριβώς δίκτυο.
+    contact: Επικοινωνία
+    contact_missing: Δεν έχει οριστεί
+    domain_count_after: άλλοι διακομιστές
+    domain_count_before: Συνδέεται με
+    extended_description_html: |
+      <h3>Ένα καλό σημείο για κανόνες</h3>
+      <p>Η αναλυτική περιγραφή δεν έχει ακόμα οριστεί</p>
+    features:
+      humane_approach_body: Μαθαίνοντας από τις αποτυχίες άλλων δικτύων, το Mastodon στοχεύει να κάνει σχεδιαστικά ηθικές επιλογές για να καταπολεμήσει την κακόβουλη χρήση των κοινωνικών δικτύων.
+      humane_approach_title: Μια πιο ανθρώπινη προσέγγιση
+      not_a_product_title: Είσαι άτομο, όχι προϊόν
+      real_conversation_title: Φτιαγμένο για αληθινή συζήτηση
+      within_reach_body: Οι πολλαπλές εφαρμογές για το iOS, το Android και τις υπόλοιπες πλατφόρμες, χάρη σε ένα φιλικό προς τους προγραμματιστές οικοσύστημα API, σου επιτρέπουν να κρατάς επαφή με τους φίλους και τις φίλες σου οπουδήποτε.
+    generic_description: "%{domain} είναι ένας εξυπηρετητής στο δίκτυο"
+    hosted_on: Το Mastodon φιλοξενείται στο %{domain}
+    learn_more: Μάθε περισσότερα
+    other_instances: Λίστα διακομιστών
+    source_code: Πηγαίος κώδικας
+    status_count_after: καταστάσεις
+    status_count_before: Ποιός συνέγραψε
+    user_count_after: χρήστες
+    what_is_mastodon: Τι είναι το Mastodon;
+  accounts:
+    follow: Ακολούθησε
+    followers: Ακόλουθοι
+    following: Ακολουθεί
+    media: Πολυμέσα
+    moved_html: 'Ο/Η %{name} μετακόμισε στο %{new_profile_link}:'
+    nothing_here: Δεν υπάρχει τίποτα εδώ!
+    people_followed_by: Χρήστες που ακολουθεί ο/η %{name}
+    people_who_follow: Χρήστες που ακολουθούν τον/την %{name}
+    posts: Τουτ
+    posts_with_replies: Τουτ και απαντήσεις
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 645999d66..4c7c5078c 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -260,40 +260,29 @@ en:
       destroyed_msg: Report note successfully deleted!
     reports:
       account:
-        created_reports: Reports created by this account
-        moderation:
-          silenced: Silenced
-          suspended: Suspended
-          title: Moderation
-        moderation_notes: Moderation Notes
         note: note
         report: report
-        targeted_reports: Reports made about this account
       action_taken_by: Action taken by
       are_you_sure: Are you sure?
       assign_to_self: Assign to me
-      assigned: Assigned Moderator
+      assigned: Assigned moderator
       comment:
-        label: Report Comment
         none: None
       created_at: Reported
       delete: Delete
-      history: Moderation History
       id: ID
       mark_as_resolved: Mark as resolved
       mark_as_unresolved: Mark as unresolved
       notes:
-        create: Add Note
-        create_and_resolve: Resolve with Note
-        create_and_unresolve: Reopen with Note
+        create: Add note
+        create_and_resolve: Resolve with note
+        create_and_unresolve: Reopen with note
         delete: Delete
-        label: Moderator Notes
-        new_label: Add Moderator Note
         placeholder: Describe what actions have been taken, or any other updates to this report…
       nsfw:
         'false': Unhide media attachments
         'true': Hide media attachments
-      reopen: Reopen Report
+      reopen: Reopen report
       report: 'Report #%{id}'
       report_contents: Contents
       reported_account: Reported account
@@ -302,7 +291,6 @@ en:
       resolved_msg: Report successfully resolved!
       silence_account: Silence account
       status: Status
-      statuses: Reported Toots
       suspend_account: Suspend account
       target: Target
       title: Reports
@@ -366,8 +354,8 @@ en:
       back_to_account: Back to account page
       batch:
         delete: Delete
-        nsfw_off: NSFW OFF
-        nsfw_on: NSFW ON
+        nsfw_off: Mark as not sensitive
+        nsfw_on: Mark as sensitive
       execute: Execute
       failed_to_execute: Failed to execute
       media:
@@ -707,6 +695,9 @@ en:
         one: "%{count} video"
         other: "%{count} videos"
     content_warning: 'Content warning: %{warning}'
+    disallowed_hashtags:
+      one: 'contained a disallowed hashtag: %{tags}'
+      other: 'contained the disallowed hashtags: %{tags}'
     open_in_web: Open in web
     over_character_limit: character limit of %{max} exceeded
     pin_errors:
diff --git a/config/locales/eo.yml b/config/locales/eo.yml
index 27c62f899..c768d8a03 100644
--- a/config/locales/eo.yml
+++ b/config/locales/eo.yml
@@ -243,7 +243,6 @@ eo:
       action_taken_by: Ago farita de
       are_you_sure: Ĉu vi certas?
       comment:
-        label: Komento
         none: Nenio
       delete: Forigi
       id: ID
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 74045074e..bf449bf92 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -4,6 +4,7 @@ es:
     about_hashtag_html: Estos son toots públicos etiquetados con <strong>#%{hashtag}</strong>. Puedes interactuar con ellos si tienes una cuenta en cualquier parte del fediverso.
     about_mastodon_html: Mastodon es un servidor de red social <em>libre y de código abierto</em>. Una alternativa <em>descentralizada</em> a plataformas comerciales, que evita el riesgo de que una única compañía monopolice tu comunicación. Cualquiera puede ejecutar Mastodon y participar sin problemas en la <em>red social</em>.
     about_this: Acerca de esta instancia
+    administered_by: 'Administrado por:'
     closed_registrations: Los registros están actualmente cerrados en esta instancia.
     contact: Contacto
     contact_missing: No especificado
@@ -60,7 +61,15 @@ es:
       destroyed_msg: "¡Nota de moderación destruida con éxito!"
     accounts:
       are_you_sure: "¿Estás seguro?"
+      avatar: Avatar
       by_domain: Dominio
+      change_email:
+        changed_msg: "¡El correo electrónico se ha actualizado correctamente!"
+        current_email: Correo electrónico actual
+        label: Cambiar el correo electrónico
+        new_email: Nuevo correo electrónico
+        submit: Cambiar el correo electrónico
+        title: Cambiar el correo electrónico de %{username}
       confirm: Confirmar
       confirmed: Confirmado
       demote: Degradar
@@ -108,6 +117,7 @@ es:
       public: Público
       push_subscription_expires: Expiración de la suscripción PuSH
       redownload: Refrescar avatar
+      remove_avatar: Eliminar el avatar
       reset: Reiniciar
       reset_password: Reiniciar contraseña
       resubscribe: Re-suscribir
@@ -128,6 +138,7 @@ es:
       statuses: Estados
       subscribe: Suscribir
       title: Cuentas
+      unconfirmed_email: Correo electrónico sin confirmar
       undo_silenced: Des-silenciar
       undo_suspension: Des-suspender
       unsubscribe: Desuscribir
@@ -135,6 +146,8 @@ es:
       web: Web
     action_logs:
       actions:
+        assigned_to_self_report: "%{name} se ha asignado la denuncia %{target} a sí mismo"
+        change_email_user: "%{name} ha cambiado la dirección de correo del usuario %{target}"
         confirm_user: "%{name} confirmó la dirección de correo del usuario %{target}"
         create_custom_emoji: "%{name} subió un nuevo emoji %{target}"
         create_domain_block: "%{name} bloqueó el dominio %{target}"
@@ -150,10 +163,13 @@ es:
         enable_user: "%{name} habilitó el acceso del usuario %{target}"
         memorialize_account: "%{name} convirtió la cuenta de %{target} en una página de memorial"
         promote_user: "%{name} promoción al usuario %{target}"
+        remove_avatar_user: "%{name} ha eliminado el avatar de %{target}"
+        reopen_report: "%{name} ha reabierto la denuncia %{target}"
         reset_password_user: "%{name} restauró la contraseña del usuario %{target}"
-        resolve_report: "%{name} desestimó el reporte %{target}"
+        resolve_report: "%{name} ha resuelto la denuncia %{target}"
         silence_account: "%{name} silenció la cuenta de %{target}"
         suspend_account: "%{name} suspendió la cuenta de %{target}"
+        unassigned_report: "%{name} ha desasignado la denuncia %{target}"
         unsilence_account: "%{name} desactivó el silenciado de la cuenta de %{target}"
         unsuspend_account: "%{name} desactivó la suspensión de la cuenta de %{target}"
         update_custom_emoji: "%{name} actualizó el emoji %{target}"
@@ -239,29 +255,48 @@ es:
         expired: Expiradas
         title: Filtrar
       title: Invitaciones
+    report_notes:
+      created_msg: "¡El registro de la denuncia se ha creado correctamente!"
+      destroyed_msg: "¡El registro de la denuncia se ha borrado correctamente!"
     reports:
+      account:
+        note: nota
+        report: denuncia
       action_taken_by: Acción tomada por
       are_you_sure: "¿Estás seguro?"
+      assign_to_self: Asignármela a mí
+      assigned: Moderador asignado
       comment:
-        label: Comentario
         none: Ninguno
+      created_at: Denunciado
       delete: Eliminar
       id: ID
       mark_as_resolved: Marcar como resuelto
+      mark_as_unresolved: Marcar como no resuelto
+      notes:
+        create: Añadir una nota
+        create_and_resolve: Resolver con una nota
+        create_and_unresolve: Reabrir con una nota
+        delete: Eliminar
+        placeholder: Especificar qué acciones se han tomado o cualquier otra novedad respecto a esta denuncia…
       nsfw:
         'false': Mostrar multimedia
         'true': Ocultar multimedia
+      reopen: Reabrir denuncia
       report: 'Reportar #%{id}'
       report_contents: Contenido
       reported_account: Cuenta reportada
       reported_by: Reportado por
       resolved: Resuelto
+      resolved_msg: "¡La denuncia se ha resuelto correctamente!"
       silence_account: Silenciar cuenta
       status: Estado
       suspend_account: Suspender cuenta
       target: Objetivo
       title: Reportes
+      unassign: Desasignar
       unresolved: No resuelto
+      updated_at: Actualizado
       view: Ver
     settings:
       activity_api_enabled:
@@ -319,8 +354,8 @@ es:
       back_to_account: Volver a la cuenta
       batch:
         delete: Eliminar
-        nsfw_off: NSFW OFF
-        nsfw_on: NSFW ON
+        nsfw_off: Marcar contenido como no sensible
+        nsfw_on: Marcar contenido como sensible
       execute: Ejecutar
       failed_to_execute: Falló al ejecutar
       media:
@@ -382,6 +417,7 @@ es:
     security: Cambiar contraseña
     set_new_password: Establecer nueva contraseña
   authorize_follow:
+    already_following: Ya estás siguiendo a esta cuenta
     error: Desafortunadamente, ha ocurrido un error buscando la cuenta remota
     follow: Seguir
     follow_request: 'Tienes una solicitud de seguimiento de:'
@@ -474,6 +510,7 @@ es:
       '21600': 6 horas
       '3600': 1 hora
       '43200': 12 horas
+      '604800': 1 semana
       '86400': 1 día
     expires_in_prompt: Nunca
     generate: Generar
@@ -577,6 +614,10 @@ es:
     missing_resource: No se pudo encontrar la URL de redirección requerida para tu cuenta
     proceed: Proceder a seguir
     prompt: 'Vas a seguir a:'
+  remote_unfollow:
+    error: Error
+    title: Título
+    unfollowed: Ha dejado de seguirse
   sessions:
     activity: Última actividad
     browser: Navegador
@@ -643,6 +684,9 @@ es:
         one: "%{count} vídeo"
         other: "%{count} vídeos"
     content_warning: 'Alerta de contenido: %{warning}'
+    disallowed_hashtags:
+      one: 'contenía un hashtag no permitido: %{tags}'
+      other: 'contenía los hashtags no permitidos: %{tags}'
     open_in_web: Abrir en web
     over_character_limit: Límite de caracteres de %{max} superado
     pin_errors:
@@ -692,7 +736,7 @@ es:
       title: Descargar archivo
     welcome:
       edit_profile_action: Configurar el perfil
-      edit_profile_step: Puedes personalizar tu perfil subiendo un avatar, cabecera, cambiando tu nombre para mostrar y más. Si te gustaría revisar seguidores antes de autorizarlos a que te sigan, puedes bloquear tu cuenta.
+      edit_profile_step: Puedes personalizar tu perfil subiendo un avatar, una cabecera, cambiando tu nombre de usuario y más cosas. Si quieres revisar a tus nuevos seguidores antes de que se les permita seguirte, puedes bloquear tu cuenta.
       explanation: Aquí hay algunos consejos para empezar
       final_action: Empezar a publicar
       final_step: '¡Empieza a publicar! Incluso sin seguidores, tus mensajes públicos pueden ser vistos por otros, por ejemplo en la linea de tiempo local y con "hashtags". Podrías querer introducirte con el "hashtag" #introductions.'
@@ -702,7 +746,7 @@ es:
       review_preferences_step: Asegúrate de poner tus preferencias, como que correos te gustaría recibir, o que nivel de privacidad te gustaría que tus publicaciones tengan por defecto. Si no tienes mareos, podrías elegir habilitar la reproducción automática de "GIFs".
       subject: Bienvenido a Mastodon
       tip_bridge_html: Si esta viniendo desde Twitter, puedes encontrar a tus amigos en Mastodon usando la <a href="%{bridge_url}">aplicación puente</a>. Aunque solo funciona si ellos también usaron la aplicación puente!
-      tip_federated_timeline: La historia federada es una vista de toda la red Mastodon conocida. Sólo incluye gente a la que se han suscrito personas de tu instancia, así que no está completa.
+      tip_federated_timeline: La línea de tiempo federada es una vista de la red de Mastodon. Pero solo incluye gente que tus vecinos están siguiendo, así que no está completa.
       tip_following: Sigues a tus administradores de servidor por defecto. Para encontrar más gente interesante, revisa las lineas de tiempo local y federada.
       tip_local_timeline: La linea de tiempo local is una vista de la gente en %{instance}. Estos son tus vecinos inmediatos!
       tip_mobile_webapp: Si el navegador de tu dispositivo móvil ofrece agregar Mastodon a tu página de inicio, puedes recibir notificaciones. Actúa como una aplicación nativa en muchas formas!
diff --git a/config/locales/eu.yml b/config/locales/eu.yml
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/config/locales/eu.yml
@@ -0,0 +1 @@
+{}
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index ed25ea8c9..a3005547a 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -243,7 +243,6 @@ fa:
       action_taken_by: انجام‌دهنده
       are_you_sure: آیا مطمئن هستید؟
       comment:
-        label: توضیح
         none: خالی
       delete: پاک‌کردن
       id: شناسه
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index 62f6560bf..550ad1805 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -243,7 +243,6 @@ fi:
       action_taken_by: Toimenpiteen tekijä
       are_you_sure: Oletko varma?
       comment:
-        label: Kommentti
         none: Ei mitään
       delete: Poista
       id: Tunniste
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 1689754a0..0579123dc 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -4,6 +4,7 @@ fr:
     about_hashtag_html: Figurent ci-dessous les pouets tagués avec <strong>#%{hashtag}</strong>. Vous pouvez interagir avec eux si vous avez un compte n’importe où dans le Fediverse.
     about_mastodon_html: Mastodon est un réseau social utilisant des formats ouverts et des logiciels libres. Comme le courriel, il est décentralisé.
     about_this: À propos
+    administered_by: 'Administré par :'
     closed_registrations: Les inscriptions sont actuellement fermées sur cette instance. Cependant, vous pouvez trouver une autre instance sur laquelle vous créer un compte et à partir de laquelle vous pourrez accéder au même réseau.
     contact: Contact
     contact_missing: Manquant
@@ -60,7 +61,15 @@ fr:
       destroyed_msg: Note de modération supprimée avec succès !
     accounts:
       are_you_sure: Êtes-vous certain⋅e ?
+      avatar: Avatar
       by_domain: Domaine
+      change_email:
+        changed_msg: Courriel du compte modifié avec succès !
+        current_email: Courriel actuel
+        label: Modifier le courriel
+        new_email: Nouveau courriel
+        submit: Modifier le courriel
+        title: Modifier le courriel pour %{username}
       confirm: Confirmer
       confirmed: Confirmé
       demote: Rétrograder
@@ -108,6 +117,7 @@ fr:
       public: Publique
       push_subscription_expires: Expiration de l’abonnement PuSH
       redownload: Rafraîchir les avatars
+      remove_avatar: Supprimer l'avatar
       reset: Réinitialiser
       reset_password: Réinitialiser le mot de passe
       resubscribe: Se réabonner
@@ -128,6 +138,7 @@ fr:
       statuses: Statuts
       subscribe: S’abonner
       title: Comptes
+      unconfirmed_email: Courriel non-confirmé
       undo_silenced: Démasquer
       undo_suspension: Annuler la suspension
       unsubscribe: Se désabonner
@@ -135,6 +146,8 @@ fr:
       web: Web
     action_logs:
       actions:
+        assigned_to_self_report: "%{name} s'est assigné le signalement de %{target} à eux-même"
+        change_email_user: "%{name} a modifié l'adresse de courriel de l'utilisateur %{target}"
         confirm_user: "%{name} adresse courriel confirmée de l'utilisateur %{target}"
         create_custom_emoji: "%{name} a importé de nouveaux emoji %{target}"
         create_domain_block: "%{name} a bloqué le domaine %{target}"
@@ -150,10 +163,13 @@ fr:
         enable_user: "%{name} a activé le login pour l'utilisateur %{target}"
         memorialize_account: "%{name} a transformé le compte de %{target} en une page de mémorial"
         promote_user: "%{name} a promu l'utilisateur %{target}"
+        remove_avatar_user: "%{name} a supprimé l'avatar de %{target}'s"
+        reopen_report: "%{name} a ré-ouvert le signalement %{target}"
         reset_password_user: "%{name} a réinitialisé le mot de passe de %{target}"
-        resolve_report: "%{name} n'a pas pris en compte la dénonciation de %{target}"
+        resolve_report: "%{name} a résolu la dénonciation de %{target}"
         silence_account: "%{name} a mis le compte %{target} en mode silence"
         suspend_account: "%{name} a suspendu le compte %{target}"
+        unassigned_report: "%{name} a dés-assigné le signalement %{target}"
         unsilence_account: "%{name} a mis fin au mode silence de %{target}"
         unsuspend_account: "%{name} a réactivé le compte de %{target}"
         update_custom_emoji: "%{name} a mis à jour l'emoji %{target}"
@@ -239,29 +255,48 @@ fr:
         expired: Expiré
         title: Filtre
       title: Invitations
+    report_notes:
+      created_msg: Note de signalement créée avec succès !
+      destroyed_msg: Note de signalement effacée avec succès !
     reports:
+      account:
+        note: note
+        report: signaler
       action_taken_by: Intervention de
       are_you_sure: Êtes vous certain⋅e ?
+      assign_to_self: Me l'assigner
+      assigned: Modérateur assigné
       comment:
-        label: Commentaire
         none: Aucun
+      created_at: Signalé
       delete: Supprimer
       id: ID
       mark_as_resolved: Marquer comme résolu
+      mark_as_unresolved: Marquer comme non-résolu
+      notes:
+        create: Ajouter une note
+        create_and_resolve: Résoudre avec une note
+        create_and_unresolve: Ré-ouvrir avec une note
+        delete: Effacer
+        placeholder: Décrivez quelles actions ont été prises, ou toute autre mise à jour de ce signalement…
       nsfw:
         'false': Ré-afficher les médias
         'true': Masquer les médias
+      reopen: Ré-ouvrir le signalement
       report: 'Signalement #%{id}'
       report_contents: Contenu
       reported_account: Compte signalé
       reported_by: Signalé par
       resolved: Résolus
+      resolved_msg: Signalement résolu avec succès !
       silence_account: Masquer le compte
       status: Statut
       suspend_account: Suspendre le compte
       target: Cible
       title: Signalements
+      unassign: Dés-assigner
       unresolved: Non résolus
+      updated_at: Mis à jour
       view: Voir
     settings:
       activity_api_enabled:
@@ -319,8 +354,8 @@ fr:
       back_to_account: Retour à la page du compte
       batch:
         delete: Supprimer
-        nsfw_off: NSFW OFF
-        nsfw_on: NSFW ON
+        nsfw_off: Marquer comme non-sensible
+        nsfw_on: Marquer comme sensible
       execute: Exécuter
       failed_to_execute: Erreur d’exécution
       media:
@@ -382,6 +417,7 @@ fr:
     security: Sécurité
     set_new_password: Définir le nouveau mot de passe
   authorize_follow:
+    already_following: Vous suivez déjà ce compte
     error: Malheureusement, il y a eu une erreur en cherchant les détails du compte distant
     follow: Suivre
     follow_request: 'Vous avez demandé à suivre :'
@@ -474,6 +510,7 @@ fr:
       '21600': 6 heures
       '3600': 1 heure
       '43200': 12 heures
+      '604800': 1 semaine
       '86400': 1 jour
     expires_in_prompt: Jamais
     generate: Générer
@@ -577,6 +614,10 @@ fr:
     missing_resource: L’URL de redirection n’a pas pu être trouvée
     proceed: Continuez pour suivre
     prompt: 'Vous allez suivre :'
+  remote_unfollow:
+    error: Erreur
+    title: Titre
+    unfollowed: Non-suivi
   sessions:
     activity: Dernière activité
     browser: Navigateur
@@ -642,6 +683,10 @@ fr:
       video:
         one: "%{count} vidéo"
         other: "%{count} vidéos"
+    content_warning: 'Attention au contenu : %{warning}'
+    disallowed_hashtags:
+      one: 'contient un hashtag désactivé : %{tags}'
+      other: 'contient les hashtag désactivés : %{tags}'
     open_in_web: Ouvrir sur le web
     over_character_limit: limite de caractères dépassée de %{max} caractères
     pin_errors:
diff --git a/config/locales/gl.yml b/config/locales/gl.yml
index f4ca7e8c5..6f2270224 100644
--- a/config/locales/gl.yml
+++ b/config/locales/gl.yml
@@ -4,6 +4,7 @@ gl:
     about_hashtag_html: Estas son mensaxes públicas etiquetadas con <strong>#%{hashtag}</strong>. Pode interactuar con elas si ten unha conta nalgures do fediverso.
     about_mastodon_html: Mastodon é unha rede social que se basea en protocolos web abertos e libres, software de código aberto. É descentralizada como o correo electrónico.
     about_this: Sobre
+    administered_by: 'Administrada por:'
     closed_registrations: O rexistro en esta instancia está pechado en este intre. Porén! Pode atopar unha instancia diferente para obter unha conta e ter acceso exactamente a misma rede desde alí.
     contact: Contacto
     contact_missing: Non establecido
@@ -60,7 +61,15 @@ gl:
       destroyed_msg: Nota a moderación destruída con éxito!
     accounts:
       are_you_sure: Está segura?
+      avatar: Avatar
       by_domain: Dominio
+      change_email:
+        changed_msg: Cambiouse correctamente o correo-e da conta!
+        current_email: Correo-e actual
+        label: Cambiar correo-e
+        new_email: Novo correo-e
+        submit: Cambiar correo-e
+        title: Cambiar o correo-e de %{username}
       confirm: Confirmar
       confirmed: Confirmado
       demote: Degradar
@@ -108,6 +117,7 @@ gl:
       public: Público
       push_subscription_expires: A suscrición PuSH caduca
       redownload: Actualizar avatar
+      remove_avatar: Eliminar avatar
       reset: Restablecer
       reset_password: Restablecer contrasinal
       resubscribe: Voltar a suscribir
@@ -128,6 +138,7 @@ gl:
       statuses: Estados
       subscribe: Subscribir
       title: Contas
+      unconfirmed_email: Correo-e non confirmado
       undo_silenced: Desfacer acalar
       undo_suspension: Desfacer suspensión
       unsubscribe: Non subscribir
@@ -135,6 +146,8 @@ gl:
       web: Web
     action_logs:
       actions:
+        assigned_to_self_report: "%{name} asignou o informe %{target} a ela misma"
+        change_email_user: "%{name} cambiou o enderezo de correo-e da usuaria %{target}"
         confirm_user: "%{name} comfirmou o enderezo de correo da usuaria %{target}"
         create_custom_emoji: "%{name} subeu un novo emoji %{target}"
         create_domain_block: "%{name} bloqueou o dominio %{target}"
@@ -150,10 +163,13 @@ gl:
         enable_user: "%{name} habilitou a conexión para a usuaria %{target}"
         memorialize_account: "%{name} converteu a conta de  %{target} nunha páxina para a lembranza"
         promote_user: "%{name} promoveu a usuaria %{target}"
+        remove_avatar_user: "%{name} eliminou o avatar de %{target}"
+        reopen_report: "%{name} voltou abrir informe  %{target}"
         reset_password_user: "%{name} restableceu o contrasinal da usuaria %{target}"
-        resolve_report: "%{name} rexeitou o informe %{target}"
+        resolve_report: "%{name} solucionou o informe %{target}"
         silence_account: "%{name} acalou a conta de %{target}"
         suspend_account: "%{name} suspendeu a conta de %{target}"
+        unassigned_report: "%{name} non asignou informe %{target}"
         unsilence_account: "%{name} deulle voz a conta de %{target}"
         unsuspend_account: "%{name} activou a conta de %{target}"
         update_custom_emoji: "%{name} actualizou emoji %{target}"
@@ -239,29 +255,48 @@ gl:
         expired: Cadudado
         title: Filtro
       title: Convida
+    report_notes:
+      created_msg: Creouse correctamente a nota do informe!
+      destroyed_msg: Nota do informe eliminouse con éxito!
     reports:
+      account:
+        note: nota
+        report: informe
       action_taken_by: Acción tomada por
       are_you_sure: Está segura?
+      assign_to_self: Asignarmo
+      assigned: Moderador asignado
       comment:
-        label: Comentario
         none: Nada
+      created_at: Reportado
       delete: Eliminar
       id: ID
       mark_as_resolved: Marcar como resolto
+      mark_as_unresolved: Marcar como non resolto
+      notes:
+        create: Engadir nota
+        create_and_resolve: Resolver con nota
+        create_and_unresolve: Voltar a abrir con nota
+        delete: Eliminar
+        placeholder: Describir qué decisións foron tomadas, ou calquer actualización a este informe…
       nsfw:
         'false': Non agochar anexos de medios
         'true': Agochar anexos de medios
+      reopen: Voltar a abrir o informe
       report: 'Informe #%{id}'
       report_contents: Contidos
       reported_account: Conta reportada
       reported_by: Reportada por
       resolved: Resolto
+      resolved_msg: Resolveuse con éxito o informe!
       silence_account: Acalar conta
       status: Estado
       suspend_account: Suspender conta
       target: Obxetivo
       title: Informes
+      unassign: Non asignar
       unresolved: Non resolto
+      updated_at: Actualizado
       view: Vista
     settings:
       activity_api_enabled:
@@ -319,8 +354,8 @@ gl:
       back_to_account: Voltar a páxina da conta
       batch:
         delete: Eliminar
-        nsfw_off: NSFW apagado
-        nsfw_on: NSFW acendido
+        nsfw_off: Marcar como non sensible
+        nsfw_on: Marcar como sensible
       execute: Executar
       failed_to_execute: Fallou a execución
       media:
@@ -382,6 +417,7 @@ gl:
     security: Seguridade
     set_new_password: Establecer novo contrasinal
   authorize_follow:
+    already_following: Xa está a seguir esta conta
     error: Desgraciadamente, algo fallou ao buscar a conta remota
     follow: Seguir
     follow_request: 'Enviou unha petición de seguimento a:'
@@ -474,6 +510,7 @@ gl:
       '21600': 6 horas
       '3600': 1 hora
       '43200': 12 horas
+      '604800': 1 semana
       '86400': 1 día
     expires_in_prompt: Nunca
     generate: Xerar
@@ -577,6 +614,10 @@ gl:
     missing_resource: Non se puido atopar o URL de redirecionamento requerido para a súa conta
     proceed: Proceda para seguir
     prompt: 'Vostede vai seguir:'
+  remote_unfollow:
+    error: Fallo
+    title: Título
+    unfollowed: Deixou de seguir
   sessions:
     activity: Última actividade
     browser: Navegador
@@ -643,6 +684,9 @@ gl:
         one: "%{count}  vídeo"
         other: "%{count} vídeos"
     content_warning: 'Aviso sobre o contido: %{warning}'
+    disallowed_hashtags:
+      one: 'contiña unha etiqueta non permitida: %{tags}'
+      other: 'contiña etiquetas non permitidas: %{tags}'
     open_in_web: Abrir na web
     over_character_limit: Excedeu o límite de caracteres %{max}
     pin_errors:
@@ -662,11 +706,89 @@ gl:
   stream_entries:
     click_to_show: Pulse para mostrar
     pinned: Mensaxe fixada
-    reblogged: promocionada
+    reblogged: promovida
     sensitive_content: Contido sensible
   terms:
+    body_html: |
+      <h2>Intimidade</h2>
+      <h3 id="collect">Qué información recollemos?</h3>
+
+      <ul>
+        <li><em>Información básica da conta</em>: Si se rexistra en este servidor, pediráselle un nome de usuaria, un enderezo de correo electrónico e un contrasinal. De xeito adicional tamén poderá introducir información como un nome público e biografía, tamén subir unha fotografía de perfil e unha imaxe para a cabeceira. O nome de usuaria, o nome público, a biografía e as imaxes de perfil e cabeceira sempre se mostran publicamente.</li>
+        <li><em>Publicacións, seguimento e outra información pública</em>: O listado das persoas que segue é un listado público, o mesmo acontece coas súas seguidoras. Cando evía unha mensaxe, a data e hora gárdanse así como o aplicativo que utilizou para enviar a mensaxe. As publicacións poderían conter ficheiros de medios anexos, como fotografías e vídeos. As publicacións públicas e as non listadas están dispoñibles de xeito público. Cando destaca unha publicación no seu perfil tamén é pública. As publicacións son enviadas as súas seguidoras, en algúns casos pode acontecer que estén en diferentes servidores e gárdanse copias neles. Cando elemina unha publicación tamén se envía as súas seguidoras. A acción de voltar a publicar ou marcar como favorita outra publicación sempre é pública.</li>
+        <li><em>Mensaxes directas e só para seguidoras</em>: Todas as mensaxes gárdanse e procésanse no servidor. As mensaxes só para seguidoras son entregadas as súas seguidoras e as usuarias que son mencionadas en elas, e as mensaxes directas entréganse só as usuarias mencionadas en elas. En algúns casos esto implica que son entregadas a diferentes servidores e gárdanse copias alí. Facemos un esforzo sincero para limitar o acceso a esas publicacións só as persoas autorizadas, pero outros servidores poderían non ser tan escrupulosos. Polo tanto, é importante revisar os servidores onde se hospedan as súas seguidoras. Nos axustes pode activar a opción de aprovar ou rexeitar novas seguidoras de xeito manual.  <em>Teña en conta que a administración do servidor e todos os outros servidores implicados poden ver as mensaxes.</em>, e as destinatarias poderían facer capturas de pantalla, copiar e voltar a compartir as mensaxes. <em>Non comparta información comprometida en Mastodon.</em></li>
+        <li><em>IPs e outros metadatos</em>: Cando se conecta, gravamos o IP desde onde se conecta, así como o nome do aplicativo desde onde o fai. Todas as sesións conectadas están dispoñibles para revisar e revogar nos axustes. O último enderezo IP utilizado gárdase ate por 12 meses. Tamén poderiamos gardar informes do servidor que inclúan o enderezo IP de cada petición ao servidor.</li>
+      </ul>
+
+      <hr class="spacer" />
+
+      <h3 id="use">De qué xeito utilizamos os seus datos?</h3>
+
+      <p>Toda a información que recollemos podería ser utilizada dos seguintes xeitos:</p>
+
+      <ul>
+        <li>Para proporcionar a funcionabiliade básica de Mastodon. Só pode interactuar co contido de outra xente e publicar o seu propio contido si está conectada. Por exemplo, podería seguir outra xente e ver as súas publicacións combinadas nunha liña temporal inicial personalizada.</li>
+        <li>Para axudar a moderar a comunidade, por exemplo comparando o seu enderezo IP con outros coñecidos para evitar esquivar os rexeitamentos ou outras infraccións.</li>
+        <li>O endero de correo electrónico que nos proporciona podería ser utilizado para enviarlle información, notificacións sobre outra xente que interactúa cos seus contidos ou lle envía mensaxes, e para respostar a consultas, e/ou outras cuestións ou peticións.</li>
+      </ul>
+
+      <hr class="spacer" />
+
+      <h3 id="protect">Cómo proxetemos os seus datos?</h3>
+
+      <p>Implementamos varias medidas de seguridade para protexer os seus datos personais cando introduce, envía ou accede a súa información personal. Entre outras medidas, a súa sesión de navegación, así como o tráfico entre os seus aplicativos e o API están aseguradas mediante SSL, e o seu contrasinal está camuflado utilizando un algoritmo potente de unha sóa vía. Pode habilitar a autenticación de doble factor para protexer o acceso a súa conta aínda máis.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="data-retention">Cal é a nosa política de retención de datos?</h3>
+
+      <p>Faremos un sincero esforzo en:</p>
+
+      <ul>
+        <li>Protexer informes do servidor que conteñan direccións IP das peticións ao servidor, ate a data estos informes gárdanse por non máis de 90 días.</li>
+        <li>Reter os enderezos IP asociados con usuarias rexistradas non máis de 12 meses.</li>
+      </ul>
+
+      <p>Pode solicitar e descargar un ficheiro cos seus contidos, incluíndo publicacións, anexos de medios, imaxes de perfil e imaxe da cabeceira.</p>
+
+      <p>En calquer momento pode eliminar de xeito irreversible a súa conta.</p>
+
+      <hr class="spacer"/>
+
+      <h3 id="cookies">Utilizamos testemuños?</h3>
+
+      <p>Si. Os testemuños son pequenos ficheiros que un sitio web ou o provedor de servizo transfiren ao disco duro da súa computadora a través do navegador web (si vostede o permite). Estos testemuños posibilitan ao sitio web recoñecer o seu navegador e, si ten unha conta rexistrada, asocialo con dita conta.</p>
+
+      <p>Utilizamos testemuños para comprender e gardar as súas preferencias para futuras visitas.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="disclose">Entregamos algunha información a terceiras alleas?</h3>
+
+      <p>Non vendemos, negociamos ou transferimos de algún xeito a terceiras partes alleas a súa información identificativa persoal. Esto non inclúe terceiras partes de confianza que nos axudan a operar o sitio web, a xestionar a empresa, ou darlle servizo si esas partes aceptan manter esa información baixo confidencialidade. Poderiamos liberar esa información si cremos que eso da cumplimento axeitado a lei, reforza as políticas do noso sitio ou protexe os nosos, e de outros, dereitos, propiedade ou seguridade.</p>
+
+      <p>O seu contido público podería ser descargado por outros servidores na rede. As súas publicacións públicas e para só seguidoras son entregadas aos servidores onde residen as súas seguidoras na rede, e as mensaxes directas son entregadas aos servidores das destinatarias sempre que esas seguidoras ou destinatarios residan en servidores distintos de este.</p>
+
+      <p>Cado autoriza a este aplicativo a utilizar a súa conta, dependendo da amplitude dos permisos que autorice, podería acceder a información pública de perfil, ao listado de seguimento, as súas seguidoras, os seus listados, todas as súas publicacións, as publicacións favoritas. Os aplicativos non poden nunca acceder ao seu enderezo de correo nin ao seu contrasinal.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="coppa">Children's Online Privacy Protection Act Compliance</h3>
+
+      <p>O noso sitio, productos e servizos diríxense a persoas que teñen un mínimo de 13 anos. Si este servidor está en EEUU, e ten vostede menos de 13 anos, a requerimento da COPPA (<a href="https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act">Children's Online Privacy Protection Act</a>) non utilice este sitio.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="changes">Cambios na nosa política de intimidade</h3>
+
+      <p>Si decidimos cambiar a nosa política de intimidade publicaremos os cambios en esta páxina.</p>
+
+      <p>Este documento ten licenza CC-BY-SA. Actualizouse o 7 de Marzo de 2018.</p>
+
+      <p>Adaptado do orixinal <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>.</p>
     title: "%{instance} Termos do Servizo e Política de Intimidade"
   themes:
+    contrast: Alto contraste
     default: Mastodon
   time:
     formats:
diff --git a/config/locales/he.yml b/config/locales/he.yml
index 1a7c84d7c..d641c6e1a 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -180,7 +180,6 @@ he:
     reports:
       are_you_sure: 100% על בטוח?
       comment:
-        label: הערה
         none: ללא
       delete: מחיקה
       id: ID
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index 2560b3816..7fe431d37 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -243,7 +243,6 @@ hu:
       action_taken_by: 'Kezelte:'
       are_you_sure: Biztos vagy benne?
       comment:
-        label: Hozzászólás
         none: Egyik sem
       delete: Törlés
       id: ID
diff --git a/config/locales/id.yml b/config/locales/id.yml
index 0ef1d5040..5a63b8038 100644
--- a/config/locales/id.yml
+++ b/config/locales/id.yml
@@ -106,7 +106,6 @@ id:
       title: Server yang diketahui
     reports:
       comment:
-        label: Komentar
         none: Tidak ada
       delete: Hapus
       id: ID
diff --git a/config/locales/io.yml b/config/locales/io.yml
index 29ab4516b..7c25acc47 100644
--- a/config/locales/io.yml
+++ b/config/locales/io.yml
@@ -105,7 +105,6 @@ io:
       title: Known Instances
     reports:
       comment:
-        label: Comment
         none: None
       delete: Delete
       id: ID
diff --git a/config/locales/it.yml b/config/locales/it.yml
index 7e5bfd20e..0518d20e6 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -3,43 +3,312 @@ it:
   about:
     about_mastodon_html: Mastodon è un social network <em>gratuito e open-source</em>. Un'alternativa <em>decentralizzata</em> alle piattaforme commerciali che evita che una singola compagnia monopolizzi il tuo modo di comunicare. Scegli un server di cui ti fidi &mdash; qualunque sia la tua scelta, potrai interagire con chiunque altro. Chiunque può sviluppare un suo server Mastodon e partecipare alla vita del <em>social network</em>.
     about_this: A proposito di questo server
-    closed_registrations: Al momento le iscrizioni a questo server sono chiuse.
+    administered_by: 'Amministrato da:'
+    closed_registrations: Al momento le iscrizioni a questo server sono chiuse. Tuttavia! Puoi provare a cercare un istanza diversa su cui creare un account ed avere accesso alla stessa identica rete di questa.
     contact: Contatti
+    contact_missing: Non impostato
+    contact_unavailable: N/D
     description_headline: Cos'è %{domain}?
     domain_count_after: altri server
     domain_count_before: Connesso a
-    other_instances: Altri server
+    features:
+      humane_approach_body: Imparando dai fallimenti degli altri networks, Mastodon mira a fare scelte di design etico per combattere l'abuso dei social media.
+      humane_approach_title: Un approccio più umano
+      not_a_product_body: Mastodon non è una rete commerciale. Niente pubblicità, niente data mining, nessun giardino murato. Non c'è nessuna autorità centrale.
+      not_a_product_title: Tu sei una persona, non un prodotto
+      real_conversation_body: Con 500 caratteri a disposizione, un supporto per i contenuti granulari ed avvisi sui media potrai esprimerti nel modo desiderato.
+      real_conversation_title: Creato per conversazioni reali
+      within_reach_body: Apps per iOS, Android ed altre piattaforme, realizzate grazie ad un ecosistema di API adatto agli sviluppatori, ti consentono di poter stare ovunque al passo con i tuoi amici.
+      within_reach_title: Sempre a portata di mano
+    generic_description: "%{domain} è un server nella rete"
+    hosted_on: Mastodon ospitato su %{domain}
+    learn_more: Scopri altro
+    other_instances: Elenco istanze
     source_code: Codice sorgente
-    status_count_after: status
+    status_count_after: stati
     status_count_before: Che hanno pubblicato
     user_count_after: utenti
-    user_count_before: Casa di
+    user_count_before: Home di
+    what_is_mastodon: Che cos'è Mastodon?
   accounts:
     follow: Segui
     followers: Seguaci
     following: Seguiti
+    media: Media
+    moved_html: "%{name} è stato spostato su %{new_profile_link}:"
     nothing_here: Qui non c'è nulla!
     people_followed_by: Persone seguite da %{name}
     people_who_follow: Persone che seguono %{name}
     posts: Posts
+    posts_with_replies: Toot e repliche
     remote_follow: Segui da remoto
+    reserved_username: Il nome utente è riservato
+    roles:
+      admin: Amministratore
+      moderator: Mod
     unfollow: Non seguire più
+  admin:
+    account_moderation_notes:
+      account: Moderatore
+      create: Crea
+      created_at: Data
+      created_msg: Nota di moderazione creata con successo!
+      delete: Elimina
+      destroyed_msg: Nota di moderazione distrutta con successo!
+    accounts:
+      are_you_sure: Sei sicuro?
+      avatar: Avatar
+      by_domain: Dominio
+      change_email:
+        changed_msg: Account email cambiato con successo!
+        current_email: Email corrente
+        label: Cambia email
+        new_email: Nuova email
+        submit: Cambia email
+        title: Cambia email per %{username}
+      confirm: Conferma
+      confirmed: Confermato
+      demote: Declassa
+      disable: Disabilita
+      disable_two_factor_authentication: Disabilita 2FA
+      disabled: Disabilitato
+      display_name: Nome visualizzato
+      domain: Dominio
+      edit: Modifica
+      email: Email
+      enable: Abilita
+      enabled: Abilitato
+      feed_url: URL Feed
+      followers: Follower
+      followers_url: URL follower
+      follows: Follows
+      inbox_url: URL inbox
+      ip: IP
+      location:
+        all: Tutto
+        local: Locale
+        remote: Remoto
+        title: Luogo
+      login_status: Stato login
+      media_attachments: Media allegati
+      memorialize: Trasforma in memoriam
+      moderation:
+        all: Tutto
+        silenced: Silenziati
+        suspended: Sospesi
+        title: Moderazione
+      moderation_notes: Note di moderazione
+      most_recent_activity: Attività più recenti
+      most_recent_ip: IP più recenti
+      not_subscribed: Non sottoscritto
+      order:
+        alphabetic: Alfabetico
+        most_recent: Più recente
+        title: Ordine
+      outbox_url: URL outbox
+      perform_full_suspension: Esegui sospensione completa
+      profile_url: URL profilo
+      promote: Promuovi
+      protocol: Protocollo
+      public: Pubblico
+      redownload: Aggiorna avatar
+      remove_avatar: Rimuovi avatar
+      reset: Reimposta
+      reset_password: Reimposta password
+      resubscribe: Riscriversi
+      role: Permessi
+      roles:
+        admin: Amministratore
+        moderator: Moderatore
+        staff: Staff
+        user: Utente
+      search: Cerca
+      silence: Silenzia
+      statuses: Stati
+      subscribe: Sottoscrivi
+      title: Account
+      unconfirmed_email: Email non confermata
+      undo_silenced: Rimuovi silenzia
+      undo_suspension: Rimuovi sospensione
+      unsubscribe: Annulla l'iscrizione
+      username: Nome utente
+      web: Web
+    action_logs:
+      actions:
+        change_email_user: "%{name} ha cambiato l'indirizzo e-mail per l'utente %{target}"
+        confirm_user: "%{name} ha confermato l'indirizzo email per l'utente %{target}"
+        create_custom_emoji: "%{name} ha caricato un nuovo emoji %{target}"
+        create_domain_block: "%{name} ha bloccato il dominio %{target}"
+    custom_emojis:
+      by_domain: Dominio
+      copied_msg: Creata con successo una copia locale dell'emoji
+      copy: Copia
+      copy_failed_msg: Impossibile creare una copia locale di questo emoji
+      created_msg: Emoji creato con successo!
+      delete: Elimina
+      destroyed_msg: Emoji distrutto con successo!
+      disable: Disabilita
+      disabled_msg: Questa emoji è stata disabilitata con successo
+      emoji: Emoji
+      enable: Abilita
+      enabled_msg: Questa emoji è stata abilitata con successo
+      image_hint: PNG fino a 50KB
+      listed: Elencato
+      new:
+        title: Aggiungi nuovo emoji personalizzato
+      overwrite: Sovrascrivi
+      shortcode: Shortcode
+      title: Emoji personalizzate
+      unlisted: Non elencato
+      update_failed_msg: Impossibile aggiornare questa emojii
+      updated_msg: Emoji aggiornata con successo!
+      upload: Carica
+    domain_blocks:
+      add_new: Aggiungi nuovo
+      created_msg: Il blocco del dominio sta venendo processato
+      destroyed_msg: Il blocco del dominio è stato rimosso
+      domain: Dominio
+      new:
+        create: Crea blocco
+        severity:
+          noop: Nessuno
+          silence: Silenzia
+          suspend: Sospendi
+        title: Nuovo blocco dominio
+      severities:
+        noop: Nessuno
+        silence: Silenzia
+        suspend: Sospendi
+      severity: Severità
+      show:
+        undo: Annulla
+      title: Blocchi dominio
+      undo: Annulla
+    email_domain_blocks:
+      add_new: Aggiungi nuovo
+      created_msg: Dominio e-mail aggiunto con successo alla lista nera
+      delete: Elimina
+      destroyed_msg: Dominio e-mail cancellato con successo dalla lista nera
+      domain: Dominio
+      new:
+        create: Aggiungi dominio
+    instances:
+      account_count: Accounts conosciuti
+      domain_name: Dominio
+      reset: Reimposta
+      search: Cerca
+      title: Istanze conosciute
+    invites:
+      filter:
+        all: Tutto
+        available: Disponibile
+        expired: Scaduto
+        title: Filtro
+      title: Inviti
+    reports:
+      account:
+        note: note
+      action_taken_by: Azione intrapresa da
+      are_you_sure: Sei sicuro?
+      assign_to_self: Assegna a me
+      assigned: Moderatore assegnato
+      comment:
+        none: Nessuno
+      delete: Elimina
+      id: ID
+      mark_as_resolved: Segna come risolto
+      mark_as_unresolved: Segna come non risolto
+      notes:
+        create: Aggiungi nota
+        create_and_resolve: Risolvi con nota
+        create_and_unresolve: Riapri con nota
+        delete: Elimina
+      nsfw:
+        'false': Mostra gli allegati multimediali
+        'true': Nascondi allegati multimediali
+      report_contents: Contenuti
+      resolved: Risolto
+      silence_account: Silenzia account
+      status: Stato
+      suspend_account: Sospendi account
+      target: Obbiettivo
+      unassign: Non assegnare
+      unresolved: Non risolto
+      updated_at: Aggiornato
+      view: Mostra
+    settings:
+      activity_api_enabled:
+        title: Pubblica statistiche aggregate circa l'attività dell'utente
+      contact_information:
+        username: Nome utente del contatto
+      peers_api_enabled:
+        title: Pubblica elenco di istanze scoperte
+      registrations:
+        deletion:
+          desc_html: Consenti a chiunque di cancellare il proprio account
+          title: Apri la cancellazione dell'account
+        min_invite_role:
+          disabled: Nessuno
+        open:
+          desc_html: Consenti a chiunque di creare un account
+      show_staff_badge:
+        title: Mostra badge staff
+      site_description:
+        title: Descrizione istanza
+      site_terms:
+        title: Termini di servizio personalizzati
+      site_title: Nome istanza
+      timeline_preview:
+        title: Anteprima timeline
+      title: Impostazioni sito
+    statuses:
+      batch:
+        delete: Elimina
+        nsfw_off: NSFW OFF
+        nsfw_on: NSFW ON
+      execute: Esegui
+      failed_to_execute: Impossibile eseguire
+      media:
+        hide: Nascondi media
+        show: Mostra media
+        title: Media
+      no_media: Nessun media
+      with_media: con media
+    subscriptions:
+      callback_url: URL Callback
+      confirmed: Confermato
+      expires_in: Scade in
+      topic: Argomento
+    title: Amministrazione
   application_mailer:
+    notification_preferences: Cambia preferenze email
+    salutation: "%{name},"
     settings: 'Cambia le impostazioni per le e-mail: %{link}'
     view: 'Guarda:'
+    view_profile: Mostra profilo
+    view_status: Mostra stati
   applications:
+    created: Applicazione creata con successo
+    destroyed: Applicazione eliminata con successo
     invalid_url: L'URL fornito non è valido
   auth:
+    change_password: Password
+    confirm_email: Conferma email
+    delete_account: Elimina account
     didnt_get_confirmation: Non hai ricevuto le istruzioni di conferma?
     forgot_password: Hai dimenticato la tua password?
     login: Entra
     logout: Logout
+    migrate_account: Sposta ad un account differente
+    or: o
     register: Iscriviti
+    register_elsewhere: Iscriviti su un altro server
     resend_confirmation: Invia di nuovo le istruzioni di conferma
     reset_password: Resetta la password
     security: Credenziali
     set_new_password: Imposta una nuova password
   authorize_follow:
+    already_following: Stai già seguendo questo account
     error: Sfortunatamente c'è stato un errore nel consultare l'account remoto
     follow: Segui
     title: Segui %{acct}
@@ -161,6 +430,10 @@ it:
     manual_instructions: 'Se non puoi scannerizzare il QR code e hai bisogno di inserirlo manualmente, questo è il codice segreto in chiaro:'
     setup: Configura
     wrong_code: Il codice inserito non è corretto! Assicurati che l'orario del server e l'orario del telefono siano corretti.
+  user_mailer:
+    welcome:
+      tips: Suggerimenti
+      title: Benvenuto a bordo, %{name}!
   users:
     invalid_email: L'indirizzo e-mail inserito non è valido
     invalid_otp_token: Codice d'accesso non valido
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 01fb9657f..5585ec098 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -68,7 +68,7 @@ ja:
         current_email: 現在のメールアドレス
         label: メールアドレスを変更
         new_email: 新しいメールアドレス
-        submit: Change Email
+        submit: メールアドレスの変更
         title: "%{username} さんのメールアドレスを変更"
       confirm: 確認
       confirmed: 確認済み
@@ -259,16 +259,17 @@ ja:
       created_msg: レポートメモを書き込みました!
       destroyed_msg: レポートメモを削除しました!
     reports:
+      account:
+        note: メモ
+        report: レポート
       action_taken_by: レポート処理者
       are_you_sure: 本当に実行しますか?
       assign_to_self: 担当になる
       assigned: 担当者
       comment:
-        label: コメント
         none: なし
       created_at: レポート日時
       delete: 削除
-      history: モデレーション履歴
       id: ID
       mark_as_resolved: 解決済みとしてマーク
       mark_as_unresolved: 未解決として再び開く
@@ -277,9 +278,7 @@ ja:
         create_and_resolve: 書き込み、解決済みにする
         create_and_unresolve: 書き込み、未解決として開く
         delete: 削除
-        label: モデレーターメモ
-        new_label: モデレーターメモの追加
-        placeholder: このレポートに取られた措置やその他更新を記述してください
+        placeholder: このレポートに取られた措置や、その他の更新を記述してください…
       nsfw:
         'false': NSFW オフ
         'true': NSFW オン
@@ -292,7 +291,6 @@ ja:
       resolved_msg: レポートを解決済みにしました!
       silence_account: アカウントをサイレンス
       status: ステータス
-      statuses: 通報されたトゥート
       suspend_account: アカウントを停止
       target: ターゲット
       title: レポート
@@ -356,8 +354,8 @@ ja:
       back_to_account: アカウントページに戻る
       batch:
         delete: 削除
-        nsfw_off: NSFW オフ
-        nsfw_on: NSFW オン
+        nsfw_off: 閲覧注意のマークを取り除く
+        nsfw_on: 閲覧注意としてマークする
       execute: 実行
       failed_to_execute: 実行に失敗しました
       media:
@@ -697,6 +695,9 @@ ja:
         one: "%{count} 本の動画"
         other: "%{count} 本の動画"
     content_warning: '閲覧注意: %{warning}'
+    disallowed_hashtags:
+      one: '許可されていないハッシュタグが含まれています: %{tags}'
+      other: '許可されていないハッシュタグが含まれています: %{tags}'
     open_in_web: Webで開く
     over_character_limit: 上限は %{max}文字までです
     pin_errors:
@@ -798,6 +799,7 @@ ja:
       <p>オリジナルの出典: <a href="https://github.com/discourse/discourse">Discourse privacy policy</a></p>
     title: "%{instance} 利用規約・プライバシーポリシー"
   themes:
+    contrast: ハイコントラスト
     default: Mastodon
   time:
     formats:
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index bbf27d5c3..251c0c3d7 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -4,6 +4,7 @@ ko:
     about_hashtag_html: "<strong>#%{hashtag}</strong> 라는 해시태그가 붙은 공개 툿 입니다. 같은 연합에 속한 임의의 인스턴스에 계정을 생성하면 당신도 대화에 참여할 수 있습니다."
     about_mastodon_html: Mastodon은 <em>오픈 소스 기반의</em> 소셜 네트워크 서비스 입니다. 상용 플랫폼의 대체로서 <em>분산형 구조</em>를 채택해, 여러분의 대화가 한 회사에 독점되는 것을 방지합니다. 신뢰할 수 있는 인스턴스를 선택하세요 &mdash; 어떤 인스턴스를 고르더라도, 누구와도 대화할 수 있습니다. 누구나 자신만의 Mastodon 인스턴스를 만들 수 있으며, 아주 매끄럽게 <em>소셜 네트워크</em>에 참가할 수 있습니다.
     about_this: 이 인스턴스에 대해서
+    administered_by: '관리자:'
     closed_registrations: 현재 이 인스턴스에서는 신규 등록을 받고 있지 않습니다.
     contact: 연락처
     contact_missing: 미설정
@@ -60,7 +61,15 @@ ko:
       destroyed_msg: 모더레이션 기록이 성공적으로 삭제되었습니다!
     accounts:
       are_you_sure: 정말로 실행하시겠습니까?
+      avatar: 아바타
       by_domain: 도메인
+      change_email:
+        changed_msg: 이메일이 성공적으로 바뀌었습니다!
+        current_email: 현재 이메일 주소
+        label: 이메일 주소 변경
+        new_email: 새 이메일 주소
+        submit: 이메일 주소 변경
+        title: "%{username}의 이메일 주소 변경"
       confirm: 확인
       confirmed: 확인됨
       demote: 모더레이터 강등
@@ -108,6 +117,7 @@ ko:
       public: 전체 공개
       push_subscription_expires: PuSH 구독 기간 만료
       redownload: 아바타 업데이트
+      remove_avatar: 아바타 지우기
       reset: 초기화
       reset_password: 비밀번호 초기화
       resubscribe: 다시 구독
@@ -128,6 +138,7 @@ ko:
       statuses: 툿 수
       subscribe: 구독하기
       title: 계정
+      unconfirmed_email: 미확인 된 이메일 주소
       undo_silenced: 침묵 해제
       undo_suspension: 정지 해제
       unsubscribe: 구독 해제
@@ -135,6 +146,8 @@ ko:
       web: 웹
     action_logs:
       actions:
+        assigned_to_self_report: "%{name}이 리포트 %{target}을 자신에게 할당했습니다"
+        change_email_user: "%{name}이 %{target}의 이메일 주소를 변경했습니다"
         confirm_user: "%{name}이 %{target}의 이메일 주소를 컨펌했습니다"
         create_custom_emoji: "%{name}이 새로운 에모지 %{target}를 추가했습니다"
         create_domain_block: "%{name}이 도메인 %{target}를 차단했습니다"
@@ -150,10 +163,13 @@ ko:
         enable_user: "%{name}이 %{target}의 로그인을 활성화 했습니다"
         memorialize_account: "%{name}이 %{target}의 계정을 메모리엄으로 전환했습니다"
         promote_user: "%{name}이 %{target}를 승급시켰습니다"
+        remove_avatar_user: "%{name}이 %{target}의 아바타를 지웠습니다"
+        reopen_report: "%{name}이 리포트 %{target}을 다시 열었습니다"
         reset_password_user: "%{name}이 %{target}의 암호를 초기화했습니다"
         resolve_report: "%{name}이 %{target} 신고를 처리됨으로 변경하였습니다"
         silence_account: "%{name}이 %{target}의 계정을 뮤트시켰습니다"
         suspend_account: "%{name}이 %{target}의 계정을 정지시켰습니다"
+        unassigned_report: "%{name}이 리포트 %{target}을 할당 해제했습니다"
         unsilence_account: "%{name}이 %{target}에 대한 뮤트를 해제했습니다"
         unsuspend_account: "%{name}이 %{target}에 대한 정지를 해제했습니다"
         update_custom_emoji: "%{name}이 에모지 %{target}를 업데이트 했습니다"
@@ -241,29 +257,48 @@ ko:
         expired: 만료됨
         title: 필터
       title: 초대
+    report_notes:
+      created_msg: 리포트 노트가 성공적으로 작성되었습니다!
+      destroyed_msg: 리포트 노트가 성공적으로 삭제되었습니다!
     reports:
+      account:
+        note: 노트
+        report: 리포트
       action_taken_by: 신고 처리자
       are_you_sure: 정말로 실행하시겠습니까?
+      assign_to_self: 나에게 할당 됨
+      assigned: 할당 된 모더레이터
       comment:
-        label: 코멘트
         none: 없음
+      created_at: 리포트 시각
       delete: 삭제
       id: ID
       mark_as_resolved: 해결 완료 처리
+      mark_as_unresolved: 미해결로 표시
+      notes:
+        create: 노트 추가
+        create_and_resolve: 노트를 작성하고 해결됨으로 표시
+        create_and_unresolve: 노트 작성과 함께 미해결로 표시
+        delete: 삭제
+        placeholder: 이 리포트에 대한 조치, 다른 업데이트 사항에 대해 설명합니다…
       nsfw:
         'false': NSFW 꺼짐
         'true': NSFW 켜짐
+      reopen: 리포트 다시 열기
       report: '신고 #%{id}'
       report_contents: 내용
       reported_account: 신고 대상 계정
       reported_by: 신고자
       resolved: 해결됨
+      resolved_msg: 리포트가 성공적으로 해결되었습니다!
       silence_account: 계정을 침묵 처리
       status: 상태
       suspend_account: 계정을 정지
       target: 대상
       title: 신고
+      unassign: 할당 해제
       unresolved: 미해결
+      updated_at: 업데이트 시각
       view: 표시
     settings:
       activity_api_enabled:
@@ -384,6 +419,7 @@ ko:
     security: 보안
     set_new_password: 새 비밀번호
   authorize_follow:
+    already_following: 이미 이 계정을 팔로우 하고 있습니다
     error: 리모트 계정을 확인하는 도중 오류가 발생했습니다
     follow: 팔로우
     follow_request: '당신은 다음 계정에 팔로우 신청을 했습니다:'
@@ -476,6 +512,7 @@ ko:
       '21600': 6 시간
       '3600': 1 시간
       '43200': 12 시간
+      '604800': 1주일
       '86400': 하루
     expires_in_prompt: 영원히
     generate: 생성
@@ -548,7 +585,7 @@ ko:
           quadrillion: Q
           thousand: K
           trillion: T
-          unit: ''
+          unit: "."
   pagination:
     newer: 새로운 툿
     next: 다음
@@ -579,6 +616,10 @@ ko:
     missing_resource: 리디렉션 대상을 찾을 수 없습니다
     proceed: 팔로우 하기
     prompt: '팔로우 하려 하고 있습니다:'
+  remote_unfollow:
+    error: 에러
+    title: 타이틀
+    unfollowed: 언팔로우됨
   sessions:
     activity: 마지막 활동
     browser: 브라우저
diff --git a/config/locales/ms.yml b/config/locales/ms.yml
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/config/locales/ms.yml
@@ -0,0 +1 @@
+{}
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 16e68fffe..1ccc01a8f 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -4,6 +4,7 @@ nl:
     about_hashtag_html: Dit zijn openbare toots die getagged zijn met <strong>#%{hashtag}</strong>. Je kunt er op reageren of iets anders mee doen als je op Mastodon (of ergens anders in de fediverse) een account hebt.
     about_mastodon_html: Mastodon is een sociaal netwerk dat gebruikt maakt van open webprotocollen en vrije software. Het is net zoals e-mail gedecentraliseerd.
     about_this: Over deze server
+    administered_by: 'Beheerd door:'
     closed_registrations: Registreren op deze server is momenteel uitgeschakeld.
     contact: Contact
     contact_missing: Niet ingesteld
@@ -60,7 +61,15 @@ nl:
       destroyed_msg: Verwijderen van opmerking voor moderatoren geslaagd!
     accounts:
       are_you_sure: Weet je het zeker?
+      avatar: Avatar
       by_domain: Domein
+      change_email:
+        changed_msg: E-mailadres van account succesvol veranderd!
+        current_email: Huidig e-mailadres
+        label: E-mailadres veranderen
+        new_email: Nieuw e-mailadres
+        submit: E-mailadres veranderen
+        title: E-mailadres veranderen voor %{username}
       confirm: Bevestigen
       confirmed: Bevestigd
       demote: Degraderen
@@ -108,6 +117,7 @@ nl:
       public: Openbaar
       push_subscription_expires: PuSH-abonnement verloopt op
       redownload: Avatar vernieuwen
+      remove_avatar: Avatar verwijderen
       reset: Opnieuw
       reset_password: Wachtwoord opnieuw instellen
       resubscribe: Opnieuw abonneren
@@ -128,6 +138,7 @@ nl:
       statuses: Toots
       subscribe: Abonneren
       title: Accounts
+      unconfirmed_email: Onbevestigd e-mailadres
       undo_silenced: Niet meer negeren
       undo_suspension: Niet meer opschorten
       unsubscribe: Opzeggen
@@ -135,6 +146,8 @@ nl:
       web: Webapp
     action_logs:
       actions:
+        assigned_to_self_report: "%{name} heeft gerapporteerde toot %{target} aan zichzelf toegewezen"
+        change_email_user: "%{name} veranderde het e-mailadres van gebruiker %{target}"
         confirm_user: E-mailadres van gebruiker %{target} is door %{name} bevestigd
         create_custom_emoji: Nieuwe emoji %{target} is door %{name} geüpload
         create_domain_block: Domein %{target} is door %{name} geblokkeerd
@@ -150,10 +163,13 @@ nl:
         enable_user: Inloggen voor %{target} is door %{name} ingeschakeld
         memorialize_account: Account %{target} is door %{name} in een in-memoriampagina veranderd
         promote_user: Gebruiker %{target} is door %{name} gepromoveerd
+        remove_avatar_user: "%{name} verwijderde de avatar van %{target}"
+        reopen_report: "%{name} heeft gerapporteerde toot %{target} heropend"
         reset_password_user: Wachtwoord van gebruiker %{target} is door %{name} opnieuw ingesteld
-        resolve_report: Gerapporteerde toots van %{target} zijn door %{name} verworpen
+        resolve_report: "%{name} heeft gerapporteerde toot %{target} opgelost"
         silence_account: Account %{target} is door %{name} genegeerd
         suspend_account: Account %{target} is door %{name} opgeschort
+        unassigned_report: "%{name} heeft het toewijzen van gerapporteerde toot %{target} ongedaan gemaakt"
         unsilence_account: Negeren van account %{target} is door %{name} opgeheven
         unsuspend_account: Opschorten van account %{target} is door %{name} opgeheven
         update_custom_emoji: Emoji %{target} is door %{name} bijgewerkt
@@ -239,29 +255,48 @@ nl:
         expired: Verlopen
         title: Filter
       title: Uitnodigingen
+    report_notes:
+      created_msg: Opmerking bij gerapporteerde toot succesvol aangemaakt!
+      destroyed_msg: Opmerking bij gerapporteerde toot succesvol verwijderd!
     reports:
+      account:
+        note: opmerking
+        report: gerapporteerde toot
       action_taken_by: Actie uitgevoerd door
       are_you_sure: Weet je het zeker?
+      assign_to_self: Aan mij toewijzen
+      assigned: Toegewezen moderator
       comment:
-        label: Opmerking
         none: Geen
+      created_at: Gerapporteerd op
       delete: Verwijderen
       id: ID
       mark_as_resolved: Markeer als opgelost
+      mark_as_unresolved: Markeer als onopgelost
+      notes:
+        create: Opmerking toevoegen
+        create_and_resolve: Oplossen met opmerking
+        create_and_unresolve: Heropenen met opmerking
+        delete: Verwijderen
+        placeholder: Beschrijf welke acties zijn ondernomen of andere opmerkingen over deze gerapporteerde toot…
       nsfw:
         'false': Media tonen
         'true': Media verbergen
+      reopen: Gerapporteerde toot heropenen
       report: 'Gerapporteerde toot #%{id}'
       report_contents: Inhoud
       reported_account: Gerapporteerde account
       reported_by: Gerapporteerd door
       resolved: Opgelost
+      resolved_msg: Gerapporteerde toot succesvol opgelost!
       silence_account: Account negeren
       status: Toot
       suspend_account: Account opschorten
       target: Gerapporteerde account
       title: Gerapporteerde toots
+      unassign: Niet meer toewijzen
       unresolved: Onopgelost
+      updated_at: Bijgewerkt
       view: Weergeven
     settings:
       activity_api_enabled:
@@ -319,8 +354,8 @@ nl:
       back_to_account: Terug naar accountpagina
       batch:
         delete: Verwijderen
-        nsfw_off: NSFW UIT
-        nsfw_on: NSFW AAN
+        nsfw_off: Als niet gevoelig markeren
+        nsfw_on: Als gevoelig markeren
       execute: Uitvoeren
       failed_to_execute: Uitvoeren mislukt
       media:
@@ -382,6 +417,7 @@ nl:
     security: Beveiliging
     set_new_password: Nieuw wachtwoord instellen
   authorize_follow:
+    already_following: Je volgt dit account al
     error: Helaas, er is een fout opgetreden bij het opzoeken van de externe account
     follow: Volgen
     follow_request: 'Jij hebt een volgverzoek ingediend bij:'
@@ -474,6 +510,7 @@ nl:
       '21600': 6 uur
       '3600': 1 uur
       '43200': 12 uur
+      '604800': 1 week
       '86400': 1 dag
     expires_in_prompt: Nooit
     generate: Genereren
@@ -577,6 +614,10 @@ nl:
     missing_resource: Kon vereiste doorverwijzings-URL voor jouw account niet vinden
     proceed: Ga door om te volgen
     prompt: 'Jij gaat volgen:'
+  remote_unfollow:
+    error: Fout
+    title: Titel
+    unfollowed: Ontvolgd
   sessions:
     activity: Laatst actief
     browser: Webbrowser
@@ -643,6 +684,9 @@ nl:
         one: "%{count} video"
         other: "%{count} video's"
     content_warning: 'Tekstwaarschuwing: %{warning}'
+    disallowed_hashtags:
+      one: 'bevatte een niet toegestane hashtag: %{tags}'
+      other: 'bevatte niet toegestane hashtags: %{tags}'
     open_in_web: In de webapp openen
     over_character_limit: Limiet van %{max} tekens overschreden
     pin_errors:
@@ -665,6 +709,83 @@ nl:
     reblogged: boostte
     sensitive_content: Gevoelige inhoud
   terms:
+    body_html: |
+      <h2>Privacy Policy</h2>
+      <h3 id="collect">What information do we collect?</h3>
+
+      <ul>
+        <li><em>Basic account information</em>: If you register on this server, you may be asked to enter a username, an e-mail address and a password. You may also enter additional profile information such as a display name and biography, and upload a profile picture and header image. The username, display name, biography, profile picture and header image are always listed publicly.</li>
+        <li><em>Posts, following and other public information</em>: The list of people you follow is listed publicly, the same is true for your followers. When you submit a message, the date and time is stored as well as the application you submitted the message from. Messages may contain media attachments, such as pictures and videos. Public and unlisted posts are available publicly. When you feature a post on your profile, that is also publicly available information. Your posts are delivered to your followers, in some cases it means they are delivered to different servers and copies are stored there. When you delete posts, this is likewise delivered to your followers. The action of reblogging or favouriting another post is always public.</li>
+        <li><em>Direct and followers-only posts</em>: All posts are stored and processed on the server. Followers-only posts are delivered to your followers and users who are mentioned in them, and direct posts are delivered only to users mentioned in them. In some cases it means they are delivered to different servers and copies are stored there. We make a good faith effort to limit the access to those posts only to authorized persons, but other servers may fail to do so. Therefore it's important to review servers your followers belong to. You may toggle an option to approve and reject new followers manually in the settings. <em>Please keep in mind that the operators of the server and any receiving server may view such messages</em>, and that recipients may screenshot, copy or otherwise re-share them. <em>Do not share any dangerous information over Mastodon.</em></li>
+        <li><em>IPs and other metadata</em>: When you log in, we record the IP address you log in from, as well as the name of your browser application. All the logged in sessions are available for your review and revocation in the settings. The latest IP address used is stored for up to 12 months. We also may retain server logs which include the IP address of every request to our server.</li>
+      </ul>
+
+      <hr class="spacer" />
+
+      <h3 id="use">What do we use your information for?</h3>
+
+      <p>Any of the information we collect from you may be used in the following ways:</p>
+
+      <ul>
+        <li>To provide the core functionality of Mastodon. You can only interact with other people's content and post your own content when you are logged in. For example, you may follow other people to view their combined posts in your own personalized home timeline.</li>
+        <li>To aid moderation of the community, for example comparing your IP address with other known ones to determine ban evasion or other violations.</li>
+        <li>The email address you provide may be used to send you information, notifications about other people interacting with your content or sending you messages, and to respond to inquiries, and/or other requests or questions.</li>
+      </ul>
+
+      <hr class="spacer" />
+
+      <h3 id="protect">How do we protect your information?</h3>
+
+      <p>We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information. Among other things, your browser session, as well as the traffic between your applications and the API, are secured with SSL, and your password is hashed using a strong one-way algorithm. You may enable two-factor authentication to further secure access to your account.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="data-retention">What is our data retention policy?</h3>
+
+      <p>We will make a good faith effort to:</p>
+
+      <ul>
+        <li>Retain server logs containing the IP address of all requests to this server, in so far as such logs are kept, no more than 90 days.</li>
+        <li>Retain the IP addresses associated with registered users no more than 12 months.</li>
+      </ul>
+
+      <p>You can request and download an archive of your content, including your posts, media attachments, profile picture, and header image.</p>
+
+      <p>You may irreversibly delete your account at any time.</p>
+
+      <hr class="spacer"/>
+
+      <h3 id="cookies">Do we use cookies?</h3>
+
+      <p>Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account.</p>
+
+      <p>We use cookies to understand and save your preferences for future visits.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="disclose">Do we disclose any information to outside parties?</h3>
+
+      <p>We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety.</p>
+
+      <p>Your public content may be downloaded by other servers in the network. Your public and followers-only posts are delivered to the servers where your followers reside, and direct messages are delivered to the servers of the recipients, in so far as those followers or recipients reside on a different server than this.</p>
+
+      <p>When you authorize an application to use your account, depending on the scope of permissions you approve, it may access your public profile information, your following list, your followers, your lists, all your posts, and your favourites. Applications can never access your e-mail address or password.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="coppa">Children's Online Privacy Protection Act Compliance</h3>
+
+      <p>Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA (<a href="https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act">Children's Online Privacy Protection Act</a>) do not use this site.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="changes">Changes to our Privacy Policy</h3>
+
+      <p>If we decide to change our privacy policy, we will post those changes on this page.</p>
+
+      <p>This document is CC-BY-SA. It was last updated March 7, 2018.</p>
+
+      <p>Originally adapted from the <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>.</p>
     title: "%{instance} Terms of Service and Privacy Policy"
   time:
     formats:
diff --git a/config/locales/no.yml b/config/locales/no.yml
index d5edb3975..8b84182af 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -243,7 +243,6 @@
       action_taken_by: Handling utført av
       are_you_sure: Er du sikker?
       comment:
-        label: Kommentar
         none: Ingen
       delete: Slett
       id: ID
diff --git a/config/locales/oc.yml b/config/locales/oc.yml
index f8e819c53..c248ffd85 100644
--- a/config/locales/oc.yml
+++ b/config/locales/oc.yml
@@ -4,6 +4,7 @@ oc:
     about_hashtag_html: Vaquí los estatuts publics ligats a <strong>#%{hashtag}</strong>. Podètz interagir amb eles s’avètz un compte ont que siasque sul fediverse.
     about_mastodon_html: Mastodon es un malhum social bastit amb de protocòls liures e gratuits. Es descentralizat coma los corrièls.
     about_this: A prepaus d’aquesta instància
+    administered_by: 'Gerida per :'
     closed_registrations: Las inscripcions son clavadas pel moment sus aquesta instància.
     contact: Contacte
     contact_missing: Pas parametrat
@@ -60,7 +61,15 @@ oc:
       destroyed_msg: Nòta de moderacion ben suprimida !
     accounts:
       are_you_sure: Sètz segur ?
+      avatar: Avatar
       by_domain: Domeni
+      change_email:
+        changed_msg: Adreça corrèctament cambiada !
+        current_email: Adreça actuala
+        label: Cambiar d’adreça
+        new_email: Novèla adreça
+        submit: Cambiar
+        title: Cambiar l’adreça a %{username}
       confirm: Confirmar
       confirmed: Confirmat
       demote: Retrogradar
@@ -108,6 +117,7 @@ oc:
       public: Public
       push_subscription_expires: Fin de l’abonament PuSH
       redownload: Actualizar los avatars
+      remove_avatar: Supriir l’avatar
       reset: Reïnicializar
       reset_password: Reïnicializar lo senhal
       resubscribe: Se tornar abonar
@@ -128,6 +138,7 @@ oc:
       statuses: Estatuts
       subscribe: S’abonar
       title: Comptes
+      unconfirmed_email: Adreça pas confirmada
       undo_silenced: Levar lo silenci
       undo_suspension: Levar la suspension
       unsubscribe: Se desabonar
@@ -135,6 +146,8 @@ oc:
       web: Web
     action_logs:
       actions:
+        assigned_to_self_report: "%{name} s’assignèt lo rapòrt %{target}"
+        change_email_user: "%{name} cambièt l’adreça de corrièl de %{target}"
         confirm_user: "%{name} confirmèt l’adreça a %{target}"
         create_custom_emoji: "%{name} mandèt un nòu emoji %{target}"
         create_domain_block: "%{name} bloquèt lo domeni %{target}"
@@ -150,6 +163,7 @@ oc:
         enable_user: "%{name} activèt la connexion per %{target}"
         memorialize_account: "%{name} transformèt en memorial la pagina de perfil a %{target}"
         promote_user: "%{name} promoguèt %{target}"
+        remove_avatar_user: "%{name} suprimèt l’avatar a %{target}"
         reset_password_user: "%{name} reïnicializèt lo senhal a %{target}"
         resolve_report: "%{name} anullèt lo rapòrt de %{target}"
         silence_account: "%{name} metèt en silenci lo compte a %{target}"
@@ -239,18 +253,31 @@ oc:
         expired: Expirats
         title: Filtre
       title: Convits
+    report_notes:
+      created_msg: Nòta de moderacion corrèctament creada !
+      destroyed_msg: Nòta de moderacion corrèctament suprimida !
     reports:
+      account:
+        note: nòta
+        report: rapòrt
       action_taken_by: Mesura menada per
       are_you_sure: Es segur ?
       comment:
-        label: Comentari
         none: Pas cap
+      created_at: Creacion
       delete: Suprimir
       id: ID
-      mark_as_resolved: Marcat coma resolgut
+      mark_as_resolved: Marcar coma resolgut
+      mark_as_unresolved: Marcar coma pas resolgut
+      notes:
+        create: Ajustar una nòta
+        create_and_resolve: Resòlvre amb una nòta
+        create_and_unresolve: Tornar dobrir amb una nòta
+        placeholder: Explicatz las accions que son estadas menadas o çò qu’es estat fach per aqueste rapòrt…
       nsfw:
         'false': Sens contengut sensible
         'true': Contengut sensible activat
+      reopen: Tornar dobrir lo rapòrt
       report: 'senhalament #%{id}'
       report_contents: Contenguts
       reported_account: Compte senhalat
@@ -382,6 +409,7 @@ oc:
     security: Seguretat
     set_new_password: Picar un nòu senhal
   authorize_follow:
+    already_following: Seguètz ja aqueste compte
     error: O planhèm, i a agut una error al moment de cercar lo compte
     follow: Sègre
     follow_request: 'Avètz demandat de sègre :'
@@ -552,6 +580,7 @@ oc:
       '21600': 6 oras
       '3600': 1 ora
       '43200': 12 oras
+      '604800': 1 setmana
       '86400': 1 jorn
     expires_in_prompt: Jamai
     generate: Generar
@@ -653,8 +682,12 @@ oc:
   remote_follow:
     acct: Picatz vòstre utilizaire@instància que cal utilizar per sègre aqueste utilizaire
     missing_resource: URL de redireccion pas trobada
-    proceed: Contunhatz per sègre
+    proceed: Clicatz per sègre
     prompt: 'Sètz per sègre :'
+  remote_unfollow:
+    error: Error
+    title: Títol
+    unfollowed: Pas mai seguit
   sessions:
     activity: Darrièra activitat
     browser: Navigator
@@ -720,6 +753,9 @@ oc:
       video:
         one: "%{count} vidèo"
         other: "%{count} vidèos"
+    disallowed_hashtags:
+      one: 'conten una etiqueta desactivada : %{tags}'
+      other: 'conten las etiquetas desactivadas : %{tags}'
     open_in_web: Dobrir sul web
     over_character_limit: limit de %{max} caractèrs passat
     pin_errors:
@@ -743,6 +779,8 @@ oc:
     sensitive_content: Contengut sensible
   terms:
     title: Condicions d’utilizacion e politica de confidencialitat de %{instance}
+  themes:
+    contrast: Fòrt contrast
   time:
     formats:
       default: Lo %d %b de %Y a %Ho%M
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 4fba2c0c1..519207d38 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -261,25 +261,16 @@ pl:
       destroyed_msg: Pomyślnie usunięto notatkę moderacyjną.
     reports:
       account:
-        created_reports: Zgłoszenia utworzone z tego konta
-        moderation:
-          silenced: Wyciszone
-          suspended: Zawieszone
-          title: Moderacja
-        moderation_notes: Notatki moderacyjne
         note: notatka
         report: zgłoszenie
-        targeted_reports: Zgłoszenia dotycząće tego konta
       action_taken_by: Działanie podjęte przez
       are_you_sure: Czy na pewno?
       assign_to_self: Przypisz do siebie
       assigned: Przypisany moderator
       comment:
-        label: Komentarz do zgłoszenia
         none: Brak
       created_at: Zgłoszono
       delete: Usuń
-      history: Historia moderacji
       id: ID
       mark_as_resolved: Oznacz jako rozwiązane
       mark_as_unresolved: Oznacz jako nierozwiązane
@@ -288,8 +279,6 @@ pl:
         create_and_resolve: Rozwiąż i pozostaw notatkę
         create_and_unresolve: Cofnij rozwiązanie i pozostaw notatkę
         delete: Usuń
-        label: Notatki
-        new_label: Dodaj notatkę moderacyjną
         placeholder: Opisz wykonane akcje i inne szczegóły dotyczące tego zgłoszenia…
       nsfw:
         'false': Nie oznaczaj jako NSFW
@@ -303,7 +292,6 @@ pl:
       resolved_msg: Pomyślnie rozwiązano zgłoszenie.
       silence_account: Wycisz konto
       status: Stan
-      statuses: Zgłoszone wpisy
       suspend_account: Zawieś konto
       target: Cel
       title: Zgłoszenia
@@ -478,7 +466,7 @@ pl:
     archive_takeout:
       date: Data
       download: Pobierz swoje archiwum
-      hint_html: Możesz uzyskać archiwum swoich <strong>wpisów i wysłanej zawartości multimedialnej</strong>. Wyeksportowane dane będą dostępne w formacie ActivityPub, obsługiwanym przez odpowiednie programy.
+      hint_html: Możesz uzyskać archiwum swoich <strong>wpisów i wysłanej zawartości multimedialnej</strong>. Wyeksportowane dane będą dostępne w formacie ActivityPub, który możesz otworzyć w obsługujących go programach.
       in_progress: Tworzenie archiwum…
       request: Uzyskaj archiwum
       size: Rozmiar
@@ -497,7 +485,7 @@ pl:
       one: W trakcie usuwania śledzących z jednej domeny…
       other: W trakcie usuwania śledzących z %{count} domen…
     true_privacy_html: Pamiętaj, że <strong>rzeczywista prywatność może zostać uzyskana wyłącznie dzięki szyfrowaniu end-to-end</strong>.
-    unlocked_warning_html: Każdy może Cię śledzić, aby natychmiastowo zobaczyć twoje wpisy. %{lock_link} aby móc kontrolować, kto Cię śledzi.
+    unlocked_warning_html: Każdy może Cię śledzić, dzięki czemu może zobaczyć Twoje niepubliczne wpisy. %{lock_link} aby móc kontrolować, kto Cię śledzi.
     unlocked_warning_title: Twoje konto nie jest zablokowane
   generic:
     changes_saved_msg: Ustawienia zapisane!
@@ -505,10 +493,12 @@ pl:
     save_changes: Zapisz zmiany
     use_this: Użyj tego
     validation_errors:
-      one: Coś jest wciąż nie tak! Przyjrzyj się błędowi poniżej
-      other: Coś jest wciąż nie tak! Przejrzyj błędy (%{count}) poniżej
+      few: Coś jest wciąż nie tak! Przejrzyj %{count} poniższe błędy
+      many: Coś jest wciąż nie tak! Przejrzyj %{count} poniższych błędów
+      one: Coś jest wciąż nie tak! Przyjrzyj się poniższemu błędowi
+      other: Coś jest wciąż nie tak! Przejrzyj poniższe błędy (%{count})
   imports:
-    preface: Możesz zaimportować pewne dane (jak dane kont, które śledzisz lub blokujesz) do swojego konta na tym serwerze, korzystając z danych wyeksportowanych z innego serwera.
+    preface: Możesz zaimportować pewne dane (np. lista kont, które śledzisz lub blokujesz) do swojego konta na tym serwerze, korzystając z danych wyeksportowanych z innego serwera.
     success: Twoje dane zostały załadowane i zostaną niebawem przetworzone
     types:
       blocking: Lista blokowanych
@@ -718,6 +708,9 @@ pl:
         one: "%{count} film"
         other: "%{count} filmów"
     content_warning: 'Ostrzeżenie o zawartości: %{warning}'
+    disallowed_hashtags:
+      one: 'zawiera niedozwolony hashtag: %{tags}'
+      other: 'zawiera niedozwolone hashtagi: %{tags}'
     open_in_web: Otwórz w przeglądarce
     over_character_limit: limit %{max} znaków przekroczony
     pin_errors:
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index ed7879525..a575998a8 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -4,6 +4,7 @@ pt-BR:
     about_hashtag_html: Estes são toots públicos com a hashtag <strong>#%{hashtag}</strong>. Você pode interagir com eles se tiver uma conta em qualquer lugar no fediverso.
     about_mastodon_html: Mastodon é uma rede social baseada em protocolos abertos e software gratuito e de código aberto. É descentralizada como e-mail.
     about_this: Sobre
+    administered_by: 'Administrado por:'
     closed_registrations: Os cadastros estão atualmente fechados nesta instância. No entanto, você pode procurar uma instância diferente na qual possa criar uma conta e acessar a mesma rede por lá.
     contact: Contato
     contact_missing: Não definido
@@ -60,7 +61,15 @@ pt-BR:
       destroyed_msg: Nota de moderação excluída com sucesso!
     accounts:
       are_you_sure: Você tem certeza?
+      avatar: Avatar
       by_domain: Domínio
+      change_email:
+        changed_msg: E-mail da conta modificado com sucesso!
+        current_email: E-mail atual
+        label: Mudar e-mail
+        new_email: Novo e-mail
+        submit: Mudar e-mail
+        title: Mudar e-mail para %{username}
       confirm: Confirmar
       confirmed: Confirmado
       demote: Rebaixar
@@ -108,6 +117,7 @@ pt-BR:
       public: Público
       push_subscription_expires: Inscrição PuSH expira
       redownload: Atualizar avatar
+      remove_avatar: Remover avatar
       reset: Anular
       reset_password: Modificar senha
       resubscribe: Reinscrever-se
@@ -128,6 +138,7 @@ pt-BR:
       statuses: Postagens
       subscribe: Inscrever-se
       title: Contas
+      unconfirmed_email: E-mail não confirmado
       undo_silenced: Retirar silenciamento
       undo_suspension: Retirar suspensão
       unsubscribe: Desinscrever-se
@@ -135,6 +146,8 @@ pt-BR:
       web: Web
     action_logs:
       actions:
+        assigned_to_self_report: "%{name} designou a denúncia %{target} para si"
+        change_email_user: "%{name} mudou o endereço de e-mail do usuário %{target}"
         confirm_user: "%{name} confirmou o endereço de e-mail do usuário %{target}"
         create_custom_emoji: "%{name} enviou o emoji novo %{target}"
         create_domain_block: "%{name} bloqueou o domínio %{target}"
@@ -150,10 +163,13 @@ pt-BR:
         enable_user: "%{name} habilitou o acesso para o usuário %{target}"
         memorialize_account: "%{name} transformou a conta de %{target} em um memorial"
         promote_user: "%{name} promoveu o usuário %{target}"
+        remove_avatar_user: "%{name} removeu o avatar de %{target}"
+        reopen_report: "%{name} reabriu a denúncia %{target}"
         reset_password_user: "%{name} redefiniu a senha do usuário %{target}"
-        resolve_report: "%{name} dispensou a denúncia %{target}"
+        resolve_report: "%{name} resolveu a denúncia %{target}"
         silence_account: "%{name} silenciou a conta de %{target}"
         suspend_account: "%{name} suspendeu a conta de %{target}"
+        unassigned_report: "%{name} desatribuiu a denúncia %{target}"
         unsilence_account: "%{name} desativou o silêncio de %{target}"
         unsuspend_account: "%{name} desativou a suspensão de  %{target}"
         update_custom_emoji: "%{name} atualizou o emoji %{target}"
@@ -239,29 +255,48 @@ pt-BR:
         expired: Expirados
         title: Filtro
       title: Convites
+    report_notes:
+      created_msg: Nota de denúncia criada com sucesso!
+      destroyed_msg: Nota de denúncia excluída com sucesso!
     reports:
+      account:
+        note: nota
+        report: denúncia
       action_taken_by: Ação realizada por
       are_you_sure: Você tem certeza?
+      assign_to_self: Designar para mim
+      assigned: Moderador designado
       comment:
-        label: Comentário
         none: Nenhum
+      created_at: Denunciado
       delete: Excluir
       id: ID
       mark_as_resolved: Marcar como resolvido
+      mark_as_unresolved: Marcar como não resolvido
+      notes:
+        create: Adicionar nota
+        create_and_resolve: Resolver com nota
+        create_and_unresolve: Reabrir com nota
+        delete: Excluir
+        placeholder: Descreva que ações foram tomadas, ou quaisquer atualizações sobre esta denúncia…
       nsfw:
         'false': Mostrar mídias anexadas
         'true': Esconder mídias anexadas
+      reopen: Reabrir denúncia
       report: 'Denúncia #%{id}'
       report_contents: Conteúdos
       reported_account: Conta denunciada
       reported_by: Denunciada por
       resolved: Resolvido
+      resolved_msg: Denúncia resolvida com sucesso!
       silence_account: Silenciar conta
       status: Status
       suspend_account: Suspender conta
       target: Alvo
       title: Denúncias
+      unassign: Desatribuir
       unresolved: Não resolvido
+      updated_at: Atualizado
       view: Visualizar
     settings:
       activity_api_enabled:
@@ -319,8 +354,8 @@ pt-BR:
       back_to_account: Voltar para página da conta
       batch:
         delete: Deletar
-        nsfw_off: NSFW ATIVADO
-        nsfw_on: NSFW DESATIVADO
+        nsfw_off: Marcar como não-sensível
+        nsfw_on: Marcar como sensível
       execute: Executar
       failed_to_execute: Falha em executar
       media:
@@ -382,6 +417,7 @@ pt-BR:
     security: Segurança
     set_new_password: Definir uma nova senha
   authorize_follow:
+    already_following: Você já está seguindo esta conta
     error: Infelizmente, ocorreu um erro ao buscar a conta remota
     follow: Seguir
     follow_request: 'Você mandou uma solicitação de seguidor para:'
@@ -474,6 +510,7 @@ pt-BR:
       '21600': 6 horas
       '3600': 1 hora
       '43200': 12 horas
+      '604800': 1 semana
       '86400': 1 dia
     expires_in_prompt: Nunca
     generate: Gerar
@@ -577,6 +614,9 @@ pt-BR:
     missing_resource: Não foi possível encontrar a URL de direcionamento para a sua conta
     proceed: Prosseguir para seguir
     prompt: 'Você irá seguir:'
+  remote_unfollow:
+    error: Erro
+    title: Título
   sessions:
     activity: Última atividade
     browser: Navegador
@@ -643,6 +683,9 @@ pt-BR:
         one: "%{count} vídeo"
         other: "%{count} vídeos"
     content_warning: 'Aviso de conteúdo: %{warning}'
+    disallowed_hashtags:
+      one: 'continha a hashtag não permitida: %{tags}'
+      other: 'continha as hashtags não permitidas: %{tags}'
     open_in_web: Abrir na web
     over_character_limit: limite de caracteres de %{max} excedido
     pin_errors:
@@ -665,6 +708,83 @@ pt-BR:
     reblogged: compartilhou
     sensitive_content: Conteúdo sensível
   terms:
+    body_html: |
+      <h2>Política de privacidade</h2>
+      <h3 id="collect">Que informação nós coletamos?</h3>
+
+      <ul>
+        <li><em>Informação básica de conta</em>: Se você se registrar nesse servidor, podemos pedir que você utilize um nome de usuário, um e-mail e uma senha. Você também pode adicionar informações extras como um nome de exibição e biografia; enviar uma imagem de perfil e imagem de cabeçalho. O nome de usuário, nome de exibição, biografia, imagem de perfil e imagem de cabeçalho são sempre listadas publicamente.</li>
+        <li><em>Posts, informação de seguidores e outras informações públicas</em>: A lista de pessoas que você segue é listada publicamente, o mesmo é verdade para quem te segue. Quando você envia uma mensagem, a data e o horário são armazenados, assim como a aplicação que você usou para enviar a mensagem. Mensagens podem conter mídias anexadas, como imagens e vídeos. Posts públicos e não-listados estão disponíveis publicamente. Quando você destaca um post no seu perfil, isso também é uma informação pública. Seus posts são entregues aos seus seguidores e em alguns casos isso significa que eles são enviados para servidores diferentes e cópias são armazenadas nesses servidores. Quando você remove posts, essa informação também é entregue aos seus seguidores. O ato de compartilhar ou favoritar um outro post é sempre público.<li>
+        <li><em>Mensagens diretas e posts somente para seguidores</em>: Todos os posts são armazenados e processados no servidor. Posts somente para seguidores são entregues aos seus seguidores e usuários que são mencionados neles; mensagens diretas são entregues somente aos usuários mencionados nelas. Em alguns casos isso significa que as mensagens são entregues para servidores diferentes e cópias são armazenadas nesses servidores. Nós fazemos esforços substanciais para limitar o acesso dessas mensagens somente para as pessoas autorizadas, mas outros servidores podem não fazer o mesmo. É importante portanto revisar os servidores à qual seus seguidores pertencem. Você pode usar uma opção para aprovar ou rejeitar novos seguidores manualmente nas configurações. <em>Por favor tenha em mente que os operadores do servidor e de qualquer servidores do destinatário podem ver tais mensagens</em>, e que os destinatários podem fazer capturas de tela, copiar ou de outra maneira compartilhar as mensagens. <em>Não compartilhe informação confidencial pelo Mastodon.</em></li>
+        <li><em>IPs and other metadata</em>: When you log in, we record the IP address you log in from, as well as the name of your browser application. All the logged in sessions are available for your review and revocation in the settings. The latest IP address used is stored for up to 12 months. We also may retain server logs which include the IP address of every request to our server.</li>
+      </ul>
+
+      <hr class="spacer" />
+
+      <h3 id="use">What do we use your information for?</h3>
+
+      <p>Any of the information we collect from you may be used in the following ways:</p>
+
+      <ul>
+        <li>To provide the core functionality of Mastodon. You can only interact with other people's content and post your own content when you are logged in. For example, you may follow other people to view their combined posts in your own personalized home timeline.</li>
+        <li>To aid moderation of the community, for example comparing your IP address with other known ones to determine ban evasion or other violations.</li>
+        <li>The email address you provide may be used to send you information, notifications about other people interacting with your content or sending you messages, and to respond to inquiries, and/or other requests or questions.</li>
+      </ul>
+
+      <hr class="spacer" />
+
+      <h3 id="protect">How do we protect your information?</h3>
+
+      <p>We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information. Among other things, your browser session, as well as the traffic between your applications and the API, are secured with SSL, and your password is hashed using a strong one-way algorithm. You may enable two-factor authentication to further secure access to your account.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="data-retention">What is our data retention policy?</h3>
+
+      <p>We will make a good faith effort to:</p>
+
+      <ul>
+        <li>Retain server logs containing the IP address of all requests to this server, in so far as such logs are kept, no more than 90 days.</li>
+        <li>Retain the IP addresses associated with registered users no more than 12 months.</li>
+      </ul>
+
+      <p>You can request and download an archive of your content, including your posts, media attachments, profile picture, and header image.</p>
+
+      <p>You may irreversibly delete your account at any time.</p>
+
+      <hr class="spacer"/>
+
+      <h3 id="cookies">Do we use cookies?</h3>
+
+      <p>Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account.</p>
+
+      <p>We use cookies to understand and save your preferences for future visits.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="disclose">Do we disclose any information to outside parties?</h3>
+
+      <p>We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety.</p>
+
+      <p>Your public content may be downloaded by other servers in the network. Your public and followers-only posts are delivered to the servers where your followers reside, and direct messages are delivered to the servers of the recipients, in so far as those followers or recipients reside on a different server than this.</p>
+
+      <p>When you authorize an application to use your account, depending on the scope of permissions you approve, it may access your public profile information, your following list, your followers, your lists, all your posts, and your favourites. Applications can never access your e-mail address or password.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="coppa">Children's Online Privacy Protection Act Compliance</h3>
+
+      <p>Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA (<a href="https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act">Children's Online Privacy Protection Act</a>) do not use this site.</p>
+
+      <hr class="spacer" />
+
+      <h3 id="changes">Changes to our Privacy Policy</h3>
+
+      <p>If we decide to change our privacy policy, we will post those changes on this page.</p>
+
+      <p>This document is CC-BY-SA. It was last updated March 7, 2018.</p>
+
+      <p>Originally adapted from the <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>.</p>
     title: "%{instance} Termos de Serviço e Política de Privacidade"
   time:
     formats:
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index 27d4e88e3..fb2a6cad1 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -243,7 +243,6 @@ pt:
       action_taken_by: Ação tomada por
       are_you_sure: Tens a certeza?
       comment:
-        label: Comentário
         none: Nenhum
       delete: Eliminar
       id: ID
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index 176ace92d..bf4225758 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -245,7 +245,6 @@ ru:
       action_taken_by: 'Действие предпринято:'
       are_you_sure: Вы уверены?
       comment:
-        label: Комментарий
         none: Нет
       delete: Удалить
       id: ID
diff --git a/config/locales/simple_form.ar.yml b/config/locales/simple_form.ar.yml
index 414f0c342..28cfa8ab7 100644
--- a/config/locales/simple_form.ar.yml
+++ b/config/locales/simple_form.ar.yml
@@ -5,8 +5,15 @@ ar:
       defaults:
         avatar: ملف PNG أو GIF أو JPG. حجمه على أقصى تصدير 2MB. سيتم تصغيره إلى 400x400px
         digest: تُرسَل إليك بعد مُضيّ مدة مِن خمول نشاطك و فقط إذا ما تلقيت رسائل شخصية مباشِرة أثناء فترة غيابك مِن الشبكة
+        display_name:
+          one: <span class="name-counter">1</span> حرف باقي
+          other: <span class="name-counter">%{count}</span> حروف متبقية
+        fields: يُمكنك عرض 4 عناصر على شكل جدول في ملفك الشخصي
         header: ملف PNG أو GIF أو JPG. حجمه على أقصى تصدير 2MB. سيتم تصغيره إلى 700x335px
         locked: يتطلب منك الموافقة يدويا على طلبات المتابعة
+        note:
+          one: <span class="note-counter">1</span> حرف متبقي
+          other: <span class="note-counter">%{count}</span> حروف متبقية
         setting_noindex: ذلك يؤثر على حالة ملفك الشخصي و صفحاتك
         setting_theme: ذلك يؤثر على الشكل الذي سيبدو عليه ماستدون عندما تقوم بالدخول مِن أي جهاز.
       imports:
@@ -16,6 +23,10 @@ ar:
       user:
         filtered_languages: سوف يتم تصفية و إخفاء اللغات المختارة من خيوطك العمومية
     labels:
+      account:
+        fields:
+          name: التسمية
+          value: المحتوى
       defaults:
         avatar: الصورة الرمزية
         confirm_new_password: تأكيد كلمة السر الجديدة
@@ -25,6 +36,7 @@ ar:
         display_name: الإسم المعروض
         email: عنوان البريد الإلكتروني
         expires_in: تنتهي مدة صلاحيته بعد
+        fields: واصفات بيانات الملف الشخصي
         filtered_languages: اللغات التي تم تصفيتها
         header: الرأسية
         locale: اللغة
diff --git a/config/locales/simple_form.ca.yml b/config/locales/simple_form.ca.yml
index 300da45a5..1b04da90a 100644
--- a/config/locales/simple_form.ca.yml
+++ b/config/locales/simple_form.ca.yml
@@ -8,6 +8,7 @@ ca:
         display_name:
           one: <span class="name-counter">1</span> càracter restant
           other: <span class="name-counter">%{count}</span> càracters restans
+        fields: Pots tenir fins a 4 elements que es mostren com a taula al teu perfil
         header: PNG, GIF o JPG. Màxim 2MB. S'escalarà a 700x335px
         locked: Requereix que aprovis manualment els seguidors
         note:
@@ -22,6 +23,10 @@ ca:
       user:
         filtered_languages: Les llengües seleccionades s'eliminaran de les línies de temps públiques
     labels:
+      account:
+        fields:
+          name: Etiqueta
+          value: Contingut
       defaults:
         avatar: Avatar
         confirm_new_password: Confirma la contrasenya nova
@@ -31,6 +36,7 @@ ca:
         display_name: Nom visible
         email: Adreça de correu electrònic
         expires_in: Expira després
+        fields: Metadades del perfil
         filtered_languages: Llengües filtrades
         header: Capçalera
         locale: Llengua
diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml
index 5a65173be..a9d650a26 100644
--- a/config/locales/simple_form.de.yml
+++ b/config/locales/simple_form.de.yml
@@ -8,6 +8,7 @@ de:
         display_name:
           one: <span class="name-counter">1</span> Zeichen verbleibt
           other: <span class="name-counter">%{count}</span> Zeichen verbleiben
+        fields: Du kannst bis zu 4 Elemente als Tabelle dargestellt auf deinem Profil anzeigen lassen
         header: PNG, GIF oder JPG. Maximal 2 MB. Wird auf 700×335 px herunterskaliert
         locked: Wer dir folgen möchte, muss um deine Erlaubnis bitten
         note:
@@ -22,6 +23,10 @@ de:
       user:
         filtered_languages: Ausgewählte Sprachen werden aus deinen öffentlichen Zeitleisten gefiltert
     labels:
+      account:
+        fields:
+          name: Bezeichnung
+          value: Inhalt
       defaults:
         avatar: Profilbild
         confirm_new_password: Neues Passwort bestätigen
@@ -31,6 +36,7 @@ de:
         display_name: Anzeigename
         email: E-Mail-Adresse
         expires_in: Gültig bis
+        fields: Profil-Metadaten
         filtered_languages: Gefilterte Sprachen
         header: Kopfbild
         locale: Sprache
diff --git a/config/locales/simple_form.eu.yml b/config/locales/simple_form.eu.yml
new file mode 100644
index 000000000..d856feac5
--- /dev/null
+++ b/config/locales/simple_form.eu.yml
@@ -0,0 +1,32 @@
+---
+eu:
+  simple_form:
+    hints:
+      defaults:
+        avatar: PNG, GIF edo JPG. Gehienez 2MB. 400x400px neurrira eskalatuko da
+        locked: Jarraitzaileak eskuz onartu behar dituzu
+        note:
+          other: <span class="note-counter"> %{count}</span> karaktere faltan
+        setting_noindex: Zure profil publiko eta egoera orrietan eragina du
+        setting_theme: Edozein gailutik konektatzean Mastodon-en itxuran eragiten du.
+      imports:
+        data: Mastodon-en beste instantzia batetik CSV fitxategia esportatu da
+      user:
+        filtered_languages: Aukeratutako hizkuntzak timeline publikotik filtratuko dira
+    labels:
+      account:
+        fields:
+          name: Etiketa
+          value: Edukia
+      defaults:
+        confirm_new_password: Pasahitz berria berretsi
+        confirm_password: Pasahitza berretsi
+        current_password: Oraingo pasahitza
+        display_name: Izena erakutsi
+        email: Helbide elektronikoa
+        fields: Profilaren metadatuak
+        filtered_languages: Iragazitako hizkuntzak
+        locale: Hizkuntza
+        new_password: Pasahitz berria
+        note: Bio
+        password: Pasahitza
diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml
index 71674199d..88e1b8873 100644
--- a/config/locales/simple_form.fr.yml
+++ b/config/locales/simple_form.fr.yml
@@ -8,6 +8,7 @@ fr:
         display_name:
           one: <span class="name-counter">1</span> caractère restant
           other: <span class="name-counter">%{count}</span> caractères restants
+        fields: Vous pouvez avoir jusqu'à 4 éléments affichés en tant que tableau sur votre profil
         header: Au format PNG, GIF ou JPG. 2 Mo maximum. Sera réduit à 700x335px
         locked: Vous devrez approuver chaque abonné⋅e et vos statuts ne s’afficheront qu’à vos abonné⋅es
         note:
@@ -22,6 +23,10 @@ fr:
       user:
         filtered_languages: Les langues sélectionnées seront filtrées hors de vos fils publics pour vous
     labels:
+      account:
+        fields:
+          name: Étiquette
+          value: Contenu
       defaults:
         avatar: Image de profil
         confirm_new_password: Confirmation du nouveau mot de passe
@@ -31,6 +36,7 @@ fr:
         display_name: Nom public
         email: Adresse courriel
         expires_in: Expire après
+        fields: Métadonnées du profil
         filtered_languages: Langues filtrées
         header: Image d’en-tête
         locale: Langue
diff --git a/config/locales/simple_form.gl.yml b/config/locales/simple_form.gl.yml
index 4dcdd0459..72633c759 100644
--- a/config/locales/simple_form.gl.yml
+++ b/config/locales/simple_form.gl.yml
@@ -8,6 +8,7 @@ gl:
         display_name:
           one: <span class="name-counter">1</span> caracter restante
           other: <span class="name-counter">%{count}</span> caracteres restantes
+        fields: Pode ter ate 4 elementos no seu perfil mostrados como unha táboa
         header: PNG, GIF ou JPG. Como moito 2MB. Será reducida a 700x335px
         locked: Require que vostede aprove as seguidoras de xeito manual
         note:
@@ -22,6 +23,10 @@ gl:
       user:
         filtered_languages: Os idiomas marcados filtraranse das liñas temporais públicas para vostede
     labels:
+      account:
+        fields:
+          name: Etiqueta
+          value: Contido
       defaults:
         avatar: Avatar
         confirm_new_password: Confirme o novo contrasinal
@@ -31,8 +36,9 @@ gl:
         display_name: Nome mostrado
         email: enderezo correo electrónico
         expires_in: Caducidade despois de
+        fields: Metadatos do perfil
         filtered_languages: Idiomas filtrados
-        header: Cabezallo
+        header: Cabeceira
         locale: Idioma
         locked: Protexer conta
         max_uses: Número máximo de usos
diff --git a/config/locales/simple_form.it.yml b/config/locales/simple_form.it.yml
index b2fcef109..5d9ae18f5 100644
--- a/config/locales/simple_form.it.yml
+++ b/config/locales/simple_form.it.yml
@@ -3,45 +3,77 @@ it:
   simple_form:
     hints:
       defaults:
-        avatar: PNG, GIF o JPG. Al massimo 2MB. Sarà ridotto a 400x400px
-        display_name: Al massimo 30 characters
-        header: PNG, GIF or JPG. Al massimo 2MB. Sarà ridotto a 700x335px
-        locked: Richiede la tua approvazione per i nuovi seguaci e rende i nuovi post automaticamente visibili solo ai seguaci
-        note: Al massimo 160 caratteri
+        avatar: PNG, GIF o JPG. Al massimo 2MB. Verranno scalate a 400x400px
+        digest: Inviata solo dopo un lungo periodo di intattività e solo se hai ricevuto qualsiasi messaggio personale in tua assenza
+        display_name:
+          one: <span class="name-counter">1</span> carattere rimanente
+          other: <span class="name-counter">%{count}</span> caratteri rimanenti
+        fields: Puoi avere fino a 4 voci visualizzate come una tabella sul tuo profilo
+        header: PNG, GIF o JPG. Al massimo 2MB. Verranno scalate a 700x335px
+        locked: Richiede che approvi i follower manualmente
+        note:
+          one: <span class="note-counter">1</span> carattere rimanente
+          other: <span class="note-counter">%{count}</span> caratteri rimanenti
+        setting_noindex: Coinvolge il tuo profilo pubblico e le pagine di stato
+        setting_theme: Coinvolge il modo in cui Mastodon verrà visualizzato quando sarai collegato da qualsiasi dispositivo.
       imports:
-        data: CSV esportato da un altro server Mastodon
+        data: File CSV esportato da un altra istanza di Mastodon
+      sessions:
+        otp: Inserisci il codice due-fattori dal tuo telefono o usa uno dei codici di recupero.
+      user:
+        filtered_languages: Le lingue selezionate verranno filtrate dalla timeline pubblica per te
     labels:
+      account:
+        fields:
+          name: Etichetta
+          value: Contenuto
       defaults:
         avatar: Avatar
-        confirm_new_password: Conferma la nuova password
-        confirm_password: Conferma la password
+        confirm_new_password: Conferma nuova password
+        confirm_password: Conferma password
         current_password: Password corrente
         data: Data
-        display_name: Nome pubblico
-        email: Indirizzo e-mail
+        display_name: Nome visualizzato
+        email: Indirizzo email
+        expires_in: Scade dopo
+        fields: Metadata profilo
+        filtered_languages: Lingue filtrate
         header: Header
         locale: Lingua
-        locked: Rendi l'account privato
+        locked: Blocca account
+        max_uses: Numero massimo di utilizzi
         new_password: Nuova password
-        note: Biografia
-        otp_attempt: Codice d'accesso
+        note: Bio
+        otp_attempt: Codice due-fattori
         password: Password
-        setting_boost_modal: Mostra finestra di conferma prima di condividere
-        setting_default_privacy: Privacy del post
-        type: Importa
-        username: Username
+        setting_auto_play_gif: Play automatico GIF animate
+        setting_boost_modal: Mostra dialogo di conferma prima del boost
+        setting_default_privacy: Privacy post
+        setting_default_sensitive: Segna sempre i media come sensibili
+        setting_delete_modal: Mostra dialogo di conferma prima di eliminare un toot
+        setting_display_sensitive_media: Mostra sempre i media segnati come sensibili
+        setting_noindex: Non indicizzare dai motori di ricerca
+        setting_reduce_motion: Riduci movimento nelle animazioni
+        setting_system_font_ui: Usa il carattere di default del sistema
+        setting_theme: Tema sito
+        setting_unfollow_modal: Mostra dialogo di conferma prima di smettere di seguire qualcuno
+        severity: Severità
+        type: Tipo importazione
+        username: Nome utente
+        username_or_email: Nome utente o email
       interactions:
-        must_be_follower: Blocca notifiche da chi non ti segue
-        must_be_following: Blocca notifiche da chi non segui
+        must_be_follower: Blocca notifiche dai non follower
+        must_be_following: Blocca notifiche dalle persone che non segui
+        must_be_following_dm: Blocca i messaggi diretti dalle persone che non segui
       notification_emails:
-        digest: Invia riassunto via e-mail
-        favourite: Invia e-mail quando qualcuno apprezza i tuoi status
-        follow: Invia e-mail quando qualcuno ti segue
-        follow_request: Invia e-mail quando qualcuno ti richiede di seguirti
-        mention: Invia e-mail quando qualcuno ti menziona
-        reblog: Invia e-mail quando qualcuno condivide i tuoi status
+        digest: Invia email riassuntive
+        favourite: Invia email quando segna come preferito al tuo stato
+        follow: Invia email quando qualcuno ti segue
+        follow_request: Invia email quando qualcuno richiede di seguirti
+        mention: Invia email quando qualcuno ti menziona
+        reblog: Invia email quando qualcuno da un boost al tuo stato
     'no': 'No'
     required:
       mark: "*"
       text: richiesto
-    'yes': Sì
+    'yes': Si
diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml
index d11430338..9e4d40405 100644
--- a/config/locales/simple_form.ja.yml
+++ b/config/locales/simple_form.ja.yml
@@ -6,6 +6,7 @@ ja:
         avatar: 2MBまでのPNGやGIF、JPGが利用可能です。400x400pxまで縮小されます
         digest: 長期間使用していない場合と不在時に返信を受けた場合のみ送信されます
         display_name: あと<span class="name-counter">%{count}</span>文字入力できます。
+        fields: プロフィールに表として4つまでの項目を表示することができます
         header: 2MBまでのPNGやGIF、JPGが利用可能です。 700x335pxまで縮小されます
         locked: フォロワーを手動で承認する必要があります
         note: あと<span class="note-counter">%{count}</span>文字入力できます。
@@ -18,6 +19,10 @@ ja:
       user:
         filtered_languages: 選択した言語があなたの公開タイムラインから取り除かれます
     labels:
+      account:
+        fields:
+          name: ラベル
+          value: 内容
       defaults:
         avatar: アイコン
         confirm_new_password: 新しいパスワード(確認用)
@@ -27,6 +32,7 @@ ja:
         display_name: 表示名
         email: メールアドレス
         expires_in: 有効期限
+        fields: プロフィール補足情報
         filtered_languages: 除外する言語
         header: ヘッダー
         locale: 言語
@@ -47,7 +53,7 @@ ja:
         setting_reduce_motion: アニメーションの動きを減らす
         setting_system_font_ui: システムのデフォルトフォントを使う
         setting_theme: サイトテーマ
-        setting_unfollow_modal: フォロー解除する前に確認ダイアログを表示する
+        setting_unfollow_modal: フォローを解除する前に確認ダイアログを表示する
         severity: 重大性
         type: インポートする項目
         username: ユーザー名
diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml
index 85eccf091..ccb05fd25 100644
--- a/config/locales/simple_form.ko.yml
+++ b/config/locales/simple_form.ko.yml
@@ -8,6 +8,7 @@ ko:
         display_name:
           one: <span class="name-counter">1</span> 글자 남음
           other: <span class="name-counter">%{count}</span> 글자 남음
+        fields: 당신의 프로파일에 최대 4개까지 표 형식으로 나타낼 수 있습니다
         header: PNG, GIF 혹은 JPG. 최대 2MB. 700x335px로 다운스케일 됨
         locked: 수동으로 팔로워를 승인하고, 기본 툿 프라이버시 설정을 팔로워 전용으로 변경
         note:
@@ -22,6 +23,10 @@ ko:
       user:
         filtered_languages: 선택된 언어가 공개 타임라인에서 제외 될 것입니다
     labels:
+      account:
+        fields:
+          name: 라벨
+          value: 내용
       defaults:
         avatar: 아바타
         confirm_new_password: 새로운 비밀번호 다시 입력
@@ -31,6 +36,7 @@ ko:
         display_name: 표시되는 이름
         email: 이메일 주소
         expires_in: 만료시각
+        fields: 프로필 메타데이터
         filtered_languages: 숨긴 언어들
         header: 헤더
         locale: 언어
diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml
index 9876230b3..ec42adfd7 100644
--- a/config/locales/simple_form.nl.yml
+++ b/config/locales/simple_form.nl.yml
@@ -8,6 +8,7 @@ nl:
         display_name:
           one: <span class="name-counter">1</span> teken over
           other: <span class="name-counter">%{count}</span> tekens over
+        fields: Je kan maximaal 4 items als een tabel op je profiel weergeven
         header: PNG, GIF of JPG. Maximaal 2MB. Wordt teruggeschaald naar 700x335px
         locked: Vereist dat je handmatig volgers moet accepteren
         note:
@@ -22,6 +23,10 @@ nl:
       user:
         filtered_languages: De geselecteerde talen worden uit de lokale en globale tijdlijn verwijderd
     labels:
+      account:
+        fields:
+          name: Label
+          value: Inhoud
       defaults:
         avatar: Avatar
         confirm_new_password: Nieuw wachtwoord bevestigen
@@ -31,6 +36,7 @@ nl:
         display_name: Weergavenaam
         email: E-mailadres
         expires_in: Vervalt na
+        fields: Metadata profiel
         filtered_languages: Talen filteren
         header: Omslagfoto
         locale: Taal
@@ -63,7 +69,7 @@ nl:
         digest: Periodiek e-mails met een samenvatting versturen
         favourite: Een e-mail versturen wanneer iemand jouw toot als favoriet markeert
         follow: Een e-mail versturen wanneer iemand jou volgt
-        follow_request: Een e-mail versturen wanneer iemand jou wilt volgen
+        follow_request: Een e-mail versturen wanneer iemand jou wil volgen
         mention: Een e-mail versturen wanneer iemand jou vermeld
         reblog: Een e-mail versturen wanneer iemand jouw toot heeft geboost
     'no': Nee
diff --git a/config/locales/simple_form.oc.yml b/config/locales/simple_form.oc.yml
index 690d1de20..4ca58c102 100644
--- a/config/locales/simple_form.oc.yml
+++ b/config/locales/simple_form.oc.yml
@@ -8,6 +8,7 @@ oc:
         display_name:
           one: Demòra encara <span class="name-counter">1</span> caractèr
           other: Demòran encara <span class="name-counter">%{count}</span> caractèrs
+        fields: Podètz far veire cap a 4 elements sus vòstre perfil
         header: PNG, GIF o JPG. Maximum 2 Mo. Serà retalhada en 700x335px
         locked: Demanda qu’acceptetz manualament lo mond que vos sègon e botarà la visibilitat de vòstras publicacions coma accessiblas a vòstres seguidors solament
         note:
@@ -22,6 +23,10 @@ oc:
       user:
         filtered_languages: Las lengas seleccionadas seràn levadas de vòstre flux d’actualitat
     labels:
+      account:
+        fields:
+          name: Nom
+          value: Contengut
       defaults:
         avatar: Avatar
         confirm_new_password: Confirmacion del nòu senhal
@@ -31,6 +36,7 @@ oc:
         display_name: Escais
         email: Corrièl
         expires_in: Expira aprèp
+        fields: Metadonada del perfil
         filtered_languages: Lengas filtradas
         header: Bandièra
         locale: Lenga
diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml
index 325cb2691..8a6d47a01 100644
--- a/config/locales/simple_form.pl.yml
+++ b/config/locales/simple_form.pl.yml
@@ -63,7 +63,7 @@ pl:
         setting_system_font_ui: Używaj domyślnej czcionki systemu
         setting_unfollow_modal: Pytaj o potwierdzenie przed cofnięciem śledzenia
         severity: Priorytet
-        type: Typ importu
+        type: Importowane dane
         username: Nazwa użytkownika
         username_or_email: Nazwa użytkownika lub adres e-mail
       interactions:
diff --git a/config/locales/simple_form.pt-BR.yml b/config/locales/simple_form.pt-BR.yml
index 0c22b2608..cae1f671d 100644
--- a/config/locales/simple_form.pt-BR.yml
+++ b/config/locales/simple_form.pt-BR.yml
@@ -8,6 +8,7 @@ pt-BR:
         display_name:
           one: <span class="name-counter">1</span> caracter restante
           other: <span class="name-counter">%{count}</span> caracteres restantes
+        fields: Você pode ter até 4 itens exibidos em forma de tabela no seu perfil
         header: PNG, GIF or JPG. Arquivos de até 2MB. Eles serão diminuídos para 700x335px
         locked: Requer aprovação manual de seguidores
         note:
@@ -22,6 +23,10 @@ pt-BR:
       user:
         filtered_languages: Selecione os idiomas que devem ser removidos de suas timelines públicas
     labels:
+      account:
+        fields:
+          name: Rótulo
+          value: Conteúdo
       defaults:
         avatar: Avatar
         confirm_new_password: Confirmar nova senha
@@ -31,6 +36,7 @@ pt-BR:
         display_name: Nome de exibição
         email: Endereço de e-mail
         expires_in: Expira em
+        fields: Metadados do perfil
         filtered_languages: Idiomas filtrados
         header: Cabeçalho
         locale: Idioma
diff --git a/config/locales/simple_form.sk.yml b/config/locales/simple_form.sk.yml
index e504c9774..134e62ee3 100644
--- a/config/locales/simple_form.sk.yml
+++ b/config/locales/simple_form.sk.yml
@@ -8,6 +8,7 @@ sk:
         display_name:
           one: Ostáva ti <span class="name-counter">1</span> znak
           other: Ostáva ti <span class="name-counter">%{count}</span> znakov
+        fields: Môžeš mať 4 položky na svojom profile zobrazené vo forme tabuľky
         header: PNG, GIF alebo JPG. Maximálne 2MB. Bude zmenšený na 700x335px
         locked: Musíte manuálne schváliť sledujúcich
         note:
@@ -22,15 +23,20 @@ sk:
       user:
         filtered_languages: Zaškrtnuté jazyky budú pre teba vynechané nebudú z verejnej časovej osi
     labels:
+      account:
+        fields:
+          name: Označenie
+          value: Obsah
       defaults:
         avatar: Avatar
-        confirm_new_password: Opäť vaše nové heslo pre potvrdenie
-        confirm_password: Potvrďte heslo
+        confirm_new_password: Znovu tvoje nové heslo, pre potvrdenie
+        confirm_password: Potvrď heslo
         current_password: Súčasné heslo
         data: Dáta
         display_name: Meno
         email: Emailová adresa
         expires_in: Expirovať po
+        fields: Metadáta profilu
         filtered_languages: Filtrované jazyky
         header: Obrázok v hlavičke
         locale: Jazyk
@@ -43,9 +49,9 @@ sk:
         setting_auto_play_gif: Automaticky prehrávať animované GIFy
         setting_boost_modal: Zobrazovať potvrdzovacie okno pred re-toot
         setting_default_privacy: Nastavenie súkromia príspevkov
-        setting_default_sensitive: Označiť každý obrázok/video/súbor ako chúlostivý
-        setting_delete_modal: Zobrazovať potvrdzovacie okno pred zmazaním toot-u
-        setting_display_sensitive_media: Vždy zobrazovať médiá označované ako senzitívne
+        setting_default_sensitive: Označ všetky mediálne súbory ako chúlostivé
+        setting_delete_modal: Zobrazuj potvrdzovacie okno pred vymazaním toot-u
+        setting_display_sensitive_media: Vždy zobraz médiá označené ako chúlostivé
         setting_noindex: Nezaraďuj príspevky do indexu pre vyhľadávče
         setting_reduce_motion: Redukovať pohyb v animáciách
         setting_system_font_ui: Použiť základné systémové písmo
diff --git a/config/locales/simple_form.sv.yml b/config/locales/simple_form.sv.yml
index 52ff32753..81ba61fb3 100644
--- a/config/locales/simple_form.sv.yml
+++ b/config/locales/simple_form.sv.yml
@@ -8,6 +8,7 @@ sv:
         display_name:
           one: <span class="name-counter">1</span> tecken kvar
           other: <span class="name-counter">%{count}</span> tecken kvar
+        fields: Du kan ha upp till 4 objekt visade som en tabell på din profil
         header: NG, GIF eller JPG. Högst 2 MB. Kommer nedskalas till 700x335px
         locked: Kräver att du manuellt godkänner följare
         note:
@@ -22,6 +23,10 @@ sv:
       user:
         filtered_languages: Kontrollerade språk filtreras från offentliga tidslinjer för dig
     labels:
+      account:
+        fields:
+          name: Etikett
+          value: Innehåll
       defaults:
         avatar: Avatar
         confirm_new_password: Bekräfta nytt lösenord
@@ -31,6 +36,7 @@ sv:
         display_name: Visningsnamn
         email: E-postadress
         expires_in: Förfaller efter
+        fields: Profil-metadata
         filtered_languages: Filtrerade språk
         header: Bakgrundsbild
         locale: Språk
diff --git a/config/locales/simple_form.zh-HK.yml b/config/locales/simple_form.zh-HK.yml
index 6b890b036..a21439a98 100644
--- a/config/locales/simple_form.zh-HK.yml
+++ b/config/locales/simple_form.zh-HK.yml
@@ -5,19 +5,23 @@ zh-HK:
       defaults:
         avatar: 支援 PNG, GIF 或 JPG 圖片,檔案最大為 2MB,會縮裁成 400x400px
         digest: 僅在你長時間未登錄,且收到了私信時發送
-        display_name: 最多 30 個字元
+        display_name:
+          one: 尚餘 <span class="name-counter">1</span> 個字
+          other: 尚餘 <span class="name-counter">%{count}</span> 個字
         fields: 個人資料頁可顯示多至 4 個項目
         header: 支援 PNG, GIF 或 JPG 圖片,檔案最大為 2MB,會縮裁成 700x335px
         locked: 你必須人手核准每個用戶對你的關注請求,而你的文章私隱會被預設為「只有關注你的人能看」
-        note: 最多 160 個字元
+        note:
+          one: 尚餘 <span class="note-counter">1</span> 個字
+          other: 尚餘 <span class="note-counter">%{count}</span> 個字
         setting_noindex: 此設定會影響到你的公開個人資料以及文章頁面
-        setting_theme: 此設置會影響到你從任意設備登入時 Mastodon 的顯示樣式
+        setting_theme: 此設置會影響到你從任意設備登入時 Mastodon 的顯示樣式。
       imports:
         data: 自其他服務站匯出的 CSV 檔案
       sessions:
         otp: 輸入你手機上生成的雙重認證碼,或者任意一個恢復代碼。
       user:
-        filtered_languages: 下面被選擇的語言的文章將不會出現在你的公共時間軸上。
+        filtered_languages: 下面被選擇的語言的文章將不會出現在你的公共時間軸上
     labels:
       account:
         fields:
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index 25e672604..fc9e9452c 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -4,6 +4,7 @@ sk:
     about_hashtag_html: Toto sú verejné toot príspevky otagované <strong>#%{tagom}</strong>. Ak máš účet niekde vo fediverse, môžeš ich používať.
     about_mastodon_html: Mastodon je sociálna sieť založená na otvorených webových protokoloch. Jej zrojový kód je otvorený a je decentralizovaná podobne ako email.
     about_this: O tejto instancii
+    administered_by: 'Správca je:'
     closed_registrations: Registrácie sú momentálne uzatvorené. Avšak, môžeš nájsť nejaký iný Mastodon server kde si založ účet a získaj tak prístup do presne tej istej siete, odtiaľ.
     contact: Kontakt
     contact_missing: Nezadané
@@ -60,7 +61,15 @@ sk:
       destroyed_msg: Poznámka moderátora bola úspešne zmazaná!
     accounts:
       are_you_sure: Ste si istý?
+      avatar: Maskot
       by_domain: Doména
+      change_email:
+        changed_msg: Email k tomuto účtu bol úspešne zmenený!
+        current_email: Súčastný email
+        label: Zmeniť email
+        new_email: Nový email
+        submit: Zmeniť email
+        title: Zmeň email pre %{username}
       confirm: Potvrdiť
       confirmed: Potvrdený
       demote: Degradovať
@@ -108,6 +117,7 @@ sk:
       public: Verejná os
       push_subscription_expires: PuSH odoberanie expiruje
       redownload: Obnoviť avatar
+      remove_avatar: Odstrániť avatár
       reset: Reset
       reset_password: Obnoviť heslo
       resubscribe: Znovu odoberať
@@ -128,6 +138,7 @@ sk:
       statuses: Príspevky
       subscribe: Odoberať
       title: Účty
+      unconfirmed_email: Nepotvrdený email
       undo_silenced: Zrušiť stíšenie
       undo_suspension: Zrušiť suspendáciu
       unsubscribe: Prestať odoberať
@@ -135,6 +146,8 @@ sk:
       web: Web
     action_logs:
       actions:
+        assigned_to_self_report: "%{name}pridelil/a hlásenie užívateľa %{target}sebe"
+        change_email_user: "%{name} zmenil/a emailovú adresu užívateľa %{target}"
         confirm_user: "%{name} potvrdil e-mailovú adresu používateľa %{target}"
         create_custom_emoji: "%{name} nahral nový emoji %{target}"
         create_domain_block: "%{name} zablokoval doménu %{target}"
@@ -150,8 +163,10 @@ sk:
         enable_user: "%{name} povolil prihlásenie pre používateľa %{target}"
         memorialize_account: '%{name} zmenil účet %{target} na stránku "Navždy budeme spomínať"'
         promote_user: "%{name} povýšil/a používateľa %{target}"
+        remove_avatar_user: "%{name} odstránil/a %{target}ov avatár"
+        reopen_report: "%{name} znovu otvoril/a hlásenie užívateľa %{target}"
         reset_password_user: "%{name} resetoval/a heslo pre používateľa %{target}"
-        resolve_report: "%{name} zamietli nahlásenie %{target}"
+        resolve_report: "%{name} vyriešili nahlásenie užívateľa %{target}"
         silence_account: "%{name} utíšil/a účet %{target}"
         suspend_account: "%{name} zablokoval/a účet používateľa %{target}"
         unsilence_account: "%{name} zrušil/a utíšenie účtu používateľa %{target}"
@@ -239,29 +254,48 @@ sk:
         expired: Expirované
         title: Filtrovať
       title: Pozvánky
+    report_notes:
+      created_msg: Poznámka o nahlásení úspešne vytvorená!
+      destroyed_msg: Poznámka o nahlásení úspešne vymazaná!
     reports:
-      action_taken_by: Zákrok vykonal
+      account:
+        note: poznámka
+        report: nahlás
+      action_taken_by: Zákrok vykonal/a
       are_you_sure: Ste si istý/á?
+      assign_to_self: Priraď sebe
+      assigned: Priradený moderátor
       comment:
-        label: Vyjadriť sa
         none: Žiadne
+      created_at: Nahlásené
       delete: Vymazať
       id: Identifikácia
       mark_as_resolved: Označiť ako vyriešené
+      mark_as_unresolved: Označ ako nevyriešené
+      notes:
+        create: Pridaj poznámku
+        create_and_resolve: Vyrieš s poznámkou
+        create_and_unresolve: Otvor znovu, s poznámkou
+        delete: Vymaž
+        placeholder: Opíš aké opatrenia boli urobené, alebo akékoľvek iné aktualizácie k tomuto nahláseniu…
       nsfw:
         'false': Odkryť mediálne prílohy
         'true': Skryť mediálne prílohy
+      reopen: Znovu otvor report
       report: Nahlásiť
       report_contents: Obsah
       reported_account: Nahlásený účet
       reported_by: Nahlásené užívateľom
       resolved: Vyriešené
+      resolved_msg: Hlásenie úspešne vyriešené!
       silence_account: Zamĺčať účet
       status: Stav
       suspend_account: Pozastaviť účet
       target: Cieľ
       title: Reporty
+      unassign: Odobrať
       unresolved: Nevyriešené
+      updated_at: Aktualizované
       view: Zobraziť
     settings:
       activity_api_enabled:
@@ -319,8 +353,8 @@ sk:
       back_to_account: Späť na účet
       batch:
         delete: Vymazať
-        nsfw_off: Nevhodný obsah je vypnutý
-        nsfw_on: Nevhodný obsah je zapnutý
+        nsfw_off: Obsah nieje chúlostivý
+        nsfw_on: Označ obeah aka chúlostivý
       execute: Vykonať
       failed_to_execute: Nepodarilo sa vykonať
       media:
@@ -382,6 +416,7 @@ sk:
     security: Zabezpečenie
     set_new_password: Nastaviť nové heslo
   authorize_follow:
+    already_following: Tento účet už následuješ
     error: Naneštastie nastala chyba pri hľadaní vzdialeného účtu
     follow: Následovať
     follow_request: 'Poslali ste požiadavku následovať užívateľa:'
@@ -473,6 +508,7 @@ sk:
       '21600': 6 hodín
       '3600': 1 hodina
       '43200': 12 hodín
+      '604800': 1 týždeň
       '86400': 1 deň
     expires_in_prompt: Nikdy
     generate: Vygeneruj
@@ -575,6 +611,10 @@ sk:
     missing_resource: Nemôžeme nájsť potrebnú presmerovaciu adresu k tvojmu účtu
     proceed: Začni následovať
     prompt: 'Budeš sledovať:'
+  remote_unfollow:
+    error: Chyba
+    title: Názov
+    unfollowed: Už nesleduješ
   sessions:
     activity: Najnovšia aktivita
     browser: Prehliadač
@@ -644,6 +684,7 @@ sk:
   terms:
     title: Podmienky užívania, a pravidlá o súkromí pre %{instance}
   themes:
+    contrast: Vysoký kontrast
     default: Mastodon
   time:
     formats:
diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml
index 8d39d35b0..742c976d1 100644
--- a/config/locales/sr-Latn.yml
+++ b/config/locales/sr-Latn.yml
@@ -245,7 +245,6 @@ sr-Latn:
       action_taken_by: Akciju izveo
       are_you_sure: Da li ste sigurni?
       comment:
-        label: Komentar
         none: Ništa
       delete: Obriši
       id: ID
diff --git a/config/locales/sr.yml b/config/locales/sr.yml
index af4c6a846..0d55910a6 100644
--- a/config/locales/sr.yml
+++ b/config/locales/sr.yml
@@ -245,7 +245,6 @@ sr:
       action_taken_by: Акцију извео
       are_you_sure: Да ли сте сигурни?
       comment:
-        label: Коментар
         none: Ништа
       delete: Обриши
       id: ID
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index f85ed6efb..845248652 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -4,12 +4,13 @@ sv:
     about_hashtag_html: Dessa är offentliga toots märkta med <strong>#%{hashtag}</strong>. Du kan interagera med dem om du har ett konto någonstans i federationen.
     about_mastodon_html: Mastodon är ett socialt nätverk baserat på öppna webbprotokoll och gratis, öppen källkodsprogramvara. Det är decentraliserat som e-post.
     about_this: Om
+    administered_by: 'Administreras av:'
     closed_registrations: Registreringar är för närvarande stängda i denna instans. Dock så kan du hitta en annan instans för att skapa ett konto och få tillgång till samma nätverk från det.
     contact: Kontakt
     contact_missing: Inte inställd
     contact_unavailable: N/A
     description_headline: Vad är %{domain}?
-    domain_count_after: annan instans
+    domain_count_after: andra instanser
     domain_count_before: Uppkopplad mot
     extended_description_html: |
       <h3>En bra plats för regler</h3>
@@ -29,7 +30,7 @@ sv:
     other_instances: Instanslista
     source_code: Källkod
     status_count_after: statusar
-    status_count_before: Vem författade
+    status_count_before: Som skapat
     user_count_after: användare
     user_count_before: Hem till
     what_is_mastodon: Vad är Mastodon?
@@ -48,7 +49,7 @@ sv:
     reserved_username: Användarnamnet är reserverat
     roles:
       admin: Admin
-      moderator: Moderera
+      moderator: Moderator
     unfollow: Sluta följa
   admin:
     account_moderation_notes:
@@ -60,7 +61,15 @@ sv:
       destroyed_msg: Modereringsnotering borttagen utan problem!
     accounts:
       are_you_sure: Är du säker?
+      avatar: Avatar
       by_domain: Domän
+      change_email:
+        changed_msg: E-postadressen har ändrats!
+        current_email: Nuvarande E-postadress
+        label: Byt E-postadress
+        new_email: Ny E-postadress
+        submit: Byt E-postadress
+        title: Byt E-postadress för %{username}
       confirm: Bekräfta
       confirmed: Bekräftad
       demote: Degradera
@@ -108,6 +117,7 @@ sv:
       public: Offentlig
       push_subscription_expires: PuSH-prenumerationen löper ut
       redownload: Uppdatera avatar
+      remove_avatar: Ta bort avatar
       reset: Återställ
       reset_password: Återställ lösenord
       resubscribe: Starta en ny prenumeration
@@ -128,6 +138,7 @@ sv:
       statuses: Status
       subscribe: Prenumerera
       title: Konton
+      unconfirmed_email: Obekräftad E-postadress
       undo_silenced: Ångra tystnad
       undo_suspension: Ångra avstängning
       unsubscribe: Avsluta prenumeration
@@ -135,6 +146,8 @@ sv:
       web: Webb
     action_logs:
       actions:
+        assigned_to_self_report: "%{name} tilldelade anmälan %{target} till sig själv"
+        change_email_user: "%{name} bytte e-postadress för användare %{target}"
         confirm_user: "%{name} bekräftade e-postadress för användare %{target}"
         create_custom_emoji: "%{name} laddade upp ny emoji %{target}"
         create_domain_block: "%{name} blockerade domän %{target}"
@@ -150,10 +163,13 @@ sv:
         enable_user: "%{name} aktiverade inloggning för användare %{target}"
         memorialize_account: "%{name} omvandlade %{target}s konto till en memoriam-sida"
         promote_user: "%{name} flyttade upp användare %{target}"
+        remove_avatar_user: "%{name} tog bort %{target}s avatar"
+        reopen_report: "%{name} återupptog anmälan %{target}"
         reset_password_user: "%{name} återställde lösenord för användaren %{target}"
-        resolve_report: "%{name} avvisade anmälan %{target}"
+        resolve_report: "%{name} löste anmälan %{target}"
         silence_account: "%{name} tystade ner %{target}s konto"
         suspend_account: "%{name} suspenderade %{target}s konto"
+        unassigned_report: "%{name} otilldelade anmälan %{target}"
         unsilence_account: "%{name} återljudade %{target}s konto"
         unsuspend_account: "%{name} aktiverade %{target}s konto"
         update_custom_emoji: "%{name} uppdaterade emoji %{target}"
@@ -239,29 +255,48 @@ sv:
         expired: Utgångna
         title: Filtrera
       title: Inbjudningar
+    report_notes:
+      created_msg: Anmälningsanteckning har skapats!
+      destroyed_msg: Anmälningsanteckning har raderats!
     reports:
+      account:
+        note: anteckning
+        report: anmälan
       action_taken_by: Åtgärder vidtagna av
       are_you_sure: Är du säker?
+      assign_to_self: Tilldela till mig
+      assigned: Tilldelad moderator
       comment:
-        label: Kommentar
         none: Ingen
+      created_at: Anmäld
       delete: Radera
       id: ID
       mark_as_resolved: Markera som löst
+      mark_as_unresolved: Markera som olöst
+      notes:
+        create: Lägg till anteckning
+        create_and_resolve: Lös med anteckning
+        create_and_unresolve: Återuppta med anteckning
+        delete: Radera
+        placeholder: Beskriv vilka åtgärder som vidtagits eller andra uppdateringar till den här anmälan…
       nsfw:
         'false': Visa bifogade mediafiler
         'true': Dölj bifogade mediafiler
+      reopen: Återuppta anmälan
       report: 'Anmäl #%{id}'
       report_contents: Innehåll
       reported_account: Anmält konto
       reported_by: Anmäld av
       resolved: Löst
+      resolved_msg: Anmälan har lösts framgångsrikt!
       silence_account: Tysta ner konto
       status: Status
       suspend_account: Suspendera konto
       target: Mål
       title: Anmälningar
+      unassign: Otilldela
       unresolved: Olösta
+      updated_at: Uppdaterad
       view: Granska
     settings:
       activity_api_enabled:
@@ -319,8 +354,8 @@ sv:
       back_to_account: Tillbaka till kontosidan
       batch:
         delete: Radera
-        nsfw_off: NSFW AV
-        nsfw_on: NSFW PÅ
+        nsfw_off: Markera som ej känslig
+        nsfw_on: Markera som känslig
       execute: Utför
       failed_to_execute: Misslyckades att utföra
       media:
@@ -382,6 +417,7 @@ sv:
     security: Säkerhet
     set_new_password: Skriv in nytt lösenord
   authorize_follow:
+    already_following: Du följer redan detta konto
     error: Tyvärr inträffade ett fel när vi kontrollerade fjärrkontot
     follow: Följ
     follow_request: 'Du har skickat en följaförfrågan till:'
@@ -474,6 +510,7 @@ sv:
       '21600': 6 timmar
       '3600': 1 timma
       '43200': 12 timmar
+      '604800': 1 vecka
       '86400': 1 dag
     expires_in_prompt: Aldrig
     generate: Skapa
@@ -546,7 +583,7 @@ sv:
           quadrillion: Q
           thousand: K
           trillion: T
-          unit: enhet
+          unit: ''
   pagination:
     newer: Nyare
     next: Nästa
@@ -577,6 +614,10 @@ sv:
     missing_resource: Det gick inte att hitta den begärda omdirigeringsadressen för ditt konto
     proceed: Fortsätt för att följa
     prompt: 'Du kommer att följa:'
+  remote_unfollow:
+    error: Fel
+    title: Titel
+    unfollowed: Slutade följa
   sessions:
     activity: Senaste aktivitet
     browser: Webbläsare
@@ -634,6 +675,18 @@ sv:
     two_factor_authentication: Tvåstegsautentisering
     your_apps: Dina applikationer
   statuses:
+    attached:
+      description: 'Bifogad: %{attached}'
+      image:
+        one: "%{count} bild"
+        other: "%{count} bilder"
+      video:
+        one: "%{count} video"
+        other: "%{count} videor"
+    content_warning: 'Innehållsvarning: %{warning}'
+    disallowed_hashtags:
+      one: 'innehöll en otillåten hashtag: %{tags}'
+      other: 'innehöll de otillåtna hashtagarna: %{tags}'
     open_in_web: Öppna på webben
     over_character_limit: teckengräns på %{max} har överskridits
     pin_errors:
diff --git a/config/locales/te.yml b/config/locales/te.yml
new file mode 100644
index 000000000..f28b56052
--- /dev/null
+++ b/config/locales/te.yml
@@ -0,0 +1,5 @@
+---
+te:
+  about:
+    about_this: గురించి
+    contact: సంప్రదించండి
diff --git a/config/locales/th.yml b/config/locales/th.yml
index 45fe1e475..350b93b52 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -108,7 +108,6 @@ th:
       title: Known Instances
     reports:
       comment:
-        label: คอมเม้นต์
         none: None
       delete: ลบ
       id: ไอดี
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index ee0e33074..6e7aeb77e 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -107,7 +107,6 @@ tr:
       title: Bilinen Sunucular
     reports:
       comment:
-        label: Yorum
         none: Yok
       delete: Sil
       id: ID
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 4c1c66b31..44f64b5c9 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -99,7 +99,6 @@ uk:
       undo: Відмінити
     reports:
       comment:
-        label: Коментар
         none: Немає
       delete: Видалити
       id: ID
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index be868e6e7..78c72bd30 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -241,7 +241,6 @@ zh-CN:
       action_taken_by: 操作执行者
       are_you_sure: 你确定吗?
       comment:
-        label: 备注
         none: 没有
       delete: 删除
       id: ID
diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml
index 964ff5811..a27b0c04c 100644
--- a/config/locales/zh-HK.yml
+++ b/config/locales/zh-HK.yml
@@ -259,25 +259,16 @@ zh-HK:
       destroyed_msg: 舉報筆記已刪除。
     reports:
       account:
-        created_reports: 由此帳號發出的舉報
-        moderation:
-          silenced: 被靜音的
-          suspended: 被停權的
-          title: 管理操作
-        moderation_notes: 管理筆記
         note: 筆記
         report: 舉報
-        targeted_reports: 關於此帳號的舉報
       action_taken_by: 操作執行者
       are_you_sure: 你確認嗎?
       assign_to_self: 指派給自己
       assigned: 指派負責人
       comment:
-        label: 詳細解釋
         none: 沒有
       created_at: 日期
       delete: 刪除
-      history: 執行紀錄
       id: ID
       mark_as_resolved: 標示為「已處理」
       mark_as_unresolved: 標示為「未處理」
@@ -286,8 +277,6 @@ zh-HK:
         create_and_resolve: 建立筆記並標示為「已處理」
         create_and_unresolve: 建立筆記並標示為「未處理」
         delete: 刪除
-        label: 管理筆記
-        new_label: 建立管理筆記
         placeholder: 記錄已執行的動作,或其他更新
       nsfw:
         'false': 取消 NSFW 標記
@@ -301,7 +290,6 @@ zh-HK:
       resolved_msg: 舉報已處理。
       silence_account: 將用戶靜音
       status: 狀態
-      statuses: 被舉報的文章
       suspend_account: 將用戶停權
       target: 對象
       title: 舉報
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 2fec09ed8..f69d22d79 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -79,7 +79,6 @@ zh-TW:
       title: 網域封鎖
     reports:
       comment:
-        label: 留言
         none: 無
       delete: 刪除
       id: ID
diff --git a/config/settings.yml b/config/settings.yml
index 580a20895..a92a0bfd0 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -49,6 +49,7 @@ defaults: &defaults
     - root
     - webmaster
     - administrator
+  disallowed_hashtags: # space separated string or list of hashtags without the hash
   bootstrap_timeline_accounts: ''
   activity_api_enabled: true
   peers_api_enabled: true
diff --git a/lib/json_ld/activitystreams.rb b/lib/json_ld/activitystreams.rb
deleted file mode 100644
index ce740f93b..000000000
--- a/lib/json_ld/activitystreams.rb
+++ /dev/null
@@ -1,153 +0,0 @@
-# -*- encoding: utf-8 -*-
-# frozen_string_literal: true
-# This file generated automatically from https://www.w3.org/ns/activitystreams
-require 'json/ld'
-class JSON::LD::Context
-  add_preloaded("https://www.w3.org/ns/activitystreams") do
-    new(vocab: "_:", processingMode: "json-ld-1.0", term_definitions: {
-      "Accept" => TermDefinition.new("Accept", id: "https://www.w3.org/ns/activitystreams#Accept", simple: true),
-      "Activity" => TermDefinition.new("Activity", id: "https://www.w3.org/ns/activitystreams#Activity", simple: true),
-      "Add" => TermDefinition.new("Add", id: "https://www.w3.org/ns/activitystreams#Add", simple: true),
-      "Announce" => TermDefinition.new("Announce", id: "https://www.w3.org/ns/activitystreams#Announce", simple: true),
-      "Application" => TermDefinition.new("Application", id: "https://www.w3.org/ns/activitystreams#Application", simple: true),
-      "Arrive" => TermDefinition.new("Arrive", id: "https://www.w3.org/ns/activitystreams#Arrive", simple: true),
-      "Article" => TermDefinition.new("Article", id: "https://www.w3.org/ns/activitystreams#Article", simple: true),
-      "Audio" => TermDefinition.new("Audio", id: "https://www.w3.org/ns/activitystreams#Audio", simple: true),
-      "Block" => TermDefinition.new("Block", id: "https://www.w3.org/ns/activitystreams#Block", simple: true),
-      "Collection" => TermDefinition.new("Collection", id: "https://www.w3.org/ns/activitystreams#Collection", simple: true),
-      "CollectionPage" => TermDefinition.new("CollectionPage", id: "https://www.w3.org/ns/activitystreams#CollectionPage", simple: true),
-      "Create" => TermDefinition.new("Create", id: "https://www.w3.org/ns/activitystreams#Create", simple: true),
-      "Delete" => TermDefinition.new("Delete", id: "https://www.w3.org/ns/activitystreams#Delete", simple: true),
-      "Dislike" => TermDefinition.new("Dislike", id: "https://www.w3.org/ns/activitystreams#Dislike", simple: true),
-      "Document" => TermDefinition.new("Document", id: "https://www.w3.org/ns/activitystreams#Document", simple: true),
-      "Event" => TermDefinition.new("Event", id: "https://www.w3.org/ns/activitystreams#Event", simple: true),
-      "Flag" => TermDefinition.new("Flag", id: "https://www.w3.org/ns/activitystreams#Flag", simple: true),
-      "Follow" => TermDefinition.new("Follow", id: "https://www.w3.org/ns/activitystreams#Follow", simple: true),
-      "Group" => TermDefinition.new("Group", id: "https://www.w3.org/ns/activitystreams#Group", simple: true),
-      "Ignore" => TermDefinition.new("Ignore", id: "https://www.w3.org/ns/activitystreams#Ignore", simple: true),
-      "Image" => TermDefinition.new("Image", id: "https://www.w3.org/ns/activitystreams#Image", simple: true),
-      "IntransitiveActivity" => TermDefinition.new("IntransitiveActivity", id: "https://www.w3.org/ns/activitystreams#IntransitiveActivity", simple: true),
-      "Invite" => TermDefinition.new("Invite", id: "https://www.w3.org/ns/activitystreams#Invite", simple: true),
-      "IsContact" => TermDefinition.new("IsContact", id: "https://www.w3.org/ns/activitystreams#IsContact", simple: true),
-      "IsFollowedBy" => TermDefinition.new("IsFollowedBy", id: "https://www.w3.org/ns/activitystreams#IsFollowedBy", simple: true),
-      "IsFollowing" => TermDefinition.new("IsFollowing", id: "https://www.w3.org/ns/activitystreams#IsFollowing", simple: true),
-      "IsMember" => TermDefinition.new("IsMember", id: "https://www.w3.org/ns/activitystreams#IsMember", simple: true),
-      "Join" => TermDefinition.new("Join", id: "https://www.w3.org/ns/activitystreams#Join", simple: true),
-      "Leave" => TermDefinition.new("Leave", id: "https://www.w3.org/ns/activitystreams#Leave", simple: true),
-      "Like" => TermDefinition.new("Like", id: "https://www.w3.org/ns/activitystreams#Like", simple: true),
-      "Link" => TermDefinition.new("Link", id: "https://www.w3.org/ns/activitystreams#Link", simple: true),
-      "Listen" => TermDefinition.new("Listen", id: "https://www.w3.org/ns/activitystreams#Listen", simple: true),
-      "Mention" => TermDefinition.new("Mention", id: "https://www.w3.org/ns/activitystreams#Mention", simple: true),
-      "Move" => TermDefinition.new("Move", id: "https://www.w3.org/ns/activitystreams#Move", simple: true),
-      "Note" => TermDefinition.new("Note", id: "https://www.w3.org/ns/activitystreams#Note", simple: true),
-      "Object" => TermDefinition.new("Object", id: "https://www.w3.org/ns/activitystreams#Object", simple: true),
-      "Offer" => TermDefinition.new("Offer", id: "https://www.w3.org/ns/activitystreams#Offer", simple: true),
-      "OrderedCollection" => TermDefinition.new("OrderedCollection", id: "https://www.w3.org/ns/activitystreams#OrderedCollection", simple: true),
-      "OrderedCollectionPage" => TermDefinition.new("OrderedCollectionPage", id: "https://www.w3.org/ns/activitystreams#OrderedCollectionPage", simple: true),
-      "Organization" => TermDefinition.new("Organization", id: "https://www.w3.org/ns/activitystreams#Organization", simple: true),
-      "Page" => TermDefinition.new("Page", id: "https://www.w3.org/ns/activitystreams#Page", simple: true),
-      "Person" => TermDefinition.new("Person", id: "https://www.w3.org/ns/activitystreams#Person", simple: true),
-      "Place" => TermDefinition.new("Place", id: "https://www.w3.org/ns/activitystreams#Place", simple: true),
-      "Profile" => TermDefinition.new("Profile", id: "https://www.w3.org/ns/activitystreams#Profile", simple: true),
-      "Question" => TermDefinition.new("Question", id: "https://www.w3.org/ns/activitystreams#Question", simple: true),
-      "Read" => TermDefinition.new("Read", id: "https://www.w3.org/ns/activitystreams#Read", simple: true),
-      "Reject" => TermDefinition.new("Reject", id: "https://www.w3.org/ns/activitystreams#Reject", simple: true),
-      "Relationship" => TermDefinition.new("Relationship", id: "https://www.w3.org/ns/activitystreams#Relationship", simple: true),
-      "Remove" => TermDefinition.new("Remove", id: "https://www.w3.org/ns/activitystreams#Remove", simple: true),
-      "Service" => TermDefinition.new("Service", id: "https://www.w3.org/ns/activitystreams#Service", simple: true),
-      "TentativeAccept" => TermDefinition.new("TentativeAccept", id: "https://www.w3.org/ns/activitystreams#TentativeAccept", simple: true),
-      "TentativeReject" => TermDefinition.new("TentativeReject", id: "https://www.w3.org/ns/activitystreams#TentativeReject", simple: true),
-      "Tombstone" => TermDefinition.new("Tombstone", id: "https://www.w3.org/ns/activitystreams#Tombstone", simple: true),
-      "Travel" => TermDefinition.new("Travel", id: "https://www.w3.org/ns/activitystreams#Travel", simple: true),
-      "Undo" => TermDefinition.new("Undo", id: "https://www.w3.org/ns/activitystreams#Undo", simple: true),
-      "Update" => TermDefinition.new("Update", id: "https://www.w3.org/ns/activitystreams#Update", simple: true),
-      "Video" => TermDefinition.new("Video", id: "https://www.w3.org/ns/activitystreams#Video", simple: true),
-      "View" => TermDefinition.new("View", id: "https://www.w3.org/ns/activitystreams#View", simple: true),
-      "accuracy" => TermDefinition.new("accuracy", id: "https://www.w3.org/ns/activitystreams#accuracy", type_mapping: "http://www.w3.org/2001/XMLSchema#float"),
-      "actor" => TermDefinition.new("actor", id: "https://www.w3.org/ns/activitystreams#actor", type_mapping: "@id"),
-      "altitude" => TermDefinition.new("altitude", id: "https://www.w3.org/ns/activitystreams#altitude", type_mapping: "http://www.w3.org/2001/XMLSchema#float"),
-      "anyOf" => TermDefinition.new("anyOf", id: "https://www.w3.org/ns/activitystreams#anyOf", type_mapping: "@id"),
-      "as" => TermDefinition.new("as", id: "https://www.w3.org/ns/activitystreams#", simple: true, prefix: true),
-      "attachment" => TermDefinition.new("attachment", id: "https://www.w3.org/ns/activitystreams#attachment", type_mapping: "@id"),
-      "attributedTo" => TermDefinition.new("attributedTo", id: "https://www.w3.org/ns/activitystreams#attributedTo", type_mapping: "@id"),
-      "audience" => TermDefinition.new("audience", id: "https://www.w3.org/ns/activitystreams#audience", type_mapping: "@id"),
-      "bcc" => TermDefinition.new("bcc", id: "https://www.w3.org/ns/activitystreams#bcc", type_mapping: "@id"),
-      "bto" => TermDefinition.new("bto", id: "https://www.w3.org/ns/activitystreams#bto", type_mapping: "@id"),
-      "cc" => TermDefinition.new("cc", id: "https://www.w3.org/ns/activitystreams#cc", type_mapping: "@id"),
-      "closed" => TermDefinition.new("closed", id: "https://www.w3.org/ns/activitystreams#closed", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
-      "content" => TermDefinition.new("content", id: "https://www.w3.org/ns/activitystreams#content", simple: true),
-      "contentMap" => TermDefinition.new("contentMap", id: "https://www.w3.org/ns/activitystreams#content", container_mapping: "@language"),
-      "context" => TermDefinition.new("context", id: "https://www.w3.org/ns/activitystreams#context", type_mapping: "@id"),
-      "current" => TermDefinition.new("current", id: "https://www.w3.org/ns/activitystreams#current", type_mapping: "@id"),
-      "deleted" => TermDefinition.new("deleted", id: "https://www.w3.org/ns/activitystreams#deleted", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
-      "describes" => TermDefinition.new("describes", id: "https://www.w3.org/ns/activitystreams#describes", type_mapping: "@id"),
-      "duration" => TermDefinition.new("duration", id: "https://www.w3.org/ns/activitystreams#duration", type_mapping: "http://www.w3.org/2001/XMLSchema#duration"),
-      "endTime" => TermDefinition.new("endTime", id: "https://www.w3.org/ns/activitystreams#endTime", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
-      "endpoints" => TermDefinition.new("endpoints", id: "https://www.w3.org/ns/activitystreams#endpoints", type_mapping: "@id"),
-      "first" => TermDefinition.new("first", id: "https://www.w3.org/ns/activitystreams#first", type_mapping: "@id"),
-      "followers" => TermDefinition.new("followers", id: "https://www.w3.org/ns/activitystreams#followers", type_mapping: "@id"),
-      "following" => TermDefinition.new("following", id: "https://www.w3.org/ns/activitystreams#following", type_mapping: "@id"),
-      "formerType" => TermDefinition.new("formerType", id: "https://www.w3.org/ns/activitystreams#formerType", type_mapping: "@id"),
-      "generator" => TermDefinition.new("generator", id: "https://www.w3.org/ns/activitystreams#generator", type_mapping: "@id"),
-      "height" => TermDefinition.new("height", id: "https://www.w3.org/ns/activitystreams#height", type_mapping: "http://www.w3.org/2001/XMLSchema#nonNegativeInteger"),
-      "href" => TermDefinition.new("href", id: "https://www.w3.org/ns/activitystreams#href", type_mapping: "@id"),
-      "hreflang" => TermDefinition.new("hreflang", id: "https://www.w3.org/ns/activitystreams#hreflang", simple: true),
-      "icon" => TermDefinition.new("icon", id: "https://www.w3.org/ns/activitystreams#icon", type_mapping: "@id"),
-      "id" => TermDefinition.new("id", id: "@id", simple: true),
-      "image" => TermDefinition.new("image", id: "https://www.w3.org/ns/activitystreams#image", type_mapping: "@id"),
-      "inReplyTo" => TermDefinition.new("inReplyTo", id: "https://www.w3.org/ns/activitystreams#inReplyTo", type_mapping: "@id"),
-      "inbox" => TermDefinition.new("inbox", id: "http://www.w3.org/ns/ldp#inbox", type_mapping: "@id"),
-      "instrument" => TermDefinition.new("instrument", id: "https://www.w3.org/ns/activitystreams#instrument", type_mapping: "@id"),
-      "items" => TermDefinition.new("items", id: "https://www.w3.org/ns/activitystreams#items", type_mapping: "@id"),
-      "last" => TermDefinition.new("last", id: "https://www.w3.org/ns/activitystreams#last", type_mapping: "@id"),
-      "latitude" => TermDefinition.new("latitude", id: "https://www.w3.org/ns/activitystreams#latitude", type_mapping: "http://www.w3.org/2001/XMLSchema#float"),
-      "ldp" => TermDefinition.new("ldp", id: "http://www.w3.org/ns/ldp#", simple: true, prefix: true),
-      "liked" => TermDefinition.new("liked", id: "https://www.w3.org/ns/activitystreams#liked", type_mapping: "@id"),
-      "location" => TermDefinition.new("location", id: "https://www.w3.org/ns/activitystreams#location", type_mapping: "@id"),
-      "longitude" => TermDefinition.new("longitude", id: "https://www.w3.org/ns/activitystreams#longitude", type_mapping: "http://www.w3.org/2001/XMLSchema#float"),
-      "mediaType" => TermDefinition.new("mediaType", id: "https://www.w3.org/ns/activitystreams#mediaType", simple: true),
-      "name" => TermDefinition.new("name", id: "https://www.w3.org/ns/activitystreams#name", simple: true),
-      "nameMap" => TermDefinition.new("nameMap", id: "https://www.w3.org/ns/activitystreams#name", container_mapping: "@language"),
-      "next" => TermDefinition.new("next", id: "https://www.w3.org/ns/activitystreams#next", type_mapping: "@id"),
-      "oauthAuthorizationEndpoint" => TermDefinition.new("oauthAuthorizationEndpoint", id: "https://www.w3.org/ns/activitystreams#oauthAuthorizationEndpoint", type_mapping: "@id"),
-      "oauthTokenEndpoint" => TermDefinition.new("oauthTokenEndpoint", id: "https://www.w3.org/ns/activitystreams#oauthTokenEndpoint", type_mapping: "@id"),
-      "object" => TermDefinition.new("object", id: "https://www.w3.org/ns/activitystreams#object", type_mapping: "@id"),
-      "oneOf" => TermDefinition.new("oneOf", id: "https://www.w3.org/ns/activitystreams#oneOf", type_mapping: "@id"),
-      "orderedItems" => TermDefinition.new("orderedItems", id: "https://www.w3.org/ns/activitystreams#items", type_mapping: "@id", container_mapping: "@list"),
-      "origin" => TermDefinition.new("origin", id: "https://www.w3.org/ns/activitystreams#origin", type_mapping: "@id"),
-      "outbox" => TermDefinition.new("outbox", id: "https://www.w3.org/ns/activitystreams#outbox", type_mapping: "@id"),
-      "partOf" => TermDefinition.new("partOf", id: "https://www.w3.org/ns/activitystreams#partOf", type_mapping: "@id"),
-      "preferredUsername" => TermDefinition.new("preferredUsername", id: "https://www.w3.org/ns/activitystreams#preferredUsername", simple: true),
-      "prev" => TermDefinition.new("prev", id: "https://www.w3.org/ns/activitystreams#prev", type_mapping: "@id"),
-      "preview" => TermDefinition.new("preview", id: "https://www.w3.org/ns/activitystreams#preview", type_mapping: "@id"),
-      "provideClientKey" => TermDefinition.new("provideClientKey", id: "https://www.w3.org/ns/activitystreams#provideClientKey", type_mapping: "@id"),
-      "proxyUrl" => TermDefinition.new("proxyUrl", id: "https://www.w3.org/ns/activitystreams#proxyUrl", type_mapping: "@id"),
-      "published" => TermDefinition.new("published", id: "https://www.w3.org/ns/activitystreams#published", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
-      "radius" => TermDefinition.new("radius", id: "https://www.w3.org/ns/activitystreams#radius", type_mapping: "http://www.w3.org/2001/XMLSchema#float"),
-      "rel" => TermDefinition.new("rel", id: "https://www.w3.org/ns/activitystreams#rel", simple: true),
-      "relationship" => TermDefinition.new("relationship", id: "https://www.w3.org/ns/activitystreams#relationship", type_mapping: "@id"),
-      "replies" => TermDefinition.new("replies", id: "https://www.w3.org/ns/activitystreams#replies", type_mapping: "@id"),
-      "result" => TermDefinition.new("result", id: "https://www.w3.org/ns/activitystreams#result", type_mapping: "@id"),
-      "sharedInbox" => TermDefinition.new("sharedInbox", id: "https://www.w3.org/ns/activitystreams#sharedInbox", type_mapping: "@id"),
-      "signClientKey" => TermDefinition.new("signClientKey", id: "https://www.w3.org/ns/activitystreams#signClientKey", type_mapping: "@id"),
-      "source" => TermDefinition.new("source", id: "https://www.w3.org/ns/activitystreams#source", simple: true),
-      "startIndex" => TermDefinition.new("startIndex", id: "https://www.w3.org/ns/activitystreams#startIndex", type_mapping: "http://www.w3.org/2001/XMLSchema#nonNegativeInteger"),
-      "startTime" => TermDefinition.new("startTime", id: "https://www.w3.org/ns/activitystreams#startTime", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
-      "streams" => TermDefinition.new("streams", id: "https://www.w3.org/ns/activitystreams#streams", type_mapping: "@id"),
-      "subject" => TermDefinition.new("subject", id: "https://www.w3.org/ns/activitystreams#subject", type_mapping: "@id"),
-      "summary" => TermDefinition.new("summary", id: "https://www.w3.org/ns/activitystreams#summary", simple: true),
-      "summaryMap" => TermDefinition.new("summaryMap", id: "https://www.w3.org/ns/activitystreams#summary", container_mapping: "@language"),
-      "tag" => TermDefinition.new("tag", id: "https://www.w3.org/ns/activitystreams#tag", type_mapping: "@id"),
-      "target" => TermDefinition.new("target", id: "https://www.w3.org/ns/activitystreams#target", type_mapping: "@id"),
-      "to" => TermDefinition.new("to", id: "https://www.w3.org/ns/activitystreams#to", type_mapping: "@id"),
-      "totalItems" => TermDefinition.new("totalItems", id: "https://www.w3.org/ns/activitystreams#totalItems", type_mapping: "http://www.w3.org/2001/XMLSchema#nonNegativeInteger"),
-      "type" => TermDefinition.new("type", id: "@type", simple: true),
-      "units" => TermDefinition.new("units", id: "https://www.w3.org/ns/activitystreams#units", simple: true),
-      "updated" => TermDefinition.new("updated", id: "https://www.w3.org/ns/activitystreams#updated", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
-      "uploadMedia" => TermDefinition.new("uploadMedia", id: "https://www.w3.org/ns/activitystreams#uploadMedia", type_mapping: "@id"),
-      "url" => TermDefinition.new("url", id: "https://www.w3.org/ns/activitystreams#url", type_mapping: "@id"),
-      "width" => TermDefinition.new("width", id: "https://www.w3.org/ns/activitystreams#width", type_mapping: "http://www.w3.org/2001/XMLSchema#nonNegativeInteger"),
-      "xsd" => TermDefinition.new("xsd", id: "http://www.w3.org/2001/XMLSchema#", simple: true, prefix: true)
-    })
-  end
-end
diff --git a/lib/json_ld/identity.rb b/lib/json_ld/identity.rb
deleted file mode 100644
index cfe50b956..000000000
--- a/lib/json_ld/identity.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# -*- encoding: utf-8 -*-
-# frozen_string_literal: true
-# This file generated automatically from https://w3id.org/identity/v1
-require 'json/ld'
-class JSON::LD::Context
-  add_preloaded("https://w3id.org/identity/v1") do
-    new(processingMode: "json-ld-1.0", term_definitions: {
-      "Credential" => TermDefinition.new("Credential", id: "https://w3id.org/credentials#Credential", simple: true),
-      "CryptographicKey" => TermDefinition.new("CryptographicKey", id: "https://w3id.org/security#Key", simple: true),
-      "CryptographicKeyCredential" => TermDefinition.new("CryptographicKeyCredential", id: "https://w3id.org/credentials#CryptographicKeyCredential", simple: true),
-      "EncryptedMessage" => TermDefinition.new("EncryptedMessage", id: "https://w3id.org/security#EncryptedMessage", simple: true),
-      "GraphSignature2012" => TermDefinition.new("GraphSignature2012", id: "https://w3id.org/security#GraphSignature2012", simple: true),
-      "Group" => TermDefinition.new("Group", id: "https://www.w3.org/ns/activitystreams#Group", simple: true),
-      "Identity" => TermDefinition.new("Identity", id: "https://w3id.org/identity#Identity", simple: true),
-      "LinkedDataSignature2015" => TermDefinition.new("LinkedDataSignature2015", id: "https://w3id.org/security#LinkedDataSignature2015", simple: true),
-      "Organization" => TermDefinition.new("Organization", id: "http://schema.org/Organization", simple: true),
-      "Person" => TermDefinition.new("Person", id: "http://schema.org/Person", simple: true),
-      "PostalAddress" => TermDefinition.new("PostalAddress", id: "http://schema.org/PostalAddress", simple: true),
-      "about" => TermDefinition.new("about", id: "http://schema.org/about", type_mapping: "@id"),
-      "accessControl" => TermDefinition.new("accessControl", id: "https://w3id.org/permissions#accessControl", type_mapping: "@id"),
-      "address" => TermDefinition.new("address", id: "http://schema.org/address", type_mapping: "@id"),
-      "addressCountry" => TermDefinition.new("addressCountry", id: "http://schema.org/addressCountry", simple: true),
-      "addressLocality" => TermDefinition.new("addressLocality", id: "http://schema.org/addressLocality", simple: true),
-      "addressRegion" => TermDefinition.new("addressRegion", id: "http://schema.org/addressRegion", simple: true),
-      "cipherAlgorithm" => TermDefinition.new("cipherAlgorithm", id: "https://w3id.org/security#cipherAlgorithm", simple: true),
-      "cipherData" => TermDefinition.new("cipherData", id: "https://w3id.org/security#cipherData", simple: true),
-      "cipherKey" => TermDefinition.new("cipherKey", id: "https://w3id.org/security#cipherKey", simple: true),
-      "claim" => TermDefinition.new("claim", id: "https://w3id.org/credentials#claim", type_mapping: "@id"),
-      "comment" => TermDefinition.new("comment", id: "http://www.w3.org/2000/01/rdf-schema#comment", simple: true),
-      "created" => TermDefinition.new("created", id: "http://purl.org/dc/terms/created", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
-      "creator" => TermDefinition.new("creator", id: "http://purl.org/dc/terms/creator", type_mapping: "@id"),
-      "cred" => TermDefinition.new("cred", id: "https://w3id.org/credentials#", simple: true, prefix: true),
-      "credential" => TermDefinition.new("credential", id: "https://w3id.org/credentials#credential", type_mapping: "@id"),
-      "dc" => TermDefinition.new("dc", id: "http://purl.org/dc/terms/", simple: true, prefix: true),
-      "description" => TermDefinition.new("description", id: "http://schema.org/description", simple: true),
-      "digestAlgorithm" => TermDefinition.new("digestAlgorithm", id: "https://w3id.org/security#digestAlgorithm", simple: true),
-      "digestValue" => TermDefinition.new("digestValue", id: "https://w3id.org/security#digestValue", simple: true),
-      "domain" => TermDefinition.new("domain", id: "https://w3id.org/security#domain", simple: true),
-      "email" => TermDefinition.new("email", id: "http://schema.org/email", simple: true),
-      "expires" => TermDefinition.new("expires", id: "https://w3id.org/security#expiration", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
-      "familyName" => TermDefinition.new("familyName", id: "http://schema.org/familyName", simple: true),
-      "givenName" => TermDefinition.new("givenName", id: "http://schema.org/givenName", simple: true),
-      "id" => TermDefinition.new("id", id: "@id", simple: true),
-      "identity" => TermDefinition.new("identity", id: "https://w3id.org/identity#", simple: true, prefix: true),
-      "identityService" => TermDefinition.new("identityService", id: "https://w3id.org/identity#identityService", type_mapping: "@id"),
-      "idp" => TermDefinition.new("idp", id: "https://w3id.org/identity#idp", type_mapping: "@id"),
-      "image" => TermDefinition.new("image", id: "http://schema.org/image", type_mapping: "@id"),
-      "initializationVector" => TermDefinition.new("initializationVector", id: "https://w3id.org/security#initializationVector", simple: true),
-      "issued" => TermDefinition.new("issued", id: "https://w3id.org/credentials#issued", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
-      "issuer" => TermDefinition.new("issuer", id: "https://w3id.org/credentials#issuer", type_mapping: "@id"),
-      "label" => TermDefinition.new("label", id: "http://www.w3.org/2000/01/rdf-schema#label", simple: true),
-      "member" => TermDefinition.new("member", id: "http://schema.org/member", type_mapping: "@id"),
-      "memberOf" => TermDefinition.new("memberOf", id: "http://schema.org/memberOf", type_mapping: "@id"),
-      "name" => TermDefinition.new("name", id: "http://schema.org/name", simple: true),
-      "nonce" => TermDefinition.new("nonce", id: "https://w3id.org/security#nonce", simple: true),
-      "normalizationAlgorithm" => TermDefinition.new("normalizationAlgorithm", id: "https://w3id.org/security#normalizationAlgorithm", simple: true),
-      "owner" => TermDefinition.new("owner", id: "https://w3id.org/security#owner", type_mapping: "@id"),
-      "password" => TermDefinition.new("password", id: "https://w3id.org/security#password", simple: true),
-      "paymentProcessor" => TermDefinition.new("paymentProcessor", id: "https://w3id.org/payswarm#processor", simple: true),
-      "perm" => TermDefinition.new("perm", id: "https://w3id.org/permissions#", simple: true, prefix: true),
-      "postalCode" => TermDefinition.new("postalCode", id: "http://schema.org/postalCode", simple: true),
-      "preferences" => TermDefinition.new("preferences", id: "https://w3id.org/payswarm#preferences", type_mapping: "@vocab"),
-      "privateKey" => TermDefinition.new("privateKey", id: "https://w3id.org/security#privateKey", type_mapping: "@id"),
-      "privateKeyPem" => TermDefinition.new("privateKeyPem", id: "https://w3id.org/security#privateKeyPem", simple: true),
-      "ps" => TermDefinition.new("ps", id: "https://w3id.org/payswarm#", simple: true, prefix: true),
-      "publicKey" => TermDefinition.new("publicKey", id: "https://w3id.org/security#publicKey", type_mapping: "@id"),
-      "publicKeyPem" => TermDefinition.new("publicKeyPem", id: "https://w3id.org/security#publicKeyPem", simple: true),
-      "publicKeyService" => TermDefinition.new("publicKeyService", id: "https://w3id.org/security#publicKeyService", type_mapping: "@id"),
-      "rdf" => TermDefinition.new("rdf", id: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", simple: true, prefix: true),
-      "rdfs" => TermDefinition.new("rdfs", id: "http://www.w3.org/2000/01/rdf-schema#", simple: true, prefix: true),
-      "recipient" => TermDefinition.new("recipient", id: "https://w3id.org/credentials#recipient", type_mapping: "@id"),
-      "revoked" => TermDefinition.new("revoked", id: "https://w3id.org/security#revoked", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
-      "schema" => TermDefinition.new("schema", id: "http://schema.org/", simple: true, prefix: true),
-      "sec" => TermDefinition.new("sec", id: "https://w3id.org/security#", simple: true, prefix: true),
-      "signature" => TermDefinition.new("signature", id: "https://w3id.org/security#signature", simple: true),
-      "signatureAlgorithm" => TermDefinition.new("signatureAlgorithm", id: "https://w3id.org/security#signatureAlgorithm", simple: true),
-      "signatureValue" => TermDefinition.new("signatureValue", id: "https://w3id.org/security#signatureValue", simple: true),
-      "streetAddress" => TermDefinition.new("streetAddress", id: "http://schema.org/streetAddress", simple: true),
-      "title" => TermDefinition.new("title", id: "http://purl.org/dc/terms/title", simple: true),
-      "type" => TermDefinition.new("type", id: "@type", simple: true),
-      "url" => TermDefinition.new("url", id: "http://schema.org/url", type_mapping: "@id"),
-      "writePermission" => TermDefinition.new("writePermission", id: "https://w3id.org/permissions#writePermission", type_mapping: "@id"),
-      "xsd" => TermDefinition.new("xsd", id: "http://www.w3.org/2001/XMLSchema#", simple: true, prefix: true)
-    })
-  end
-end
diff --git a/lib/json_ld/security.rb b/lib/json_ld/security.rb
deleted file mode 100644
index 1230206f0..000000000
--- a/lib/json_ld/security.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# -*- encoding: utf-8 -*-
-# frozen_string_literal: true
-# This file generated automatically from https://w3id.org/security/v1
-require 'json/ld'
-class JSON::LD::Context
-  add_preloaded("https://w3id.org/security/v1") do
-    new(processingMode: "json-ld-1.0", term_definitions: {
-      "CryptographicKey" => TermDefinition.new("CryptographicKey", id: "https://w3id.org/security#Key", simple: true),
-      "EcdsaKoblitzSignature2016" => TermDefinition.new("EcdsaKoblitzSignature2016", id: "https://w3id.org/security#EcdsaKoblitzSignature2016", simple: true),
-      "EncryptedMessage" => TermDefinition.new("EncryptedMessage", id: "https://w3id.org/security#EncryptedMessage", simple: true),
-      "GraphSignature2012" => TermDefinition.new("GraphSignature2012", id: "https://w3id.org/security#GraphSignature2012", simple: true),
-      "LinkedDataSignature2015" => TermDefinition.new("LinkedDataSignature2015", id: "https://w3id.org/security#LinkedDataSignature2015", simple: true),
-      "LinkedDataSignature2016" => TermDefinition.new("LinkedDataSignature2016", id: "https://w3id.org/security#LinkedDataSignature2016", simple: true),
-      "authenticationTag" => TermDefinition.new("authenticationTag", id: "https://w3id.org/security#authenticationTag", simple: true),
-      "canonicalizationAlgorithm" => TermDefinition.new("canonicalizationAlgorithm", id: "https://w3id.org/security#canonicalizationAlgorithm", simple: true),
-      "cipherAlgorithm" => TermDefinition.new("cipherAlgorithm", id: "https://w3id.org/security#cipherAlgorithm", simple: true),
-      "cipherData" => TermDefinition.new("cipherData", id: "https://w3id.org/security#cipherData", simple: true),
-      "cipherKey" => TermDefinition.new("cipherKey", id: "https://w3id.org/security#cipherKey", simple: true),
-      "created" => TermDefinition.new("created", id: "http://purl.org/dc/terms/created", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
-      "creator" => TermDefinition.new("creator", id: "http://purl.org/dc/terms/creator", type_mapping: "@id"),
-      "dc" => TermDefinition.new("dc", id: "http://purl.org/dc/terms/", simple: true, prefix: true),
-      "digestAlgorithm" => TermDefinition.new("digestAlgorithm", id: "https://w3id.org/security#digestAlgorithm", simple: true),
-      "digestValue" => TermDefinition.new("digestValue", id: "https://w3id.org/security#digestValue", simple: true),
-      "domain" => TermDefinition.new("domain", id: "https://w3id.org/security#domain", simple: true),
-      "encryptionKey" => TermDefinition.new("encryptionKey", id: "https://w3id.org/security#encryptionKey", simple: true),
-      "expiration" => TermDefinition.new("expiration", id: "https://w3id.org/security#expiration", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
-      "expires" => TermDefinition.new("expires", id: "https://w3id.org/security#expiration", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
-      "id" => TermDefinition.new("id", id: "@id", simple: true),
-      "initializationVector" => TermDefinition.new("initializationVector", id: "https://w3id.org/security#initializationVector", simple: true),
-      "iterationCount" => TermDefinition.new("iterationCount", id: "https://w3id.org/security#iterationCount", simple: true),
-      "nonce" => TermDefinition.new("nonce", id: "https://w3id.org/security#nonce", simple: true),
-      "normalizationAlgorithm" => TermDefinition.new("normalizationAlgorithm", id: "https://w3id.org/security#normalizationAlgorithm", simple: true),
-      "owner" => TermDefinition.new("owner", id: "https://w3id.org/security#owner", type_mapping: "@id"),
-      "password" => TermDefinition.new("password", id: "https://w3id.org/security#password", simple: true),
-      "privateKey" => TermDefinition.new("privateKey", id: "https://w3id.org/security#privateKey", type_mapping: "@id"),
-      "privateKeyPem" => TermDefinition.new("privateKeyPem", id: "https://w3id.org/security#privateKeyPem", simple: true),
-      "publicKey" => TermDefinition.new("publicKey", id: "https://w3id.org/security#publicKey", type_mapping: "@id"),
-      "publicKeyPem" => TermDefinition.new("publicKeyPem", id: "https://w3id.org/security#publicKeyPem", simple: true),
-      "publicKeyService" => TermDefinition.new("publicKeyService", id: "https://w3id.org/security#publicKeyService", type_mapping: "@id"),
-      "revoked" => TermDefinition.new("revoked", id: "https://w3id.org/security#revoked", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"),
-      "salt" => TermDefinition.new("salt", id: "https://w3id.org/security#salt", simple: true),
-      "sec" => TermDefinition.new("sec", id: "https://w3id.org/security#", simple: true, prefix: true),
-      "signature" => TermDefinition.new("signature", id: "https://w3id.org/security#signature", simple: true),
-      "signatureAlgorithm" => TermDefinition.new("signatureAlgorithm", id: "https://w3id.org/security#signingAlgorithm", simple: true),
-      "signatureValue" => TermDefinition.new("signatureValue", id: "https://w3id.org/security#signatureValue", simple: true),
-      "type" => TermDefinition.new("type", id: "@type", simple: true),
-      "xsd" => TermDefinition.new("xsd", id: "http://www.w3.org/2001/XMLSchema#", simple: true, prefix: true)
-    })
-  end
-end
diff --git a/lib/mastodon/migration_helpers.rb b/lib/mastodon/migration_helpers.rb
index 6f6f99f63..e154b5a2c 100644
--- a/lib/mastodon/migration_helpers.rb
+++ b/lib/mastodon/migration_helpers.rb
@@ -985,6 +985,17 @@ into similar problems in the future (e.g. when new tables are created).
         BackgroundMigrationWorker.perform_in(delay_interval * index, job_class_name, [start_id, end_id])
       end
     end
+
+    private
+
+    # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#L678-L684
+    def extract_foreign_key_action(specifier)
+      case specifier
+      when 'c'; :cascade
+      when 'n'; :nullify
+      when 'r'; :restrict
+      end
+    end
   end
 end
 
diff --git a/lib/mastodon/redis_config.rb b/lib/mastodon/redis_config.rb
index cf4f20f76..f11d94a45 100644
--- a/lib/mastodon/redis_config.rb
+++ b/lib/mastodon/redis_config.rb
@@ -1,16 +1,29 @@
 # frozen_string_literal: true
 
-if ENV['REDIS_URL'].blank?
-  password = ENV.fetch('REDIS_PASSWORD') { '' }
-  host     = ENV.fetch('REDIS_HOST') { 'localhost' }
-  port     = ENV.fetch('REDIS_PORT') { 6379 }
-  db       = ENV.fetch('REDIS_DB') { 0 }
+def setup_redis_env_url(prefix = nil, defaults = true)
+  prefix = prefix.to_s.upcase + '_' unless prefix.nil?
+  prefix = '' if prefix.nil?
 
-  ENV['REDIS_URL'] = "redis://#{password.blank? ? '' : ":#{password}@"}#{host}:#{port}/#{db}"
+  return if ENV[prefix + 'REDIS_URL'].present?
+
+  password = ENV.fetch(prefix + 'REDIS_PASSWORD') { '' if defaults }
+  host     = ENV.fetch(prefix + 'REDIS_HOST') { 'localhost' if defaults }
+  port     = ENV.fetch(prefix + 'REDIS_PORT') { 6379 if defaults }
+  db       = ENV.fetch(prefix + 'REDIS_DB') { 0 if defaults }
+
+  ENV[prefix + 'REDIS_URL'] = if [password, host, port, db].all?(&:nil?)
+                                ENV['REDIS_URL']
+                              else
+                                "redis://#{password.blank? ? '' : ":#{password}@"}#{host}:#{port}/#{db}"
+                              end
 end
 
-namespace = ENV.fetch('REDIS_NAMESPACE') { nil }
+setup_redis_env_url
+setup_redis_env_url(:cache, false)
+
+namespace       = ENV.fetch('REDIS_NAMESPACE') { nil }
 cache_namespace = namespace ? namespace + '_cache' : 'cache'
+
 REDIS_CACHE_PARAMS = {
   expires_in: 10.minutes,
   namespace: cache_namespace,
diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake
index 505c7e0fa..00a85fa5e 100644
--- a/lib/tasks/mastodon.rake
+++ b/lib/tasks/mastodon.rake
@@ -2,6 +2,8 @@
 
 require 'optparse'
 require 'colorize'
+require 'tty-command'
+require 'tty-prompt'
 
 namespace :mastodon do
   desc 'Configure the instance for production use'
@@ -107,9 +109,16 @@ namespace :mastodon do
           q.convert :int
         end
 
+        env['REDIS_PASSWORD'] = prompt.ask('Redis password:') do |q|
+          q.required false
+          q.default nil
+          q.modify :strip
+        end
+
         redis_options = {
           host: env['REDIS_HOST'],
           port: env['REDIS_PORT'],
+          password: env['REDIS_PASSWORD'],
           driver: :hiredis,
         }
 
diff --git a/spec/controllers/about_controller_spec.rb b/spec/controllers/about_controller_spec.rb
index c2c34d34a..2089b3b16 100644
--- a/spec/controllers/about_controller_spec.rb
+++ b/spec/controllers/about_controller_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe AboutController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -35,7 +35,7 @@ RSpec.describe AboutController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -49,7 +49,7 @@ RSpec.describe AboutController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb
index a8ade790c..18c249c07 100644
--- a/spec/controllers/accounts_controller_spec.rb
+++ b/spec/controllers/accounts_controller_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe AccountsController, type: :controller do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'returns correct format' do
diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/controllers/activitypub/outboxes_controller_spec.rb
index a25998021..47460b22c 100644
--- a/spec/controllers/activitypub/outboxes_controller_spec.rb
+++ b/spec/controllers/activitypub/outboxes_controller_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'returns application/activity+json' do
diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb
index 8be27d866..ff9dbbfb8 100644
--- a/spec/controllers/admin/accounts_controller_spec.rb
+++ b/spec/controllers/admin/accounts_controller_spec.rb
@@ -63,7 +63,7 @@ RSpec.describe Admin::AccountsController, type: :controller do
 
     it 'returns http success' do
       get :index
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -72,7 +72,7 @@ RSpec.describe Admin::AccountsController, type: :controller do
 
     it 'returns http success' do
       get :show, params: { id: account.id }
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 end
diff --git a/spec/controllers/admin/change_email_controller_spec.rb b/spec/controllers/admin/change_email_controller_spec.rb
index 50f94f835..31df0f0fc 100644
--- a/spec/controllers/admin/change_email_controller_spec.rb
+++ b/spec/controllers/admin/change_email_controller_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Admin::ChangeEmailsController, type: :controller do
 
       get :show, params: { account_id: account.id }
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
diff --git a/spec/controllers/admin/confirmations_controller_spec.rb b/spec/controllers/admin/confirmations_controller_spec.rb
index 3f2b28c0e..7c8034964 100644
--- a/spec/controllers/admin/confirmations_controller_spec.rb
+++ b/spec/controllers/admin/confirmations_controller_spec.rb
@@ -20,14 +20,14 @@ RSpec.describe Admin::ConfirmationsController, type: :controller do
     it 'raises an error when there is no account' do
       post :create, params: { account_id: 'fake' }
 
-      expect(response).to have_http_status(:missing)
+      expect(response).to have_http_status(404)
     end
 
     it 'raises an error when there is no user' do
       account = Fabricate(:account, user: nil)
       post :create, params: { account_id: account.id }
 
-      expect(response).to have_http_status(:missing)
+      expect(response).to have_http_status(404)
     end
   end
 end
diff --git a/spec/controllers/admin/domain_blocks_controller_spec.rb b/spec/controllers/admin/domain_blocks_controller_spec.rb
index b9e73c04b..79e7fea42 100644
--- a/spec/controllers/admin/domain_blocks_controller_spec.rb
+++ b/spec/controllers/admin/domain_blocks_controller_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do
       assigned = assigns(:domain_blocks)
       expect(assigned.count).to eq 1
       expect(assigned.klass).to be DomainBlock
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -32,7 +32,7 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do
       get :new
 
       expect(assigns(:domain_block)).to be_instance_of(DomainBlock)
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -41,7 +41,7 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do
       domain_block = Fabricate(:domain_block)
       get :show, params: { id: domain_block.id }
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
diff --git a/spec/controllers/admin/email_domain_blocks_controller_spec.rb b/spec/controllers/admin/email_domain_blocks_controller_spec.rb
index 295de9073..133d38ff1 100644
--- a/spec/controllers/admin/email_domain_blocks_controller_spec.rb
+++ b/spec/controllers/admin/email_domain_blocks_controller_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Admin::EmailDomainBlocksController, type: :controller do
       assigned = assigns(:email_domain_blocks)
       expect(assigned.count).to eq 1
       expect(assigned.klass).to be EmailDomainBlock
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -34,7 +34,7 @@ RSpec.describe Admin::EmailDomainBlocksController, type: :controller do
       get :new
 
       expect(assigns(:email_domain_block)).to be_instance_of(EmailDomainBlock)
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
diff --git a/spec/controllers/admin/instances_controller_spec.rb b/spec/controllers/admin/instances_controller_spec.rb
index f57e3fa97..412b81443 100644
--- a/spec/controllers/admin/instances_controller_spec.rb
+++ b/spec/controllers/admin/instances_controller_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Admin::InstancesController, type: :controller do
       expect(instances.size).to eq 1
       expect(instances[0].domain).to eq 'less.popular'
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 end
diff --git a/spec/controllers/admin/reported_statuses_controller_spec.rb b/spec/controllers/admin/reported_statuses_controller_spec.rb
index 297807d41..29957ed37 100644
--- a/spec/controllers/admin/reported_statuses_controller_spec.rb
+++ b/spec/controllers/admin/reported_statuses_controller_spec.rb
@@ -13,7 +13,7 @@ describe Admin::ReportedStatusesController do
 
   describe 'POST #create' do
     subject do
-      -> { post :create, params: { report_id: report, form_status_batch: { action: action, status_ids: status_ids } } }
+      -> { post :create, params: { :report_id  => report, action => '', :form_status_batch => { status_ids: status_ids } } }
     end
 
     let(:action) { 'nsfw_on' }
@@ -84,7 +84,7 @@ describe Admin::ReportedStatusesController do
       allow(RemovalWorker).to receive(:perform_async)
 
       delete :destroy, params: { report_id: report, id: status }
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(RemovalWorker).
         to have_received(:perform_async).with(status.id)
     end
diff --git a/spec/controllers/admin/reports_controller_spec.rb b/spec/controllers/admin/reports_controller_spec.rb
index 9be298df6..e50c02a72 100644
--- a/spec/controllers/admin/reports_controller_spec.rb
+++ b/spec/controllers/admin/reports_controller_spec.rb
@@ -18,7 +18,7 @@ describe Admin::ReportsController do
       reports = assigns(:reports).to_a
       expect(reports.size).to eq 1
       expect(reports[0]).to eq specified
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'returns http success with resolved filter' do
@@ -31,7 +31,7 @@ describe Admin::ReportsController do
       expect(reports.size).to eq 1
       expect(reports[0]).to eq specified
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -42,7 +42,7 @@ describe Admin::ReportsController do
       get :show, params: { id: report }
 
       expect(assigns(:report)).to eq report
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -52,7 +52,7 @@ describe Admin::ReportsController do
         report = Fabricate(:report)
         put :update, params: { id: report, outcome: 'unknown' }
 
-        expect(response).to have_http_status(:missing)
+        expect(response).to have_http_status(404)
       end
     end
 
diff --git a/spec/controllers/admin/settings_controller_spec.rb b/spec/controllers/admin/settings_controller_spec.rb
index 609bc762b..eaf99679a 100644
--- a/spec/controllers/admin/settings_controller_spec.rb
+++ b/spec/controllers/admin/settings_controller_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Admin::SettingsController, type: :controller do
       it 'returns http success' do
         get :edit
 
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
     end
 
diff --git a/spec/controllers/admin/statuses_controller_spec.rb b/spec/controllers/admin/statuses_controller_spec.rb
index 1515e299b..cbaf39786 100644
--- a/spec/controllers/admin/statuses_controller_spec.rb
+++ b/spec/controllers/admin/statuses_controller_spec.rb
@@ -20,7 +20,7 @@ describe Admin::StatusesController do
 
       statuses = assigns(:statuses).to_a
       expect(statuses.size).to eq 2
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'returns http success with media' do
@@ -28,7 +28,7 @@ describe Admin::StatusesController do
 
       statuses = assigns(:statuses).to_a
       expect(statuses.size).to eq 1
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -99,7 +99,7 @@ describe Admin::StatusesController do
       allow(RemovalWorker).to receive(:perform_async)
 
       delete :destroy, params: { account_id: account.id, id: status }
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(RemovalWorker).
         to have_received(:perform_async).with(status.id)
     end
diff --git a/spec/controllers/admin/subscriptions_controller_spec.rb b/spec/controllers/admin/subscriptions_controller_spec.rb
index eb6f12b16..967152abe 100644
--- a/spec/controllers/admin/subscriptions_controller_spec.rb
+++ b/spec/controllers/admin/subscriptions_controller_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Admin::SubscriptionsController, type: :controller do
       expect(subscriptions.count).to eq 1
       expect(subscriptions[0]).to eq specified
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 end
diff --git a/spec/controllers/api/base_controller_spec.rb b/spec/controllers/api/base_controller_spec.rb
index 0c7ca8990..750ccc8cf 100644
--- a/spec/controllers/api/base_controller_spec.rb
+++ b/spec/controllers/api/base_controller_spec.rb
@@ -23,7 +23,7 @@ describe Api::BaseController do
     it 'does not protect from forgery' do
       ActionController::Base.allow_forgery_protection = true
       post 'success'
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
diff --git a/spec/controllers/api/oembed_controller_spec.rb b/spec/controllers/api/oembed_controller_spec.rb
index 7af4a6a5b..7fee15a35 100644
--- a/spec/controllers/api/oembed_controller_spec.rb
+++ b/spec/controllers/api/oembed_controller_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Api::OEmbedController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 end
diff --git a/spec/controllers/api/push_controller_spec.rb b/spec/controllers/api/push_controller_spec.rb
index 647698bd1..d769d8554 100644
--- a/spec/controllers/api/push_controller_spec.rb
+++ b/spec/controllers/api/push_controller_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Api::PushController, type: :controller do
           '3600',
           nil
         )
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(202)
       end
     end
 
@@ -43,7 +43,7 @@ RSpec.describe Api::PushController, type: :controller do
           account,
           'https://callback.host/api',
         )
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(202)
       end
     end
 
diff --git a/spec/controllers/api/salmon_controller_spec.rb b/spec/controllers/api/salmon_controller_spec.rb
index 8af8b83a8..5f01f8073 100644
--- a/spec/controllers/api/salmon_controller_spec.rb
+++ b/spec/controllers/api/salmon_controller_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Api::SalmonController, type: :controller do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(202)
       end
 
       it 'creates remote account' do
diff --git a/spec/controllers/api/subscriptions_controller_spec.rb b/spec/controllers/api/subscriptions_controller_spec.rb
index d90da9e32..48eb1fc64 100644
--- a/spec/controllers/api/subscriptions_controller_spec.rb
+++ b/spec/controllers/api/subscriptions_controller_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Api::SubscriptionsController, type: :controller do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'echoes back the challenge' do
@@ -27,7 +27,7 @@ RSpec.describe Api::SubscriptionsController, type: :controller do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:missing)
+        expect(response).to have_http_status(404)
       end
     end
   end
@@ -59,7 +59,7 @@ RSpec.describe Api::SubscriptionsController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'creates statuses for feed' do
diff --git a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb
index efbef439a..08010bcc1 100644
--- a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb
@@ -14,7 +14,7 @@ describe Api::V1::Accounts::CredentialsController do
     describe 'GET #show' do
       it 'returns http success' do
         get :show
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
     end
 
@@ -36,7 +36,7 @@ describe Api::V1::Accounts::CredentialsController do
         end
 
         it 'returns http success' do
-          expect(response).to have_http_status(:success)
+          expect(response).to have_http_status(200)
         end
 
         it 'updates account info' do
diff --git a/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb b/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb
index 33982cb8f..b47af4963 100644
--- a/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb
@@ -15,7 +15,7 @@ describe Api::V1::Accounts::FollowerAccountsController do
     it 'returns http success' do
       get :index, params: { account_id: user.account.id, limit: 1 }
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 end
diff --git a/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb b/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb
index e22f54a31..29fd7cd5b 100644
--- a/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb
@@ -15,7 +15,7 @@ describe Api::V1::Accounts::FollowingAccountsController do
     it 'returns http success' do
       get :index, params: { account_id: user.account.id, limit: 1 }
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 end
diff --git a/spec/controllers/api/v1/accounts/lists_controller_spec.rb b/spec/controllers/api/v1/accounts/lists_controller_spec.rb
index 0a372f65b..df9fe0e34 100644
--- a/spec/controllers/api/v1/accounts/lists_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/lists_controller_spec.rb
@@ -17,7 +17,7 @@ describe Api::V1::Accounts::ListsController do
   describe 'GET #index' do
     it 'returns http success' do
       get :index, params: { account_id: account.id }
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 end
diff --git a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
index e0de790c8..7e350da7e 100644
--- a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
@@ -25,7 +25,7 @@ describe Api::V1::Accounts::RelationshipsController do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'returns JSON with correct data' do
@@ -43,7 +43,7 @@ describe Api::V1::Accounts::RelationshipsController do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'returns JSON with correct data' do
diff --git a/spec/controllers/api/v1/accounts/search_controller_spec.rb b/spec/controllers/api/v1/accounts/search_controller_spec.rb
index 42cc3f64d..dbc4b9f3e 100644
--- a/spec/controllers/api/v1/accounts/search_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/search_controller_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Api::V1::Accounts::SearchController, type: :controller do
     it 'returns http success' do
       get :show, params: { q: 'query' }
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 end
diff --git a/spec/controllers/api/v1/accounts/statuses_controller_spec.rb b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb
index c49a77ac3..09bb46937 100644
--- a/spec/controllers/api/v1/accounts/statuses_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb
@@ -15,7 +15,7 @@ describe Api::V1::Accounts::StatusesController do
     it 'returns http success' do
       get :index, params: { account_id: user.account.id, limit: 1 }
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(response.headers['Link'].links.size).to eq(2)
     end
 
@@ -23,7 +23,7 @@ describe Api::V1::Accounts::StatusesController do
       it 'returns http success' do
         get :index, params: { account_id: user.account.id, only_media: true }
 
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
     end
 
@@ -35,7 +35,7 @@ describe Api::V1::Accounts::StatusesController do
       it 'returns http success' do
         get :index, params: { account_id: user.account.id, exclude_replies: true }
 
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
     end
 
@@ -47,7 +47,7 @@ describe Api::V1::Accounts::StatusesController do
       it 'returns http success' do
         get :index, params: { account_id: user.account.id, pinned: true }
 
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
     end
   end
diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb
index 053c53e5a..7a9e0f8e4 100644
--- a/spec/controllers/api/v1/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts_controller_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
   describe 'GET #show' do
     it 'returns http success' do
       get :show, params: { id: user.account.id }
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -28,7 +28,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
       let(:locked) { false }
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'returns JSON with following=true and requested=false' do
@@ -47,7 +47,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
       let(:locked) { true }
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'returns JSON with following=false and requested=true' do
@@ -72,7 +72,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'removes the following relation between user and target user' do
@@ -89,7 +89,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'removes the following relation between user and target user' do
@@ -110,7 +110,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'removes the blocking relation between user and target user' do
@@ -127,7 +127,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'does not remove the following relation between user and target user' do
@@ -152,7 +152,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'does not remove the following relation between user and target user' do
@@ -177,7 +177,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'removes the muting relation between user and target user' do
diff --git a/spec/controllers/api/v1/apps/credentials_controller_spec.rb b/spec/controllers/api/v1/apps/credentials_controller_spec.rb
index 38f2a4e10..0f811d5f3 100644
--- a/spec/controllers/api/v1/apps/credentials_controller_spec.rb
+++ b/spec/controllers/api/v1/apps/credentials_controller_spec.rb
@@ -16,7 +16,7 @@ describe Api::V1::Apps::CredentialsController do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'does not contain client credentials' do
diff --git a/spec/controllers/api/v1/apps_controller_spec.rb b/spec/controllers/api/v1/apps_controller_spec.rb
index 1ad9d6383..60a4c3b41 100644
--- a/spec/controllers/api/v1/apps_controller_spec.rb
+++ b/spec/controllers/api/v1/apps_controller_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Api::V1::AppsController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'creates an OAuth app' do
diff --git a/spec/controllers/api/v1/blocks_controller_spec.rb b/spec/controllers/api/v1/blocks_controller_spec.rb
index 9b2bbdf0e..eff5fb9da 100644
--- a/spec/controllers/api/v1/blocks_controller_spec.rb
+++ b/spec/controllers/api/v1/blocks_controller_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe Api::V1::BlocksController, type: :controller do
 
     it 'returns http success' do
       get :index
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 end
diff --git a/spec/controllers/api/v1/custom_emojis_controller_spec.rb b/spec/controllers/api/v1/custom_emojis_controller_spec.rb
index 9f3522812..fe8daa7c5 100644
--- a/spec/controllers/api/v1/custom_emojis_controller_spec.rb
+++ b/spec/controllers/api/v1/custom_emojis_controller_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Api::V1::CustomEmojisController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 end
diff --git a/spec/controllers/api/v1/domain_blocks_controller_spec.rb b/spec/controllers/api/v1/domain_blocks_controller_spec.rb
index 3713931dc..bae4612a2 100644
--- a/spec/controllers/api/v1/domain_blocks_controller_spec.rb
+++ b/spec/controllers/api/v1/domain_blocks_controller_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Api::V1::DomainBlocksController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'returns blocked domains' do
@@ -31,7 +31,7 @@ RSpec.describe Api::V1::DomainBlocksController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'creates a domain block' do
@@ -45,7 +45,7 @@ RSpec.describe Api::V1::DomainBlocksController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'deletes a domain block' do
diff --git a/spec/controllers/api/v1/follow_requests_controller_spec.rb b/spec/controllers/api/v1/follow_requests_controller_spec.rb
index 51df006a2..3c0b84af8 100644
--- a/spec/controllers/api/v1/follow_requests_controller_spec.rb
+++ b/spec/controllers/api/v1/follow_requests_controller_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -28,7 +28,7 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'allows follower to follow' do
@@ -42,7 +42,7 @@ RSpec.describe Api::V1::FollowRequestsController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'removes follow request' do
diff --git a/spec/controllers/api/v1/follows_controller_spec.rb b/spec/controllers/api/v1/follows_controller_spec.rb
index ea9e76d68..38badb80a 100644
--- a/spec/controllers/api/v1/follows_controller_spec.rb
+++ b/spec/controllers/api/v1/follows_controller_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Api::V1::FollowsController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'creates account for remote user' do
@@ -45,7 +45,7 @@ RSpec.describe Api::V1::FollowsController, type: :controller do
 
     it 'returns http success if already following, too' do
       post :create, params: { uri: 'gargron@quitter.no' }
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 end
diff --git a/spec/controllers/api/v1/instances_controller_spec.rb b/spec/controllers/api/v1/instances_controller_spec.rb
index eba233b05..7397d25d6 100644
--- a/spec/controllers/api/v1/instances_controller_spec.rb
+++ b/spec/controllers/api/v1/instances_controller_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Api::V1::InstancesController, type: :controller do
     it 'returns http success' do
       get :show
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 end
diff --git a/spec/controllers/api/v1/lists/accounts_controller_spec.rb b/spec/controllers/api/v1/lists/accounts_controller_spec.rb
index 953e5909d..c37a481d6 100644
--- a/spec/controllers/api/v1/lists/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/lists/accounts_controller_spec.rb
@@ -17,7 +17,7 @@ describe Api::V1::Lists::AccountsController do
     it 'returns http success' do
       get :show, params: { list_id: list.id }
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -30,7 +30,7 @@ describe Api::V1::Lists::AccountsController do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'adds account to the list' do
@@ -44,7 +44,7 @@ describe Api::V1::Lists::AccountsController do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'removes account from the list' do
diff --git a/spec/controllers/api/v1/lists_controller_spec.rb b/spec/controllers/api/v1/lists_controller_spec.rb
index be08c221f..213429581 100644
--- a/spec/controllers/api/v1/lists_controller_spec.rb
+++ b/spec/controllers/api/v1/lists_controller_spec.rb
@@ -12,14 +12,14 @@ RSpec.describe Api::V1::ListsController, type: :controller do
   describe 'GET #index' do
     it 'returns http success' do
       get :index
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
   describe 'GET #show' do
     it 'returns http success' do
       get :show, params: { id: list.id }
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -29,7 +29,7 @@ RSpec.describe Api::V1::ListsController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'creates list' do
@@ -44,7 +44,7 @@ RSpec.describe Api::V1::ListsController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'updates the list' do
@@ -58,7 +58,7 @@ RSpec.describe Api::V1::ListsController, type: :controller do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'deletes the list' do
diff --git a/spec/controllers/api/v1/media_controller_spec.rb b/spec/controllers/api/v1/media_controller_spec.rb
index 0e494638f..ce260eb90 100644
--- a/spec/controllers/api/v1/media_controller_spec.rb
+++ b/spec/controllers/api/v1/media_controller_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do
         end
 
         it 'returns http 422' do
-          expect(response).to have_http_status(:error)
+          expect(response).to have_http_status(500)
         end
       end
     end
@@ -41,7 +41,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'creates a media attachment' do
@@ -63,7 +63,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'creates a media attachment' do
@@ -85,7 +85,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do
       end
 
       xit 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       xit 'creates a media attachment' do
diff --git a/spec/controllers/api/v1/mutes_controller_spec.rb b/spec/controllers/api/v1/mutes_controller_spec.rb
index 7387b9d2d..6804c9395 100644
--- a/spec/controllers/api/v1/mutes_controller_spec.rb
+++ b/spec/controllers/api/v1/mutes_controller_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Api::V1::MutesController, type: :controller do
     it 'returns http success' do
       get :index, params: { limit: 1 }
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
diff --git a/spec/controllers/api/v1/notifications_controller_spec.rb b/spec/controllers/api/v1/notifications_controller_spec.rb
index f493d0d38..2e6163fcd 100644
--- a/spec/controllers/api/v1/notifications_controller_spec.rb
+++ b/spec/controllers/api/v1/notifications_controller_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do
       notification = Fabricate(:notification, account: user.account)
       get :show, params: { id: notification.id }
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -25,7 +25,7 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do
       notification = Fabricate(:notification, account: user.account)
       post :dismiss, params: { id: notification.id }
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound)
     end
   end
@@ -36,7 +36,7 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do
       post :clear
 
       expect(notification.account.reload.notifications).to be_empty
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -56,7 +56,7 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'includes reblog' do
@@ -82,7 +82,7 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'includes reblog' do
diff --git a/spec/controllers/api/v1/reports_controller_spec.rb b/spec/controllers/api/v1/reports_controller_spec.rb
index 1eb5a4353..1e1ef9308 100644
--- a/spec/controllers/api/v1/reports_controller_spec.rb
+++ b/spec/controllers/api/v1/reports_controller_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Api::V1::ReportsController, type: :controller do
     it 'returns http success' do
       get :index
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -31,7 +31,7 @@ RSpec.describe Api::V1::ReportsController, type: :controller do
 
     it 'creates a report' do
       expect(status.reload.account.targeted_reports).not_to be_empty
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'sends e-mails to admins' do
diff --git a/spec/controllers/api/v1/search_controller_spec.rb b/spec/controllers/api/v1/search_controller_spec.rb
index ff0c254b1..024703867 100644
--- a/spec/controllers/api/v1/search_controller_spec.rb
+++ b/spec/controllers/api/v1/search_controller_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Api::V1::SearchController, type: :controller do
     it 'returns http success' do
       get :index, params: { q: 'test' }
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 end
diff --git a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb b/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb
index 556731d57..c873e05dd 100644
--- a/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :control
 
       it 'returns http success' do
         get :index, params: { status_id: status.id, limit: 1 }
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
         expect(response.headers['Link'].links.size).to eq(2)
       end
     end
@@ -43,7 +43,7 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :control
 
         it 'returns http unautharized' do
           get :index, params: { status_id: status.id }
-          expect(response).to have_http_status(:missing)
+          expect(response).to have_http_status(404)
         end
       end
     end
@@ -58,7 +58,7 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :control
 
         it 'returns http success' do
           get :index, params: { status_id: status.id }
-          expect(response).to have_http_status(:success)
+          expect(response).to have_http_status(200)
         end
       end
     end
diff --git a/spec/controllers/api/v1/statuses/favourites_controller_spec.rb b/spec/controllers/api/v1/statuses/favourites_controller_spec.rb
index aba7cd458..53f602616 100644
--- a/spec/controllers/api/v1/statuses/favourites_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/favourites_controller_spec.rb
@@ -22,7 +22,7 @@ describe Api::V1::Statuses::FavouritesController do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'updates the favourites count' do
@@ -51,7 +51,7 @@ describe Api::V1::Statuses::FavouritesController do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'updates the favourites count' do
diff --git a/spec/controllers/api/v1/statuses/mutes_controller_spec.rb b/spec/controllers/api/v1/statuses/mutes_controller_spec.rb
index 54c594e92..13b4625d1 100644
--- a/spec/controllers/api/v1/statuses/mutes_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/mutes_controller_spec.rb
@@ -22,7 +22,7 @@ describe Api::V1::Statuses::MutesController do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'creates a conversation mute' do
@@ -39,7 +39,7 @@ describe Api::V1::Statuses::MutesController do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'destroys the conversation mute' do
diff --git a/spec/controllers/api/v1/statuses/pins_controller_spec.rb b/spec/controllers/api/v1/statuses/pins_controller_spec.rb
index 79005c9de..8f5b0800b 100644
--- a/spec/controllers/api/v1/statuses/pins_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/pins_controller_spec.rb
@@ -22,7 +22,7 @@ describe Api::V1::Statuses::PinsController do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'updates the pinned attribute' do
@@ -46,7 +46,7 @@ describe Api::V1::Statuses::PinsController do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'updates the pinned attribute' do
diff --git a/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb b/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb
index ba022a96e..9c0c2b60c 100644
--- a/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe Api::V1::Statuses::RebloggedByAccountsController, type: :controll
 
       it 'returns http success' do
         get :index, params: { status_id: status.id, limit: 1 }
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
         expect(response.headers['Link'].links.size).to eq(2)
       end
     end
@@ -42,7 +42,7 @@ RSpec.describe Api::V1::Statuses::RebloggedByAccountsController, type: :controll
 
         it 'returns http unautharized' do
           get :index, params: { status_id: status.id }
-          expect(response).to have_http_status(:missing)
+          expect(response).to have_http_status(404)
         end
       end
     end
@@ -57,7 +57,7 @@ RSpec.describe Api::V1::Statuses::RebloggedByAccountsController, type: :controll
 
         it 'returns http success' do
           get :index, params: { status_id: status.id }
-          expect(response).to have_http_status(:success)
+          expect(response).to have_http_status(200)
         end
       end
     end
diff --git a/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb b/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb
index 7417ff672..e60f8da2a 100644
--- a/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb
@@ -22,7 +22,7 @@ describe Api::V1::Statuses::ReblogsController do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'updates the reblogs count' do
@@ -51,7 +51,7 @@ describe Api::V1::Statuses::ReblogsController do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'updates the reblogs count' do
diff --git a/spec/controllers/api/v1/statuses_controller_spec.rb b/spec/controllers/api/v1/statuses_controller_spec.rb
index a36265395..27e4f4eb2 100644
--- a/spec/controllers/api/v1/statuses_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses_controller_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
 
       it 'returns http success' do
         get :show, params: { id: status.id }
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
     end
 
@@ -30,7 +30,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
 
       it 'returns http success' do
         get :context, params: { id: status.id }
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
     end
 
@@ -40,7 +40,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
     end
 
@@ -52,7 +52,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'removes the status' do
@@ -72,7 +72,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
       describe 'GET #show' do
         it 'returns http unautharized' do
           get :show, params: { id: status.id }
-          expect(response).to have_http_status(:missing)
+          expect(response).to have_http_status(404)
         end
       end
 
@@ -83,14 +83,14 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
 
         it 'returns http unautharized' do
           get :context, params: { id: status.id }
-          expect(response).to have_http_status(:missing)
+          expect(response).to have_http_status(404)
         end
       end
 
       describe 'GET #card' do
         it 'returns http unautharized' do
           get :card, params: { id: status.id }
-          expect(response).to have_http_status(:missing)
+          expect(response).to have_http_status(404)
         end
       end
     end
@@ -101,7 +101,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
       describe 'GET #show' do
         it 'returns http success' do
           get :show, params: { id: status.id }
-          expect(response).to have_http_status(:success)
+          expect(response).to have_http_status(200)
         end
       end
 
@@ -112,14 +112,14 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
 
         it 'returns http success' do
           get :context, params: { id: status.id }
-          expect(response).to have_http_status(:success)
+          expect(response).to have_http_status(200)
         end
       end
 
       describe 'GET #card' do
         it 'returns http success' do
           get :card, params: { id: status.id }
-          expect(response).to have_http_status(:success)
+          expect(response).to have_http_status(200)
         end
       end
     end
diff --git a/spec/controllers/api/v1/timelines/home_controller_spec.rb b/spec/controllers/api/v1/timelines/home_controller_spec.rb
index 4d4523520..85b031641 100644
--- a/spec/controllers/api/v1/timelines/home_controller_spec.rb
+++ b/spec/controllers/api/v1/timelines/home_controller_spec.rb
@@ -23,7 +23,7 @@ describe Api::V1::Timelines::HomeController do
       it 'returns http success' do
         get :show
 
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
         expect(response.headers['Link'].links.size).to eq(2)
       end
     end
diff --git a/spec/controllers/api/v1/timelines/list_controller_spec.rb b/spec/controllers/api/v1/timelines/list_controller_spec.rb
index 07eba955a..1729217c9 100644
--- a/spec/controllers/api/v1/timelines/list_controller_spec.rb
+++ b/spec/controllers/api/v1/timelines/list_controller_spec.rb
@@ -24,7 +24,7 @@ describe Api::V1::Timelines::ListController do
 
       it 'returns http success' do
         get :show, params: { id: list.id }
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
     end
   end
diff --git a/spec/controllers/api/v1/timelines/public_controller_spec.rb b/spec/controllers/api/v1/timelines/public_controller_spec.rb
index 3acf2e267..68d87bbcb 100644
--- a/spec/controllers/api/v1/timelines/public_controller_spec.rb
+++ b/spec/controllers/api/v1/timelines/public_controller_spec.rb
@@ -22,7 +22,7 @@ describe Api::V1::Timelines::PublicController do
       it 'returns http success' do
         get :show
 
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
         expect(response.headers['Link'].links.size).to eq(2)
       end
     end
@@ -35,7 +35,7 @@ describe Api::V1::Timelines::PublicController do
       it 'returns http success' do
         get :show, params: { local: true }
 
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
         expect(response.headers['Link'].links.size).to eq(2)
       end
     end
@@ -48,7 +48,7 @@ describe Api::V1::Timelines::PublicController do
       it 'returns http success' do
         get :show
 
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
         expect(response.headers['Link']).to be_nil
       end
     end
diff --git a/spec/controllers/api/v1/timelines/tag_controller_spec.rb b/spec/controllers/api/v1/timelines/tag_controller_spec.rb
index 6c66ee58e..472779f54 100644
--- a/spec/controllers/api/v1/timelines/tag_controller_spec.rb
+++ b/spec/controllers/api/v1/timelines/tag_controller_spec.rb
@@ -21,7 +21,7 @@ describe Api::V1::Timelines::TagController do
 
       it 'returns http success' do
         get :show, params: { id: 'test' }
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
         expect(response.headers['Link'].links.size).to eq(2)
       end
     end
@@ -33,7 +33,7 @@ describe Api::V1::Timelines::TagController do
     describe 'GET #show' do
       it 'returns http success' do
         get :show, params: { id: 'test' }
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
         expect(response.headers['Link']).to be_nil
       end
     end
diff --git a/spec/controllers/api/web/settings_controller_spec.rb b/spec/controllers/api/web/settings_controller_spec.rb
index ff211c7b1..815da04c4 100644
--- a/spec/controllers/api/web/settings_controller_spec.rb
+++ b/spec/controllers/api/web/settings_controller_spec.rb
@@ -13,7 +13,7 @@ describe Api::Web::SettingsController do
       patch :update, format: :json, params: { data: { 'onboarded' => true } }
 
       user.reload
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(user_web_setting.data['onboarded']).to eq('true')
     end
 
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 3e4d27e05..c6c78d3f7 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -51,7 +51,7 @@ describe ApplicationController, type: :controller do
     routes.draw { get 'success' => 'anonymous#success' }
     allow(Rails.env).to receive(:production?).and_return(false)
     get 'success'
-    expect(response).to have_http_status(:success)
+    expect(response).to have_http_status(200)
   end
 
   it "forces ssl if Rails.env.production? is 'true'" do
@@ -145,13 +145,13 @@ describe ApplicationController, type: :controller do
 
     it 'does nothing if not signed in' do
       get 'success'
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'does nothing if user who signed in is not suspended' do
       sign_in(Fabricate(:user, account: Fabricate(:account, suspended: false)))
       get 'success'
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     it 'returns http 403 if user who signed in is suspended' do
diff --git a/spec/controllers/auth/confirmations_controller_spec.rb b/spec/controllers/auth/confirmations_controller_spec.rb
index 80a06c43a..b3af5e0ec 100644
--- a/spec/controllers/auth/confirmations_controller_spec.rb
+++ b/spec/controllers/auth/confirmations_controller_spec.rb
@@ -7,7 +7,7 @@ describe Auth::ConfirmationsController, type: :controller do
     it 'returns http success' do
       @request.env['devise.mapping'] = Devise.mappings[:user]
       get :new
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
diff --git a/spec/controllers/auth/passwords_controller_spec.rb b/spec/controllers/auth/passwords_controller_spec.rb
index 992d2e29d..dcfdebb17 100644
--- a/spec/controllers/auth/passwords_controller_spec.rb
+++ b/spec/controllers/auth/passwords_controller_spec.rb
@@ -9,7 +9,7 @@ describe Auth::PasswordsController, type: :controller do
     it 'returns http success' do
       @request.env['devise.mapping'] = Devise.mappings[:user]
       get :new
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -24,7 +24,7 @@ describe Auth::PasswordsController, type: :controller do
     context 'with valid reset_password_token' do
       it 'returns http success' do
         get :edit, params: { reset_password_token: @token }
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
     end
 
diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb
index 97d2c53df..eeb01d5ad 100644
--- a/spec/controllers/auth/registrations_controller_spec.rb
+++ b/spec/controllers/auth/registrations_controller_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
       request.env["devise.mapping"] = Devise.mappings[:user]
       sign_in(Fabricate(:user))
       get :edit
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -44,7 +44,7 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
       request.env["devise.mapping"] = Devise.mappings[:user]
       sign_in(Fabricate(:user), scope: :user)
       post :update
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -63,7 +63,7 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
       it 'returns http success' do
         Setting.open_registrations = true
         get :new
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
     end
 
@@ -73,6 +73,12 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
   describe 'POST #create' do
     let(:accept_language) { Rails.application.config.i18n.available_locales.sample.to_s }
 
+    around do |example|
+      current_locale = I18n.locale
+      example.run
+      I18n.locale = current_locale
+    end
+
     before { request.env["devise.mapping"] = Devise.mappings[:user] }
 
     context do
diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb
index d5fed17d6..97719a606 100644
--- a/spec/controllers/auth/sessions_controller_spec.rb
+++ b/spec/controllers/auth/sessions_controller_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Auth::SessionsController, type: :controller do
 
     it 'returns http success' do
       get :new
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
diff --git a/spec/controllers/authorize_follows_controller_spec.rb b/spec/controllers/authorize_follows_controller_spec.rb
index b1cbef7ea..52971c724 100644
--- a/spec/controllers/authorize_follows_controller_spec.rb
+++ b/spec/controllers/authorize_follows_controller_spec.rb
@@ -47,7 +47,7 @@ describe AuthorizeFollowsController do
 
         get :show, params: { acct: 'http://example.com' }
 
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
         expect(assigns(:account)).to eq account
       end
 
@@ -59,7 +59,7 @@ describe AuthorizeFollowsController do
 
         get :show, params: { acct: 'acct:found@hostname' }
 
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
         expect(assigns(:account)).to eq account
       end
     end
diff --git a/spec/controllers/concerns/account_controller_concern_spec.rb b/spec/controllers/concerns/account_controller_concern_spec.rb
index ae46f9ba6..93685103f 100644
--- a/spec/controllers/concerns/account_controller_concern_spec.rb
+++ b/spec/controllers/concerns/account_controller_concern_spec.rb
@@ -39,7 +39,7 @@ describe ApplicationController, type: :controller do
     it 'returns http success' do
       account = Fabricate(:account)
       get 'success', params: { account_username: account.username }
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 end
diff --git a/spec/controllers/concerns/export_controller_concern_spec.rb b/spec/controllers/concerns/export_controller_concern_spec.rb
index 9d6f782b9..6a13db69d 100644
--- a/spec/controllers/concerns/export_controller_concern_spec.rb
+++ b/spec/controllers/concerns/export_controller_concern_spec.rb
@@ -19,7 +19,7 @@ describe ApplicationController, type: :controller do
       sign_in user
       get :index, format: :csv
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(response.content_type).to eq 'text/csv'
       expect(response.headers['Content-Disposition']).to eq 'attachment; filename="anonymous.csv"'
       expect(response.body).to eq user.account.username
diff --git a/spec/controllers/concerns/localized_spec.rb b/spec/controllers/concerns/localized_spec.rb
index f71c96aff..8c80b7d2a 100644
--- a/spec/controllers/concerns/localized_spec.rb
+++ b/spec/controllers/concerns/localized_spec.rb
@@ -11,13 +11,17 @@ describe ApplicationController, type: :controller do
     end
   end
 
+  around do |example|
+    current_locale = I18n.locale
+    example.run
+    I18n.locale = current_locale
+  end
+
   before do
     routes.draw { get 'success' => 'anonymous#success' }
   end
 
   shared_examples 'default locale' do
-    after { I18n.locale = I18n.default_locale }
-
     it 'sets available and preferred language' do
       request.headers['Accept-Language'] = 'ca-ES, fa'
       get 'success'
diff --git a/spec/controllers/follower_accounts_controller_spec.rb b/spec/controllers/follower_accounts_controller_spec.rb
index b9b7fef73..3a42a6e18 100644
--- a/spec/controllers/follower_accounts_controller_spec.rb
+++ b/spec/controllers/follower_accounts_controller_spec.rb
@@ -19,7 +19,7 @@ describe FollowerAccountsController do
       expect(assigned[0]).to eq follow1
       expect(assigned[1]).to eq follow0
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 end
diff --git a/spec/controllers/following_accounts_controller_spec.rb b/spec/controllers/following_accounts_controller_spec.rb
index 55e7265c7..33376365d 100644
--- a/spec/controllers/following_accounts_controller_spec.rb
+++ b/spec/controllers/following_accounts_controller_spec.rb
@@ -19,7 +19,7 @@ describe FollowingAccountsController do
       expect(assigned[0]).to eq follow1
       expect(assigned[1]).to eq follow0
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 end
diff --git a/spec/controllers/manifests_controller_spec.rb b/spec/controllers/manifests_controller_spec.rb
index 71967e4f0..a549adef3 100644
--- a/spec/controllers/manifests_controller_spec.rb
+++ b/spec/controllers/manifests_controller_spec.rb
@@ -9,7 +9,7 @@ describe ManifestsController do
     end
 
     it 'returns http success' do
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 end
diff --git a/spec/controllers/media_controller_spec.rb b/spec/controllers/media_controller_spec.rb
index 5b03899e4..ac44a76f2 100644
--- a/spec/controllers/media_controller_spec.rb
+++ b/spec/controllers/media_controller_spec.rb
@@ -18,13 +18,13 @@ describe MediaController do
       media_attachment = Fabricate(:media_attachment, status: nil)
       get :show, params: { id: media_attachment.to_param }
 
-      expect(response).to have_http_status(:missing)
+      expect(response).to have_http_status(404)
     end
 
     it 'raises when shortcode cant be found' do
       get :show, params: { id: 'missing' }
 
-      expect(response).to have_http_status(:missing)
+      expect(response).to have_http_status(404)
     end
 
     it 'raises when not permitted to view' do
@@ -33,7 +33,7 @@ describe MediaController do
       allow_any_instance_of(MediaController).to receive(:authorize).and_raise(ActiveRecord::RecordNotFound)
       get :show, params: { id: media_attachment.to_param }
 
-      expect(response).to have_http_status(:missing)
+      expect(response).to have_http_status(404)
     end
   end
 end
diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb
index 5c2a62b48..91c2d03ef 100644
--- a/spec/controllers/oauth/authorizations_controller_spec.rb
+++ b/spec/controllers/oauth/authorizations_controller_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Oauth::AuthorizationsController, type: :controller do
 
       it 'returns http success' do
         subject
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'gives options to authorize and deny' do
diff --git a/spec/controllers/oauth/authorized_applications_controller_spec.rb b/spec/controllers/oauth/authorized_applications_controller_spec.rb
index 2a2b92283..f967b507f 100644
--- a/spec/controllers/oauth/authorized_applications_controller_spec.rb
+++ b/spec/controllers/oauth/authorized_applications_controller_spec.rb
@@ -24,7 +24,7 @@ describe Oauth::AuthorizedApplicationsController do
 
       it 'returns http success' do
         subject
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       include_examples 'stores location for user'
diff --git a/spec/controllers/remote_follow_controller_spec.rb b/spec/controllers/remote_follow_controller_spec.rb
index 86b1eb8d0..5088c2e65 100644
--- a/spec/controllers/remote_follow_controller_spec.rb
+++ b/spec/controllers/remote_follow_controller_spec.rb
@@ -10,7 +10,7 @@ describe RemoteFollowController do
       account = Fabricate(:account)
       get :new, params: { account_username: account.to_param }
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(response).to render_template(:new)
       expect(assigns(:remote_follow).acct).to be_nil
     end
@@ -20,7 +20,7 @@ describe RemoteFollowController do
       account = Fabricate(:account)
       get :new, params: { account_username: account.to_param }
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(response).to render_template(:new)
       expect(assigns(:remote_follow).acct).to eq 'user@example.com'
     end
diff --git a/spec/controllers/settings/applications_controller_spec.rb b/spec/controllers/settings/applications_controller_spec.rb
index 90e6a63d5..f87107695 100644
--- a/spec/controllers/settings/applications_controller_spec.rb
+++ b/spec/controllers/settings/applications_controller_spec.rb
@@ -15,7 +15,7 @@ describe Settings::ApplicationsController do
 
     it 'shows apps' do
       get :index
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(assigns(:applications)).to include(app)
       expect(assigns(:applications)).to_not include(other_app)
     end
@@ -25,7 +25,7 @@ describe Settings::ApplicationsController do
   describe 'GET #show' do
     it 'returns http success' do
       get :show, params: { id: app.id }
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(assigns[:application]).to eql(app)
     end
 
@@ -40,7 +40,7 @@ describe Settings::ApplicationsController do
   describe 'GET #new' do
     it 'works' do
       get :new
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
@@ -102,7 +102,7 @@ describe Settings::ApplicationsController do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'renders form again' do
@@ -151,7 +151,7 @@ describe Settings::ApplicationsController do
       end
 
       it 'returns http success' do
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'renders form again' do
diff --git a/spec/controllers/settings/deletes_controller_spec.rb b/spec/controllers/settings/deletes_controller_spec.rb
index 9b55090df..35fd64e9b 100644
--- a/spec/controllers/settings/deletes_controller_spec.rb
+++ b/spec/controllers/settings/deletes_controller_spec.rb
@@ -13,7 +13,7 @@ describe Settings::DeletesController do
 
       it 'renders confirmation page' do
         get :show
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
     end
 
diff --git a/spec/controllers/settings/exports_controller_spec.rb b/spec/controllers/settings/exports_controller_spec.rb
index 19cb0abda..b7cab4d8f 100644
--- a/spec/controllers/settings/exports_controller_spec.rb
+++ b/spec/controllers/settings/exports_controller_spec.rb
@@ -17,7 +17,7 @@ describe Settings::ExportsController do
         export = assigns(:export)
         expect(export).to be_instance_of Export
         expect(export.account).to eq user.account
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
     end
 
diff --git a/spec/controllers/settings/follower_domains_controller_spec.rb b/spec/controllers/settings/follower_domains_controller_spec.rb
index 333223c61..6d415a654 100644
--- a/spec/controllers/settings/follower_domains_controller_spec.rb
+++ b/spec/controllers/settings/follower_domains_controller_spec.rb
@@ -36,7 +36,7 @@ describe Settings::FollowerDomainsController do
     it 'returns http success' do
       sign_in user, scope: :user
       subject
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
 
     include_examples 'authenticate user'
diff --git a/spec/controllers/settings/imports_controller_spec.rb b/spec/controllers/settings/imports_controller_spec.rb
index 59b10e0da..7a9b02195 100644
--- a/spec/controllers/settings/imports_controller_spec.rb
+++ b/spec/controllers/settings/imports_controller_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Settings::ImportsController, type: :controller do
   describe "GET #show" do
     it "returns http success" do
       get :show
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
diff --git a/spec/controllers/settings/notifications_controller_spec.rb b/spec/controllers/settings/notifications_controller_spec.rb
index 0bd993448..981ef674e 100644
--- a/spec/controllers/settings/notifications_controller_spec.rb
+++ b/spec/controllers/settings/notifications_controller_spec.rb
@@ -12,7 +12,7 @@ describe Settings::NotificationsController do
   describe 'GET #show' do
     it 'returns http success' do
       get :show
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
diff --git a/spec/controllers/settings/preferences_controller_spec.rb b/spec/controllers/settings/preferences_controller_spec.rb
index 0f9431673..7877c7362 100644
--- a/spec/controllers/settings/preferences_controller_spec.rb
+++ b/spec/controllers/settings/preferences_controller_spec.rb
@@ -12,7 +12,7 @@ describe Settings::PreferencesController do
   describe 'GET #show' do
     it 'returns http success' do
       get :show
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
diff --git a/spec/controllers/settings/profiles_controller_spec.rb b/spec/controllers/settings/profiles_controller_spec.rb
index ee3315be6..a453200af 100644
--- a/spec/controllers/settings/profiles_controller_spec.rb
+++ b/spec/controllers/settings/profiles_controller_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Settings::ProfilesController, type: :controller do
   describe "GET #show" do
     it "returns http success" do
       get :show
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
diff --git a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
index aee82a3d8..7612bf90e 100644
--- a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
+++ b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
@@ -15,7 +15,7 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
       expect(assigns(:confirmation)).to be_instance_of Form::TwoFactorConfirmation
       expect(assigns(:provision_url)).to eq 'otpauth://totp/local-part@domain?secret=thisisasecretforthespecofnewview&issuer=cb6e6126.ngrok.io'
       expect(assigns(:qrcode)).to be_instance_of RQRCode::QRCode
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(response).to render_template(:new)
     end
   end
@@ -71,7 +71,7 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
 
           expect(assigns(:recovery_codes)).to eq otp_backup_codes
           expect(flash[:notice]).to eq 'Two-factor authentication successfully enabled'
-          expect(response).to have_http_status(:success)
+          expect(response).to have_http_status(200)
           expect(response).to render_template('settings/two_factor_authentication/recovery_codes/index')
         end
       end
diff --git a/spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb b/spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb
index aa28cdf3f..c04760e53 100644
--- a/spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb
+++ b/spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb
@@ -19,7 +19,7 @@ describe Settings::TwoFactorAuthentication::RecoveryCodesController do
 
       expect(assigns(:recovery_codes)).to eq otp_backup_codes
       expect(flash[:notice]).to eq 'Recovery codes successfully regenerated'
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(response).to render_template(:index)
     end
 
diff --git a/spec/controllers/settings/two_factor_authentications_controller_spec.rb b/spec/controllers/settings/two_factor_authentications_controller_spec.rb
index 6c49f6f0d..9f27222ad 100644
--- a/spec/controllers/settings/two_factor_authentications_controller_spec.rb
+++ b/spec/controllers/settings/two_factor_authentications_controller_spec.rb
@@ -18,7 +18,7 @@ describe Settings::TwoFactorAuthenticationsController do
           user.update(otp_required_for_login: true)
           get :show
 
-          expect(response).to have_http_status(:success)
+          expect(response).to have_http_status(200)
         end
       end
 
@@ -27,7 +27,7 @@ describe Settings::TwoFactorAuthenticationsController do
           user.update(otp_required_for_login: false)
           get :show
 
-          expect(response).to have_http_status(:success)
+          expect(response).to have_http_status(200)
         end
       end
     end
diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb
index 95fb4d594..b4f3c5a08 100644
--- a/spec/controllers/statuses_controller_spec.rb
+++ b/spec/controllers/statuses_controller_spec.rb
@@ -82,10 +82,53 @@ describe StatusesController do
         expect(assigns(:ancestors)).to eq []
       end
 
+      it 'assigns @descendant_threads for a thread with several statuses' do
+        status = Fabricate(:status)
+        child = Fabricate(:status, in_reply_to_id: status.id)
+        grandchild = Fabricate(:status, in_reply_to_id: child.id)
+
+        get :show, params: { account_username: status.account.username, id: status.id }
+
+        expect(assigns(:descendant_threads)[0][:statuses].pluck(:id)).to eq [child.id, grandchild.id]
+      end
+
+      it 'assigns @descendant_threads for several threads sharing the same descendant' do
+        status = Fabricate(:status)
+        child = Fabricate(:status, in_reply_to_id: status.id)
+        grandchildren = 2.times.map { Fabricate(:status, in_reply_to_id: child.id) }
+
+        get :show, params: { account_username: status.account.username, id: status.id }
+
+        expect(assigns(:descendant_threads)[0][:statuses].pluck(:id)).to eq [child.id, grandchildren[0].id]
+        expect(assigns(:descendant_threads)[1][:statuses].pluck(:id)).to eq [grandchildren[1].id]
+      end
+
+      it 'assigns @max_descendant_thread_id for the last thread if it is hitting the status limit' do
+        stub_const 'StatusesController::DESCENDANTS_LIMIT', 1
+        status = Fabricate(:status)
+        child = Fabricate(:status, in_reply_to_id: status.id)
+
+        get :show, params: { account_username: status.account.username, id: status.id }
+
+        expect(assigns(:descendant_threads)).to eq []
+        expect(assigns(:max_descendant_thread_id)).to eq child.id
+      end
+
+      it 'assigns @descendant_threads for threads with :next_status key if they are hitting the depth limit' do
+        stub_const 'StatusesController::DESCENDANTS_DEPTH_LIMIT', 1
+        status = Fabricate(:status)
+        child = Fabricate(:status, in_reply_to_id: status.id)
+
+        get :show, params: { account_username: status.account.username, id: status.id }
+
+        expect(assigns(:descendant_threads)[0][:statuses].pluck(:id)).not_to include child.id
+        expect(assigns(:descendant_threads)[0][:next_status].id).to eq child.id
+      end
+
       it 'returns a success' do
         status = Fabricate(:status)
         get :show, params: { account_username: status.account.username, id: status.id }
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'renders stream_entries/show' do
diff --git a/spec/controllers/stream_entries_controller_spec.rb b/spec/controllers/stream_entries_controller_spec.rb
index 665c5b747..534bc393d 100644
--- a/spec/controllers/stream_entries_controller_spec.rb
+++ b/spec/controllers/stream_entries_controller_spec.rb
@@ -77,7 +77,7 @@ RSpec.describe StreamEntriesController, type: :controller do
     it 'returns http success with Atom' do
       status = Fabricate(:status)
       get :show, params: { account_username: status.account.username, id: status.stream_entry.id }, format: 'atom'
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
     end
   end
 
diff --git a/spec/controllers/tags_controller_spec.rb b/spec/controllers/tags_controller_spec.rb
index b04666c0f..33ccaed61 100644
--- a/spec/controllers/tags_controller_spec.rb
+++ b/spec/controllers/tags_controller_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe TagsController, type: :controller do
     context 'when tag exists' do
       it 'returns http success' do
         get :show, params: { id: 'test', max_id: late.id }
-        expect(response).to have_http_status(:success)
+        expect(response).to have_http_status(200)
       end
 
       it 'renders application layout' do
@@ -25,7 +25,7 @@ RSpec.describe TagsController, type: :controller do
       it 'returns http missing for non-existent tag' do
         get :show, params: { id: 'none' }
 
-        expect(response).to have_http_status(:missing)
+        expect(response).to have_http_status(404)
       end
     end
   end
diff --git a/spec/controllers/well_known/host_meta_controller_spec.rb b/spec/controllers/well_known/host_meta_controller_spec.rb
index 87c1485ed..b43ae19d8 100644
--- a/spec/controllers/well_known/host_meta_controller_spec.rb
+++ b/spec/controllers/well_known/host_meta_controller_spec.rb
@@ -7,10 +7,10 @@ describe WellKnown::HostMetaController, type: :controller do
     it 'returns http success' do
       get :show, format: :xml
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(response.content_type).to eq 'application/xrd+xml'
       expect(response.body).to eq <<XML
-<?xml version="1.0"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
   <Link rel="lrdd" type="application/xrd+xml" template="https://cb6e6126.ngrok.io/.well-known/webfinger?resource={uri}"/>
 </XRD>
diff --git a/spec/controllers/well_known/webfinger_controller_spec.rb b/spec/controllers/well_known/webfinger_controller_spec.rb
index 466f87c45..b05745ea3 100644
--- a/spec/controllers/well_known/webfinger_controller_spec.rb
+++ b/spec/controllers/well_known/webfinger_controller_spec.rb
@@ -50,7 +50,7 @@ PEM
 
       json = body_as_json
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(response.content_type).to eq 'application/jrd+json'
       expect(json[:subject]).to eq 'acct:alice@cb6e6126.ngrok.io'
       expect(json[:aliases]).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice')
@@ -61,7 +61,7 @@ PEM
 
       xml = Nokogiri::XML(response.body)
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(response.content_type).to eq 'application/xrd+xml'
       expect(xml.at_xpath('//xmlns:Subject').content).to eq 'acct:alice@cb6e6126.ngrok.io'
       expect(xml.xpath('//xmlns:Alias').map(&:content)).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice')
@@ -81,7 +81,7 @@ PEM
 
       json = body_as_json
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(response.content_type).to eq 'application/jrd+json'
       expect(json[:subject]).to eq 'acct:alice@cb6e6126.ngrok.io'
       expect(json[:aliases]).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice')
diff --git a/spec/fabricators/account_fabricator.rb b/spec/fabricators/account_fabricator.rb
index 446f8ea27..7aa983f82 100644
--- a/spec/fabricators/account_fabricator.rb
+++ b/spec/fabricators/account_fabricator.rb
@@ -1,4 +1,10 @@
+keypair     = OpenSSL::PKey::RSA.new(2048)
+public_key  = keypair.public_key.to_pem
+private_key = keypair.to_pem
+
 Fabricator(:account) do
-  username { sequence(:username) { |i| "#{Faker::Internet.user_name(nil, %w(_))}#{i}" } }
+  username            { sequence(:username) { |i| "#{Faker::Internet.user_name(nil, %w(_))}#{i}" } }
   last_webfingered_at { Time.now.utc }
+  public_key          { public_key }
+  private_key         { private_key}
 end
diff --git a/spec/fixtures/requests/activitypub-actor-individual.txt b/spec/fixtures/requests/activitypub-actor-individual.txt
new file mode 100644
index 000000000..74411e544
--- /dev/null
+++ b/spec/fixtures/requests/activitypub-actor-individual.txt
@@ -0,0 +1,9 @@
+HTTP/1.1 200 OK
+Cache-Control: max-age=0, private, must-revalidate
+Content-Type: application/activity+json; charset=utf-8
+Link: <https://ap.example.com/.well-known/webfinger?resource=acct%3Afoo%40ap.example.com>; rel="lrdd"; type="application/xrd+xml", <https://ap.example.com/users/foo.atom>; rel="alternate"; type="application/atom+xml", <https://ap.example.com/users/foo>; rel="alternate"; type="application/activity+json"
+Vary: Accept-Encoding
+X-Content-Type-Options: nosniff
+X-Xss-Protection: 1; mode=block
+
+{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"vcard": "http://www.w3.org/2006/vcard/ns#"},{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation"}],"id":"https://ap.example.com/users/foo","type":["Person","vcard:individual"],"following":"https://ap.example.com/users/foo/following","followers":"https://ap.example.com/users/foo/followers","inbox":"https://ap.example.com/users/foo/inbox","outbox":"https://ap.example.com/users/foo/outbox","preferredUsername":"foo","vcard:fn":"foo","name":"","summary":"\u003cp\u003etest\u003c/p\u003e","url":"https://ap.example.com/@foo","manuallyApprovesFollowers":false,"publicKey":{"id":"https://ap.example.com/users/foo#main-key","owner":"https://ap.example.com/users/foo","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu3L4vnpNLzVH31MeWI39\n4F0wKeJFsLDAsNXGeOu0QF2x+h1zLWZw/agqD2R3JPU9/kaDJGPIV2Sn5zLyUA9S\n6swCCMOtn7BBR9g9sucgXJmUFB0tACH2QSgHywMAybGfmSb3LsEMNKsGJ9VsvYoh\n8lDET6X4Pyw+ZJU0/OLo/41q9w+OrGtlsTm/PuPIeXnxa6BLqnDaxC+4IcjG/FiP\nahNCTINl/1F/TgSSDZ4Taf4U9XFEIFw8wmgploELozzIzKq+t8nhQYkgAkt64euW\npva3qL5KD1mTIZQEP+LZvh3s2WHrLi3fhbdRuwQ2c0KkJA2oSTFPDpqqbPGZ3Qvu\nHQIDAQAB\n-----END PUBLIC KEY-----\n"},"endpoints":{"sharedInbox":"https://ap.example.com/inbox"},"icon":{"type":"Image","url":"https://quitter.no/avatar/7477-300-20160211190340.png"}}
\ No newline at end of file
diff --git a/spec/fixtures/requests/json-ld.activitystreams.txt b/spec/fixtures/requests/json-ld.activitystreams.txt
new file mode 100644
index 000000000..395797b27
--- /dev/null
+++ b/spec/fixtures/requests/json-ld.activitystreams.txt
@@ -0,0 +1,391 @@
+HTTP/1.1 200 OK

+Date: Tue, 01 May 2018 23:25:57 GMT

+Content-Location: activitystreams.jsonld

+Vary: negotiate,accept

+TCN: choice

+Last-Modified: Mon, 16 Apr 2018 00:28:23 GMT

+ETag: "1eb0-569ec4caa97c0;d3-540ee27e0eec0"

+Accept-Ranges: bytes

+Content-Length: 7856

+Cache-Control: max-age=21600

+Expires: Wed, 02 May 2018 05:25:57 GMT

+P3P: policyref="http://www.w3.org/2014/08/p3p.xml"

+Access-Control-Allow-Origin: *

+Content-Type: application/ld+json

+Strict-Transport-Security: max-age=15552000; includeSubdomains; preload

+Content-Security-Policy: upgrade-insecure-requests

+

+{
+  "@context": {
+    "@vocab": "_:",
+    "xsd": "http://www.w3.org/2001/XMLSchema#",
+    "as": "https://www.w3.org/ns/activitystreams#",
+    "ldp": "http://www.w3.org/ns/ldp#",
+    "id": "@id",
+    "type": "@type",
+    "Accept": "as:Accept",
+    "Activity": "as:Activity",
+    "IntransitiveActivity": "as:IntransitiveActivity",
+    "Add": "as:Add",
+    "Announce": "as:Announce",
+    "Application": "as:Application",
+    "Arrive": "as:Arrive",
+    "Article": "as:Article",
+    "Audio": "as:Audio",
+    "Block": "as:Block",
+    "Collection": "as:Collection",
+    "CollectionPage": "as:CollectionPage",
+    "Relationship": "as:Relationship",
+    "Create": "as:Create",
+    "Delete": "as:Delete",
+    "Dislike": "as:Dislike",
+    "Document": "as:Document",
+    "Event": "as:Event",
+    "Follow": "as:Follow",
+    "Flag": "as:Flag",
+    "Group": "as:Group",
+    "Ignore": "as:Ignore",
+    "Image": "as:Image",
+    "Invite": "as:Invite",
+    "Join": "as:Join",
+    "Leave": "as:Leave",
+    "Like": "as:Like",
+    "Link": "as:Link",
+    "Mention": "as:Mention",
+    "Note": "as:Note",
+    "Object": "as:Object",
+    "Offer": "as:Offer",
+    "OrderedCollection": "as:OrderedCollection",
+    "OrderedCollectionPage": "as:OrderedCollectionPage",
+    "Organization": "as:Organization",
+    "Page": "as:Page",
+    "Person": "as:Person",
+    "Place": "as:Place",
+    "Profile": "as:Profile",
+    "Question": "as:Question",
+    "Reject": "as:Reject",
+    "Remove": "as:Remove",
+    "Service": "as:Service",
+    "TentativeAccept": "as:TentativeAccept",
+    "TentativeReject": "as:TentativeReject",
+    "Tombstone": "as:Tombstone",
+    "Undo": "as:Undo",
+    "Update": "as:Update",
+    "Video": "as:Video",
+    "View": "as:View",
+    "Listen": "as:Listen",
+    "Read": "as:Read",
+    "Move": "as:Move",
+    "Travel": "as:Travel",
+    "IsFollowing": "as:IsFollowing",
+    "IsFollowedBy": "as:IsFollowedBy",
+    "IsContact": "as:IsContact",
+    "IsMember": "as:IsMember",
+    "subject": {
+      "@id": "as:subject",
+      "@type": "@id"
+    },
+    "relationship": {
+      "@id": "as:relationship",
+      "@type": "@id"
+    },
+    "actor": {
+      "@id": "as:actor",
+      "@type": "@id"
+    },
+    "attributedTo": {
+      "@id": "as:attributedTo",
+      "@type": "@id"
+    },
+    "attachment": {
+      "@id": "as:attachment",
+      "@type": "@id"
+    },
+    "bcc": {
+      "@id": "as:bcc",
+      "@type": "@id"
+    },
+    "bto": {
+      "@id": "as:bto",
+      "@type": "@id"
+    },
+    "cc": {
+      "@id": "as:cc",
+      "@type": "@id"
+    },
+    "context": {
+      "@id": "as:context",
+      "@type": "@id"
+    },
+    "current": {
+      "@id": "as:current",
+      "@type": "@id"
+    },
+    "first": {
+      "@id": "as:first",
+      "@type": "@id"
+    },
+    "generator": {
+      "@id": "as:generator",
+      "@type": "@id"
+    },
+    "icon": {
+      "@id": "as:icon",
+      "@type": "@id"
+    },
+    "image": {
+      "@id": "as:image",
+      "@type": "@id"
+    },
+    "inReplyTo": {
+      "@id": "as:inReplyTo",
+      "@type": "@id"
+    },
+    "items": {
+      "@id": "as:items",
+      "@type": "@id"
+    },
+    "instrument": {
+      "@id": "as:instrument",
+      "@type": "@id"
+    },
+    "orderedItems": {
+      "@id": "as:items",
+      "@type": "@id",
+      "@container": "@list"
+    },
+    "last": {
+      "@id": "as:last",
+      "@type": "@id"
+    },
+    "location": {
+      "@id": "as:location",
+      "@type": "@id"
+    },
+    "next": {
+      "@id": "as:next",
+      "@type": "@id"
+    },
+    "object": {
+      "@id": "as:object",
+      "@type": "@id"
+    },
+    "oneOf": {
+      "@id": "as:oneOf",
+      "@type": "@id"
+    },
+    "anyOf": {
+      "@id": "as:anyOf",
+      "@type": "@id"
+    },
+    "closed": {
+      "@id": "as:closed",
+      "@type": "xsd:dateTime"
+    },
+    "origin": {
+      "@id": "as:origin",
+      "@type": "@id"
+    },
+    "accuracy": {
+      "@id": "as:accuracy",
+      "@type": "xsd:float"
+    },
+    "prev": {
+      "@id": "as:prev",
+      "@type": "@id"
+    },
+    "preview": {
+      "@id": "as:preview",
+      "@type": "@id"
+    },
+    "replies": {
+      "@id": "as:replies",
+      "@type": "@id"
+    },
+    "result": {
+      "@id": "as:result",
+      "@type": "@id"
+    },
+    "audience": {
+      "@id": "as:audience",
+      "@type": "@id"
+    },
+    "partOf": {
+      "@id": "as:partOf",
+      "@type": "@id"
+    },
+    "tag": {
+      "@id": "as:tag",
+      "@type": "@id"
+    },
+    "target": {
+      "@id": "as:target",
+      "@type": "@id"
+    },
+    "to": {
+      "@id": "as:to",
+      "@type": "@id"
+    },
+    "url": {
+      "@id": "as:url",
+      "@type": "@id"
+    },
+    "altitude": {
+      "@id": "as:altitude",
+      "@type": "xsd:float"
+    },
+    "content": "as:content",
+    "contentMap": {
+      "@id": "as:content",
+      "@container": "@language"
+    },
+    "name": "as:name",
+    "nameMap": {
+      "@id": "as:name",
+      "@container": "@language"
+    },
+    "duration": {
+      "@id": "as:duration",
+      "@type": "xsd:duration"
+    },
+    "endTime": {
+      "@id": "as:endTime",
+      "@type": "xsd:dateTime"
+    },
+    "height": {
+      "@id": "as:height",
+      "@type": "xsd:nonNegativeInteger"
+    },
+    "href": {
+      "@id": "as:href",
+      "@type": "@id"
+    },
+    "hreflang": "as:hreflang",
+    "latitude": {
+      "@id": "as:latitude",
+      "@type": "xsd:float"
+    },
+    "longitude": {
+      "@id": "as:longitude",
+      "@type": "xsd:float"
+    },
+    "mediaType": "as:mediaType",
+    "published": {
+      "@id": "as:published",
+      "@type": "xsd:dateTime"
+    },
+    "radius": {
+      "@id": "as:radius",
+      "@type": "xsd:float"
+    },
+    "rel": "as:rel",
+    "startIndex": {
+      "@id": "as:startIndex",
+      "@type": "xsd:nonNegativeInteger"
+    },
+    "startTime": {
+      "@id": "as:startTime",
+      "@type": "xsd:dateTime"
+    },
+    "summary": "as:summary",
+    "summaryMap": {
+      "@id": "as:summary",
+      "@container": "@language"
+    },
+    "totalItems": {
+      "@id": "as:totalItems",
+      "@type": "xsd:nonNegativeInteger"
+    },
+    "units": "as:units",
+    "updated": {
+      "@id": "as:updated",
+      "@type": "xsd:dateTime"
+    },
+    "width": {
+      "@id": "as:width",
+      "@type": "xsd:nonNegativeInteger"
+    },
+    "describes": {
+      "@id": "as:describes",
+      "@type": "@id"
+    },
+    "formerType": {
+      "@id": "as:formerType",
+      "@type": "@id"
+    },
+    "deleted": {
+      "@id": "as:deleted",
+      "@type": "xsd:dateTime"
+    },
+    "inbox": {
+      "@id": "ldp:inbox",
+      "@type": "@id"
+    },
+    "outbox": {
+      "@id": "as:outbox",
+      "@type": "@id"
+    },
+    "following": {
+      "@id": "as:following",
+      "@type": "@id"
+    },
+    "followers": {
+      "@id": "as:followers",
+      "@type": "@id"
+    },
+    "streams": {
+      "@id": "as:streams",
+      "@type": "@id"
+    },
+    "preferredUsername": "as:preferredUsername",
+    "endpoints": {
+      "@id": "as:endpoints",
+      "@type": "@id"
+    },
+    "uploadMedia": {
+      "@id": "as:uploadMedia",
+      "@type": "@id"
+    },
+    "proxyUrl": {
+      "@id": "as:proxyUrl",
+      "@type": "@id"
+    },
+    "liked": {
+      "@id": "as:liked",
+      "@type": "@id"
+    },
+    "oauthAuthorizationEndpoint": {
+      "@id": "as:oauthAuthorizationEndpoint",
+      "@type": "@id"
+    },
+    "oauthTokenEndpoint": {
+      "@id": "as:oauthTokenEndpoint",
+      "@type": "@id"
+    },
+    "provideClientKey": {
+      "@id": "as:provideClientKey",
+      "@type": "@id"
+    },
+    "signClientKey": {
+      "@id": "as:signClientKey",
+      "@type": "@id"
+    },
+    "sharedInbox": {
+      "@id": "as:sharedInbox",
+      "@type": "@id"
+    },
+    "Public": {
+      "@id": "as:Public",
+      "@type": "@id"
+    },
+    "source": "as:source",
+    "likes": {
+      "@id": "as:likes",
+      "@type": "@id"
+    },
+    "shares": {
+      "@id": "as:shares",
+      "@type": "@id"
+    }
+  }
+}
diff --git a/spec/fixtures/requests/json-ld.identity.txt b/spec/fixtures/requests/json-ld.identity.txt
new file mode 100644
index 000000000..8810526cb
--- /dev/null
+++ b/spec/fixtures/requests/json-ld.identity.txt
@@ -0,0 +1,100 @@
+HTTP/1.1 200 OK

+Accept-Ranges: bytes

+Access-Control-Allow-Headers: DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Accept-Encoding

+Access-Control-Allow-Origin: *

+Content-Type: application/ld+json

+Date: Tue, 01 May 2018 23:28:21 GMT

+Etag: "e26-547a6fc75b04a-gzip"

+Last-Modified: Fri, 03 Feb 2017 21:30:09 GMT

+Server: Apache/2.4.7 (Ubuntu)

+Vary: Accept-Encoding

+Transfer-Encoding: chunked

+

+{
+  "@context": {
+    "id": "@id",
+    "type": "@type",
+
+    "cred": "https://w3id.org/credentials#",
+    "dc": "http://purl.org/dc/terms/",
+    "identity": "https://w3id.org/identity#",
+    "perm": "https://w3id.org/permissions#",
+    "ps": "https://w3id.org/payswarm#",
+    "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+    "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
+    "sec": "https://w3id.org/security#",
+    "schema": "http://schema.org/",
+    "xsd": "http://www.w3.org/2001/XMLSchema#",
+
+    "Group": "https://www.w3.org/ns/activitystreams#Group",
+
+    "claim": {"@id": "cred:claim", "@type": "@id"},
+    "credential": {"@id": "cred:credential", "@type": "@id"},
+    "issued": {"@id": "cred:issued", "@type": "xsd:dateTime"},
+    "issuer": {"@id": "cred:issuer", "@type": "@id"},
+    "recipient": {"@id": "cred:recipient", "@type": "@id"},
+    "Credential": "cred:Credential",
+    "CryptographicKeyCredential": "cred:CryptographicKeyCredential",
+
+    "about": {"@id": "schema:about", "@type": "@id"},
+    "address": {"@id": "schema:address", "@type": "@id"},
+    "addressCountry": "schema:addressCountry",
+    "addressLocality": "schema:addressLocality",
+    "addressRegion": "schema:addressRegion",
+    "comment": "rdfs:comment",
+    "created": {"@id": "dc:created", "@type": "xsd:dateTime"},
+    "creator": {"@id": "dc:creator", "@type": "@id"},
+    "description": "schema:description",
+    "email": "schema:email",
+    "familyName": "schema:familyName",
+    "givenName": "schema:givenName",
+    "image": {"@id": "schema:image", "@type": "@id"},
+    "label": "rdfs:label",
+    "name": "schema:name",
+    "postalCode": "schema:postalCode",
+    "streetAddress": "schema:streetAddress",
+    "title": "dc:title",
+    "url": {"@id": "schema:url", "@type": "@id"},
+    "Person": "schema:Person",
+    "PostalAddress": "schema:PostalAddress",
+    "Organization": "schema:Organization",
+
+    "identityService": {"@id": "identity:identityService", "@type": "@id"},
+    "idp": {"@id": "identity:idp", "@type": "@id"},
+    "Identity": "identity:Identity",
+
+    "paymentProcessor": "ps:processor",
+    "preferences": {"@id": "ps:preferences", "@type": "@vocab"},
+
+    "cipherAlgorithm": "sec:cipherAlgorithm",
+    "cipherData": "sec:cipherData",
+    "cipherKey": "sec:cipherKey",
+    "digestAlgorithm": "sec:digestAlgorithm",
+    "digestValue": "sec:digestValue",
+    "domain": "sec:domain",
+    "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"},
+    "initializationVector": "sec:initializationVector",
+    "member": {"@id": "schema:member", "@type": "@id"},
+    "memberOf": {"@id": "schema:memberOf", "@type": "@id"},
+    "nonce": "sec:nonce",
+    "normalizationAlgorithm": "sec:normalizationAlgorithm",
+    "owner": {"@id": "sec:owner", "@type": "@id"},
+    "password": "sec:password",
+    "privateKey": {"@id": "sec:privateKey", "@type": "@id"},
+    "privateKeyPem": "sec:privateKeyPem",
+    "publicKey": {"@id": "sec:publicKey", "@type": "@id"},
+    "publicKeyPem": "sec:publicKeyPem",
+    "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"},
+    "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"},
+    "signature": "sec:signature",
+    "signatureAlgorithm": "sec:signatureAlgorithm",
+    "signatureValue": "sec:signatureValue",
+    "CryptographicKey": "sec:Key",
+    "EncryptedMessage": "sec:EncryptedMessage",
+    "GraphSignature2012": "sec:GraphSignature2012",
+    "LinkedDataSignature2015": "sec:LinkedDataSignature2015",
+
+    "accessControl": {"@id": "perm:accessControl", "@type": "@id"},
+    "writePermission": {"@id": "perm:writePermission", "@type": "@id"}
+  }
+}
diff --git a/spec/fixtures/requests/json-ld.security.txt b/spec/fixtures/requests/json-ld.security.txt
new file mode 100644
index 000000000..0d29903e6
--- /dev/null
+++ b/spec/fixtures/requests/json-ld.security.txt
@@ -0,0 +1,61 @@
+HTTP/1.1 200 OK

+Accept-Ranges: bytes

+Access-Control-Allow-Headers: DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Accept-Encoding

+Access-Control-Allow-Origin: *

+Content-Type: application/ld+json

+Date: Wed, 02 May 2018 16:25:32 GMT

+Etag: "7e3-5651ec0f7c5ed-gzip"

+Last-Modified: Tue, 13 Feb 2018 21:34:04 GMT

+Server: Apache/2.4.7 (Ubuntu)

+Vary: Accept-Encoding

+Content-Length: 2019

+

+{
+  "@context": {
+    "id": "@id",
+    "type": "@type",
+
+    "dc": "http://purl.org/dc/terms/",
+    "sec": "https://w3id.org/security#",
+    "xsd": "http://www.w3.org/2001/XMLSchema#",
+
+    "EcdsaKoblitzSignature2016": "sec:EcdsaKoblitzSignature2016",
+    "Ed25519Signature2018": "sec:Ed25519Signature2018",
+    "EncryptedMessage": "sec:EncryptedMessage",
+    "GraphSignature2012": "sec:GraphSignature2012",
+    "LinkedDataSignature2015": "sec:LinkedDataSignature2015",
+    "LinkedDataSignature2016": "sec:LinkedDataSignature2016",
+    "CryptographicKey": "sec:Key",
+
+    "authenticationTag": "sec:authenticationTag",
+    "canonicalizationAlgorithm": "sec:canonicalizationAlgorithm",
+    "cipherAlgorithm": "sec:cipherAlgorithm",
+    "cipherData": "sec:cipherData",
+    "cipherKey": "sec:cipherKey",
+    "created": {"@id": "dc:created", "@type": "xsd:dateTime"},
+    "creator": {"@id": "dc:creator", "@type": "@id"},
+    "digestAlgorithm": "sec:digestAlgorithm",
+    "digestValue": "sec:digestValue",
+    "domain": "sec:domain",
+    "encryptionKey": "sec:encryptionKey",
+    "expiration": {"@id": "sec:expiration", "@type": "xsd:dateTime"},
+    "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"},
+    "initializationVector": "sec:initializationVector",
+    "iterationCount": "sec:iterationCount",
+    "nonce": "sec:nonce",
+    "normalizationAlgorithm": "sec:normalizationAlgorithm",
+    "owner": {"@id": "sec:owner", "@type": "@id"},
+    "password": "sec:password",
+    "privateKey": {"@id": "sec:privateKey", "@type": "@id"},
+    "privateKeyPem": "sec:privateKeyPem",
+    "publicKey": {"@id": "sec:publicKey", "@type": "@id"},
+    "publicKeyBase58": "sec:publicKeyBase58",
+    "publicKeyPem": "sec:publicKeyPem",
+    "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"},
+    "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"},
+    "salt": "sec:salt",
+    "signature": "sec:signature",
+    "signatureAlgorithm": "sec:signingAlgorithm",
+    "signatureValue": "sec:signatureValue"
+  }
+}
diff --git a/spec/fixtures/requests/oembed_json.html b/spec/fixtures/requests/oembed_json.html
index 773a4f92a..167085871 100644
--- a/spec/fixtures/requests/oembed_json.html
+++ b/spec/fixtures/requests/oembed_json.html
@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html>
   <head>
-    <link href='https://host/provider.json' rel='alternate' type='application/json+oembed'>
+    <link href='https://host.test/provider.json' rel='alternate' type='application/json+oembed'>
   </head>
   <body></body>
 </html>
diff --git a/spec/fixtures/requests/oembed_json_xml.html b/spec/fixtures/requests/oembed_json_xml.html
index 8afd8e997..9f5b9e8be 100644
--- a/spec/fixtures/requests/oembed_json_xml.html
+++ b/spec/fixtures/requests/oembed_json_xml.html
@@ -7,8 +7,8 @@
       > The type attribute must contain either application/json+oembed for JSON
       > responses, or text/xml+oembed for XML.
     -->
-    <link href='https://host/provider.json' rel='alternate' type='application/json+oembed'>
-    <link href='https://host/provider.xml' rel='alternate' type='text/xml+oembed'>
+    <link href='https://host.test/provider.json' rel='alternate' type='application/json+oembed'>
+    <link href='https://host.test/provider.xml' rel='alternate' type='text/xml+oembed'>
   </head>
   <body></body>
 </html>
diff --git a/spec/fixtures/requests/oembed_xml.html b/spec/fixtures/requests/oembed_xml.html
index bdfcca170..788dfaabd 100644
--- a/spec/fixtures/requests/oembed_xml.html
+++ b/spec/fixtures/requests/oembed_xml.html
@@ -7,7 +7,7 @@
       > The type attribute must contain either application/json+oembed for JSON
       > responses, or text/xml+oembed for XML.
     -->
-    <link href='https://host/provider.xml' rel='alternate' type='text/xml+oembed'>
+    <link href='https://host.test/provider.xml' rel='alternate' type='text/xml+oembed'>
   </head>
   <body></body>
 </html>
diff --git a/spec/helpers/jsonld_helper_spec.rb b/spec/helpers/jsonld_helper_spec.rb
index 48bfdc306..a5ab249c2 100644
--- a/spec/helpers/jsonld_helper_spec.rb
+++ b/spec/helpers/jsonld_helper_spec.rb
@@ -32,37 +32,37 @@ describe JsonLdHelper do
   describe '#fetch_resource' do
     context 'when the second argument is false' do
       it 'returns resource even if the retrieved ID and the given URI does not match' do
-        stub_request(:get, 'https://bob/').to_return body: '{"id": "https://alice/"}'
-        stub_request(:get, 'https://alice/').to_return body: '{"id": "https://alice/"}'
+        stub_request(:get, 'https://bob.test/').to_return body: '{"id": "https://alice.test/"}'
+        stub_request(:get, 'https://alice.test/').to_return body: '{"id": "https://alice.test/"}'
 
-        expect(fetch_resource('https://bob/', false)).to eq({ 'id' => 'https://alice/' })
+        expect(fetch_resource('https://bob.test/', false)).to eq({ 'id' => 'https://alice.test/' })
       end
 
       it 'returns nil if the object identified by the given URI and the object identified by the retrieved ID does not match' do
-        stub_request(:get, 'https://mallory/').to_return body: '{"id": "https://marvin/"}'
-        stub_request(:get, 'https://marvin/').to_return body: '{"id": "https://alice/"}'
+        stub_request(:get, 'https://mallory.test/').to_return body: '{"id": "https://marvin.test/"}'
+        stub_request(:get, 'https://marvin.test/').to_return body: '{"id": "https://alice.test/"}'
 
-        expect(fetch_resource('https://mallory/', false)).to eq nil
+        expect(fetch_resource('https://mallory.test/', false)).to eq nil
       end
     end
 
     context 'when the second argument is true' do
       it 'returns nil if the retrieved ID and the given URI does not match' do
-        stub_request(:get, 'https://mallory/').to_return body: '{"id": "https://alice/"}'
-        expect(fetch_resource('https://mallory/', true)).to eq nil
+        stub_request(:get, 'https://mallory.test/').to_return body: '{"id": "https://alice.test/"}'
+        expect(fetch_resource('https://mallory.test/', true)).to eq nil
       end
     end
   end
 
   describe '#fetch_resource_without_id_validation' do
     it 'returns nil if the status code is not 200' do
-      stub_request(:get, 'https://host/').to_return status: 400, body: '{}'
-      expect(fetch_resource_without_id_validation('https://host/')).to eq nil
+      stub_request(:get, 'https://host.test/').to_return status: 400, body: '{}'
+      expect(fetch_resource_without_id_validation('https://host.test/')).to eq nil
     end
 
     it 'returns hash' do
-      stub_request(:get, 'https://host/').to_return status: 200, body: '{}'
-      expect(fetch_resource_without_id_validation('https://host/')).to eq({})
+      stub_request(:get, 'https://host.test/').to_return status: 200, body: '{}'
+      expect(fetch_resource_without_id_validation('https://host.test/')).to eq({})
     end
   end
 end
diff --git a/spec/lib/activitypub/linked_data_signature_spec.rb b/spec/lib/activitypub/linked_data_signature_spec.rb
index a4d6fe8c3..1f413eec9 100644
--- a/spec/lib/activitypub/linked_data_signature_spec.rb
+++ b/spec/lib/activitypub/linked_data_signature_spec.rb
@@ -16,6 +16,10 @@ RSpec.describe ActivityPub::LinkedDataSignature do
 
   subject { described_class.new(json) }
 
+  before do
+    stub_jsonld_contexts!
+  end
+
   describe '#verify_account!' do
     context 'when signature matches' do
       let(:raw_signature) do
diff --git a/spec/lib/formatter_spec.rb b/spec/lib/formatter_spec.rb
index 6e849f379..b8683e720 100644
--- a/spec/lib/formatter_spec.rb
+++ b/spec/lib/formatter_spec.rb
@@ -2,7 +2,7 @@ require 'rails_helper'
 
 RSpec.describe Formatter do
   let(:local_account)  { Fabricate(:account, domain: nil, username: 'alice') }
-  let(:remote_account) { Fabricate(:account, domain: 'remote', username: 'bob', url: 'https://remote/') }
+  let(:remote_account) { Fabricate(:account, domain: 'remote.test', username: 'bob', url: 'https://remote.test/') }
 
   shared_examples 'encode and link URLs' do
     context 'matches a stand-alone medium URL' do
@@ -377,12 +377,12 @@ RSpec.describe Formatter do
       end
 
       context 'contains linkable mentions for remote accounts' do
-        let(:text) { '@bob@remote' }
+        let(:text) { '@bob@remote.test' }
 
         before { remote_account }
 
         it 'links' do
-          is_expected.to eq '<p><span class="h-card"><a href="https://remote/" class="u-url mention">@<span>bob</span></a></span></p>'
+          is_expected.to eq '<p><span class="h-card"><a href="https://remote.test/" class="u-url mention">@<span>bob</span></a></span></p>'
         end
       end
 
diff --git a/spec/lib/ostatus/atom_serializer_spec.rb b/spec/lib/ostatus/atom_serializer_spec.rb
index 00e6f09dc..0bd22880e 100644
--- a/spec/lib/ostatus/atom_serializer_spec.rb
+++ b/spec/lib/ostatus/atom_serializer_spec.rb
@@ -30,13 +30,13 @@ RSpec.describe OStatus::AtomSerializer do
     end
 
     it 'appends activity:object with target account' do
-      target_account = Fabricate(:account, domain: 'domain', uri: 'https://domain/id')
+      target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id')
       follow_request = Fabricate(:follow_request, target_account: target_account)
 
       follow_request_salmon = serialize(follow_request)
 
       object = follow_request_salmon.nodes.find { |node| node.name == 'activity:object' }
-      expect(object.id.text).to eq 'https://domain/id'
+      expect(object.id.text).to eq 'https://domain.test/id'
     end
   end
 
@@ -386,12 +386,6 @@ RSpec.describe OStatus::AtomSerializer do
         expect(entry.category[:term]).to eq 'tag'
       end
 
-      it 'appends category element for NSFW if status is sensitive' do
-        status = Fabricate(:status, sensitive: true)
-        entry = OStatus::AtomSerializer.new.entry(status.stream_entry)
-        expect(entry.category[:term]).to eq 'nsfw'
-      end
-
       it 'appends link elements for media attachments' do
         file = attachment_fixture('attachment.jpg')
         media_attachment = Fabricate(:media_attachment, file: file)
@@ -419,20 +413,20 @@ RSpec.describe OStatus::AtomSerializer do
 
         entry = OStatus::AtomSerializer.new.entry(remote_status.stream_entry, true)
         entry.nodes.delete_if { |node| node[:type] == 'application/activity+json' } # Remove ActivityPub link to simplify test
-        xml = OStatus::AtomSerializer.render(entry).gsub('cb6e6126.ngrok.io', 'remote')
+        xml = OStatus::AtomSerializer.render(entry).gsub('cb6e6126.ngrok.io', 'remote.test')
 
         remote_status.destroy!
         remote_account.destroy!
 
         account = Account.create!(
-          domain: 'remote',
+          domain: 'remote.test',
           username: 'username',
           last_webfingered_at: Time.now.utc
         )
 
         ProcessFeedService.new.call(xml, account)
 
-        expect(Status.find_by(uri: "https://remote/users/#{remote_status.account.to_param}/statuses/#{remote_status.id}")).to be_instance_of Status
+        expect(Status.find_by(uri: "https://remote.test/users/#{remote_status.account.to_param}/statuses/#{remote_status.id}")).to be_instance_of Status
       end
     end
 
@@ -782,13 +776,13 @@ RSpec.describe OStatus::AtomSerializer do
     end
 
     it 'appends activity:object element with target account' do
-      target_account = Fabricate(:account, domain: 'domain', uri: 'https://domain/id')
+      target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id')
       block = Fabricate(:block, target_account: target_account)
 
       block_salmon = OStatus::AtomSerializer.new.block_salmon(block)
 
       object = block_salmon.nodes.find { |node| node.name == 'activity:object' }
-      expect(object.id.text).to eq 'https://domain/id'
+      expect(object.id.text).to eq 'https://domain.test/id'
     end
 
     it 'returns element whose rendered view triggers block when processed' do
@@ -869,13 +863,13 @@ RSpec.describe OStatus::AtomSerializer do
     end
 
     it 'appends activity:object element with target account' do
-      target_account = Fabricate(:account, domain: 'domain', uri: 'https://domain/id')
+      target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id')
       block = Fabricate(:block, target_account: target_account)
 
       unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block)
 
       object = unblock_salmon.nodes.find { |node| node.name == 'activity:object' }
-      expect(object.id.text).to eq 'https://domain/id'
+      expect(object.id.text).to eq 'https://domain.test/id'
     end
 
     it 'returns element whose rendered view triggers block when processed' do
@@ -1130,13 +1124,13 @@ RSpec.describe OStatus::AtomSerializer do
     end
 
     it 'appends activity:object element with target account' do
-      target_account = Fabricate(:account, domain: 'domain', uri: 'https://domain/id')
+      target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id')
       follow = Fabricate(:follow, target_account: target_account)
 
       follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow)
 
       object = follow_salmon.nodes.find { |node| node.name == 'activity:object' }
-      expect(object.id.text).to eq 'https://domain/id'
+      expect(object.id.text).to eq 'https://domain.test/id'
     end
 
     it 'includes description' do
@@ -1248,14 +1242,14 @@ RSpec.describe OStatus::AtomSerializer do
     end
 
     it 'appends activity:object element with target account' do
-      target_account = Fabricate(:account, domain: 'domain', uri: 'https://domain/id')
+      target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id')
       follow = Fabricate(:follow, target_account: target_account)
       follow.destroy!
 
       unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow)
 
       object = unfollow_salmon.nodes.find { |node| node.name == 'activity:object' }
-      expect(object.id.text).to eq 'https://domain/id'
+      expect(object.id.text).to eq 'https://domain.test/id'
     end
 
     it 'returns element whose rendered view triggers unfollow when processed' do
diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb
index 5427a2929..3a804ac0f 100644
--- a/spec/lib/tag_manager_spec.rb
+++ b/spec/lib/tag_manager_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe TagManager do
 
     around do |example|
       original_local_domain = Rails.configuration.x.local_domain
-      Rails.configuration.x.local_domain = 'domain'
+      Rails.configuration.x.local_domain = 'domain.test'
 
       example.run
 
@@ -18,11 +18,11 @@ RSpec.describe TagManager do
     end
 
     it 'returns true if the slash-stripped string equals to local domain' do
-      expect(TagManager.instance.local_domain?('DoMaIn/')).to eq true
+      expect(TagManager.instance.local_domain?('DoMaIn.Test/')).to eq true
     end
 
     it 'returns false for irrelevant string' do
-      expect(TagManager.instance.local_domain?('DoMaIn!')).to eq false
+      expect(TagManager.instance.local_domain?('DoMaIn.Test!')).to eq false
     end
   end
 
@@ -31,7 +31,7 @@ RSpec.describe TagManager do
 
     around do |example|
       original_web_domain = Rails.configuration.x.web_domain
-      Rails.configuration.x.web_domain = 'domain'
+      Rails.configuration.x.web_domain = 'domain.test'
 
       example.run
 
@@ -43,11 +43,11 @@ RSpec.describe TagManager do
     end
 
     it 'returns true if the slash-stripped string equals to web domain' do
-      expect(TagManager.instance.web_domain?('DoMaIn/')).to eq true
+      expect(TagManager.instance.web_domain?('DoMaIn.Test/')).to eq true
     end
 
     it 'returns false for string with irrelevant characters' do
-      expect(TagManager.instance.web_domain?('DoMaIn!')).to eq false
+      expect(TagManager.instance.web_domain?('DoMaIn.Test!')).to eq false
     end
   end
 
@@ -57,7 +57,7 @@ RSpec.describe TagManager do
     end
 
     it 'returns normalized domain' do
-      expect(TagManager.instance.normalize_domain('DoMaIn/')).to eq 'domain'
+      expect(TagManager.instance.normalize_domain('DoMaIn.Test/')).to eq 'domain.test'
     end
   end
 
@@ -69,18 +69,18 @@ RSpec.describe TagManager do
     end
 
     it 'returns true if the normalized string with port is local URL' do
-      Rails.configuration.x.web_domain = 'domain:42'
-      expect(TagManager.instance.local_url?('https://DoMaIn:42/')).to eq true
+      Rails.configuration.x.web_domain = 'domain.test:42'
+      expect(TagManager.instance.local_url?('https://DoMaIn.Test:42/')).to eq true
     end
 
     it 'returns true if the normalized string without port is local URL' do
-      Rails.configuration.x.web_domain = 'domain'
-      expect(TagManager.instance.local_url?('https://DoMaIn/')).to eq true
+      Rails.configuration.x.web_domain = 'domain.test'
+      expect(TagManager.instance.local_url?('https://DoMaIn.Test/')).to eq true
     end
 
     it 'returns false for string with irrelevant characters' do
-      Rails.configuration.x.web_domain = 'domain'
-      expect(TagManager.instance.local_url?('https://domainn/')).to eq false
+      Rails.configuration.x.web_domain = 'domain.test'
+      expect(TagManager.instance.local_url?('https://domainn.test/')).to eq false
     end
   end
 
@@ -88,19 +88,19 @@ RSpec.describe TagManager do
     # The following comparisons MUST be case-insensitive.
 
     it 'returns true if the needle has a correct username and domain for remote user' do
-      expect(TagManager.instance.same_acct?('username@domain', 'UsErNaMe@DoMaIn')).to eq true
+      expect(TagManager.instance.same_acct?('username@domain.test', 'UsErNaMe@DoMaIn.Test')).to eq true
     end
 
     it 'returns false if the needle is missing a domain for remote user' do
-      expect(TagManager.instance.same_acct?('username@domain', 'UsErNaMe')).to eq false
+      expect(TagManager.instance.same_acct?('username@domain.test', 'UsErNaMe')).to eq false
     end
 
     it 'returns false if the needle has an incorrect domain for remote user' do
-      expect(TagManager.instance.same_acct?('username@domain', 'UsErNaMe@incorrect')).to eq false
+      expect(TagManager.instance.same_acct?('username@domain.test', 'UsErNaMe@incorrect.test')).to eq false
     end
 
     it 'returns false if the needle has an incorrect username for remote user' do
-      expect(TagManager.instance.same_acct?('username@domain', 'incorrect@DoMaIn')).to eq false
+      expect(TagManager.instance.same_acct?('username@domain.test', 'incorrect@DoMaIn.test')).to eq false
     end
 
     it 'returns true if the needle has a correct username and domain for local user' do
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index 3ac7208ed..a88b11482 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -94,14 +94,14 @@ RSpec.describe Account, type: :model do
 
   describe '#save_with_optional_media!' do
     before do
-      stub_request(:get, 'https://remote/valid_avatar').to_return(request_fixture('avatar.txt'))
-      stub_request(:get, 'https://remote/invalid_avatar').to_return(request_fixture('feed.txt'))
+      stub_request(:get, 'https://remote.test/valid_avatar').to_return(request_fixture('avatar.txt'))
+      stub_request(:get, 'https://remote.test/invalid_avatar').to_return(request_fixture('feed.txt'))
     end
 
     let(:account) do
       Fabricate(:account,
-                avatar_remote_url: 'https://remote/valid_avatar',
-                header_remote_url: 'https://remote/valid_avatar')
+                avatar_remote_url: 'https://remote.test/valid_avatar',
+                header_remote_url: 'https://remote.test/valid_avatar')
     end
 
     let!(:expectation) { account.dup }
@@ -121,7 +121,7 @@ RSpec.describe Account, type: :model do
 
     context 'with invalid properties' do
       before do
-        account.avatar_remote_url = 'https://remote/invalid_avatar'
+        account.avatar_remote_url = 'https://remote.test/invalid_avatar'
         account.save_with_optional_media!
       end
 
@@ -815,7 +815,8 @@ RSpec.describe Account, type: :model do
   end
 
   context 'when is local' do
-    it 'generates keys' do
+    # Test disabled because test environment omits autogenerating keys for performance
+    xit 'generates keys' do
       account = Account.create!(domain: nil, username: Faker::Internet.user_name(nil, ['_']))
       expect(account.keypair.private?).to eq true
     end
diff --git a/spec/models/concerns/account_interactions_spec.rb b/spec/models/concerns/account_interactions_spec.rb
index 95bf9561d..9c9b87daf 100644
--- a/spec/models/concerns/account_interactions_spec.rb
+++ b/spec/models/concerns/account_interactions_spec.rb
@@ -115,13 +115,15 @@ describe AccountInteractions do
   end
 
   describe '#mute!' do
+    subject { account.mute!(target_account, notifications: arg_notifications) }
+
     context 'Mute does not exist yet' do
       context 'arg :notifications is nil' do
         let(:arg_notifications) { nil }
 
-        it 'creates Mute, and returns nil' do
+        it 'creates Mute, and returns Mute' do
           expect do
-            expect(account.mute!(target_account, notifications: arg_notifications)).to be nil
+            expect(subject).to be_kind_of Mute
           end.to change { account.mute_relationships.count }.by 1
         end
       end
@@ -129,9 +131,9 @@ describe AccountInteractions do
       context 'arg :notifications is false' do
         let(:arg_notifications) { false }
 
-        it 'creates Mute, and returns nil' do
+        it 'creates Mute, and returns Mute' do
           expect do
-            expect(account.mute!(target_account, notifications: arg_notifications)).to be nil
+            expect(subject).to be_kind_of Mute
           end.to change { account.mute_relationships.count }.by 1
         end
       end
@@ -139,9 +141,9 @@ describe AccountInteractions do
       context 'arg :notifications is true' do
         let(:arg_notifications) { true }
 
-        it 'creates Mute, and returns nil' do
+        it 'creates Mute, and returns Mute' do
           expect do
-            expect(account.mute!(target_account, notifications: arg_notifications)).to be nil
+            expect(subject).to be_kind_of Mute
           end.to change { account.mute_relationships.count }.by 1
         end
       end
@@ -165,36 +167,30 @@ describe AccountInteractions do
         context 'arg :notifications is nil' do
           let(:arg_notifications) { nil }
 
-          it 'returns nil without updating mute.hide_notifications' do
+          it 'returns Mute without updating mute.hide_notifications' do
             expect do
-              expect(account.mute!(target_account, notifications: arg_notifications)).to be nil
-              mute = account.mute_relationships.find_by(target_account: target_account)
-              expect(mute.hide_notifications?).to be true
-            end
+              expect(subject).to be_kind_of Mute
+            end.not_to change { mute.reload.hide_notifications? }.from(true)
           end
         end
 
         context 'arg :notifications is false' do
           let(:arg_notifications) { false }
 
-          it 'returns true, and updates mute.hide_notifications false' do
+          it 'returns Mute, and updates mute.hide_notifications false' do
             expect do
-              expect(account.mute!(target_account, notifications: arg_notifications)).to be true
-              mute = account.mute_relationships.find_by(target_account: target_account)
-              expect(mute.hide_notifications?).to be false
-            end
+              expect(subject).to be_kind_of Mute
+            end.to change { mute.reload.hide_notifications? }.from(true).to(false)
           end
         end
 
         context 'arg :notifications is true' do
           let(:arg_notifications) { true }
 
-          it 'returns nil without updating mute.hide_notifications' do
+          it 'returns Mute without updating mute.hide_notifications' do
             expect do
-              expect(account.mute!(target_account, notifications: arg_notifications)).to be nil
-              mute = account.mute_relationships.find_by(target_account: target_account)
-              expect(mute.hide_notifications?).to be true
-            end
+              expect(subject).to be_kind_of Mute
+            end.not_to change { mute.reload.hide_notifications? }.from(true)
           end
         end
       end
@@ -205,36 +201,30 @@ describe AccountInteractions do
         context 'arg :notifications is nil' do
           let(:arg_notifications) { nil }
 
-          it 'returns true, and updates mute.hide_notifications true' do
+          it 'returns Mute, and updates mute.hide_notifications true' do
             expect do
-              expect(account.mute!(target_account, notifications: arg_notifications)).to be true
-              mute = account.mute_relationships.find_by(target_account: target_account)
-              expect(mute.hide_notifications?).to be true
-            end
+              expect(subject).to be_kind_of Mute
+            end.to change { mute.reload.hide_notifications? }.from(false).to(true)
           end
         end
 
         context 'arg :notifications is false' do
           let(:arg_notifications) { false }
 
-          it 'returns nil without updating mute.hide_notifications' do
+          it 'returns Mute without updating mute.hide_notifications' do
             expect do
-              expect(account.mute!(target_account, notifications: arg_notifications)).to be nil
-              mute = account.mute_relationships.find_by(target_account: target_account)
-              expect(mute.hide_notifications?).to be false
-            end
+              expect(subject).to be_kind_of Mute
+            end.not_to change { mute.reload.hide_notifications? }.from(false)
           end
         end
 
         context 'arg :notifications is true' do
           let(:arg_notifications) { true }
 
-          it 'returns true, and updates mute.hide_notifications true' do
+          it 'returns Mute, and updates mute.hide_notifications true' do
             expect do
-              expect(account.mute!(target_account, notifications: arg_notifications)).to be true
-              mute = account.mute_relationships.find_by(target_account: target_account)
-              expect(mute.hide_notifications?).to be true
-            end
+              expect(subject).to be_kind_of Mute
+            end.to change { mute.reload.hide_notifications? }.from(false).to(true)
           end
         end
       end
diff --git a/spec/models/concerns/status_threading_concern_spec.rb b/spec/models/concerns/status_threading_concern_spec.rb
index b8ebdd58c..e5736a307 100644
--- a/spec/models/concerns/status_threading_concern_spec.rb
+++ b/spec/models/concerns/status_threading_concern_spec.rb
@@ -89,34 +89,34 @@ describe StatusThreadingConcern do
     let!(:viewer) { Fabricate(:account, username: 'viewer') }
 
     it 'returns replies' do
-      expect(status.descendants).to include(reply1, reply2, reply3)
+      expect(status.descendants(4)).to include(reply1, reply2, reply3)
     end
 
     it 'does not return replies user is not allowed to see' do
       reply1.update(visibility: :private)
       reply3.update(visibility: :direct)
 
-      expect(status.descendants(viewer)).to_not include(reply1, reply3)
+      expect(status.descendants(4, viewer)).to_not include(reply1, reply3)
     end
 
     it 'does not return replies from blocked users' do
       viewer.block!(jeff)
-      expect(status.descendants(viewer)).to_not include(reply3)
+      expect(status.descendants(4, viewer)).to_not include(reply3)
     end
 
     it 'does not return replies from muted users' do
       viewer.mute!(jeff)
-      expect(status.descendants(viewer)).to_not include(reply3)
+      expect(status.descendants(4, viewer)).to_not include(reply3)
     end
 
     it 'does not return replies from silenced and not followed users' do
       jeff.update(silenced: true)
-      expect(status.descendants(viewer)).to_not include(reply3)
+      expect(status.descendants(4, viewer)).to_not include(reply3)
     end
 
     it 'does not return replies from blocked domains' do
       viewer.block_domain!('example.com')
-      expect(status.descendants(viewer)).to_not include(reply2)
+      expect(status.descendants(4, viewer)).to_not include(reply2)
     end
   end
 end
diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb
index d40ebf6dc..a0cd0800d 100644
--- a/spec/models/report_spec.rb
+++ b/spec/models/report_spec.rb
@@ -22,6 +22,101 @@ describe Report do
     end
   end
 
+  describe 'assign_to_self!' do
+    subject { report.assigned_account_id }
+
+    let(:report) { Fabricate(:report, assigned_account_id: original_account) }
+    let(:original_account) { Fabricate(:account) }
+    let(:current_account) { Fabricate(:account) }
+
+    before do
+      report.assign_to_self!(current_account)
+    end
+
+    it 'assigns to a given account' do
+      is_expected.to eq current_account.id
+    end
+  end
+
+  describe 'unassign!' do
+    subject { report.assigned_account_id }
+
+    let(:report) { Fabricate(:report, assigned_account_id: account.id) }
+    let(:account) { Fabricate(:account) }
+
+    before do
+      report.unassign!
+    end
+
+    it 'unassigns' do
+      is_expected.to be_nil
+    end
+  end
+
+  describe 'resolve!' do
+    subject(:report) { Fabricate(:report, action_taken: false, action_taken_by_account_id: nil) }
+
+    let(:acting_account) { Fabricate(:account) }
+
+    before do
+      report.resolve!(acting_account)
+    end
+
+    it 'records action taken' do
+      expect(report).to have_attributes(action_taken: true, action_taken_by_account_id: acting_account.id)
+    end
+  end
+
+  describe 'unresolve!' do
+    subject(:report) { Fabricate(:report, action_taken: true, action_taken_by_account_id: acting_account.id) }
+
+    let(:acting_account) { Fabricate(:account) }
+
+    before do
+      report.unresolve!
+    end
+
+    it 'unresolves' do
+      expect(report).to have_attributes(action_taken: false, action_taken_by_account_id: nil)
+    end
+  end
+
+  describe 'unresolved?' do
+    subject { report.unresolved? }
+
+    let(:report) { Fabricate(:report, action_taken: action_taken) }
+
+    context 'if action is taken' do
+      let(:action_taken) { true }
+
+      it { is_expected.to be false }
+    end
+
+    context 'if action not is taken' do
+      let(:action_taken) { false }
+
+      it { is_expected.to be true }
+    end
+  end
+
+  describe 'history' do
+    subject(:action_logs) { report.history }
+
+    let(:report) { Fabricate(:report, target_account_id: target_account.id, status_ids: [status.id], created_at: 3.days.ago, updated_at: 1.day.ago) }
+    let(:target_account) { Fabricate(:account) }
+    let(:status) { Fabricate(:status) }
+
+    before do
+      Fabricate('Admin::ActionLog', target_type: 'Report', account_id: target_account.id, target_id: report.id, created_at: 2.days.ago)
+      Fabricate('Admin::ActionLog', target_type: 'Account', account_id: target_account.id, target_id: report.target_account_id, created_at: 2.days.ago)
+      Fabricate('Admin::ActionLog', target_type: 'Status', account_id: target_account.id, target_id: status.id, created_at: 2.days.ago)
+    end
+
+    it 'returns right logs' do
+      expect(action_logs.count).to eq 3
+    end
+  end
+
   describe 'validatiions' do
     it 'has a valid fabricator' do
       report = Fabricate(:report)
diff --git a/spec/models/status_pin_spec.rb b/spec/models/status_pin_spec.rb
index 944baf639..6f0b2feb8 100644
--- a/spec/models/status_pin_spec.rb
+++ b/spec/models/status_pin_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe StatusPin, type: :model do
     end
 
     it 'allows pins above the max for remote accounts' do
-      account = Fabricate(:account, domain: 'remote', username: 'bob', url: 'https://remote/')
+      account = Fabricate(:account, domain: 'remote.test', username: 'bob', url: 'https://remote.test/')
       status = []
 
       (max_pins + 1).times do |i|
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 8171c939a..760214ded 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -324,4 +324,218 @@ RSpec.describe User, type: :model do
       expect(admin.role?('moderator')).to be true
     end
   end
+
+  describe '#disable!' do
+    subject(:user) { Fabricate(:user, disabled: false, current_sign_in_at: current_sign_in_at, last_sign_in_at: nil) }
+    let(:current_sign_in_at) { Time.zone.now }
+
+    before do
+      user.disable!
+    end
+
+    it 'disables user' do
+      expect(user).to have_attributes(disabled: true, current_sign_in_at: nil, last_sign_in_at: current_sign_in_at)
+    end
+  end
+
+  describe '#disable!' do
+    subject(:user) { Fabricate(:user, disabled: false, current_sign_in_at: current_sign_in_at, last_sign_in_at: nil) }
+    let(:current_sign_in_at) { Time.zone.now }
+
+    before do
+      user.disable!
+    end
+
+    it 'disables user' do
+      expect(user).to have_attributes(disabled: true, current_sign_in_at: nil, last_sign_in_at: current_sign_in_at)
+    end
+  end
+
+  describe '#enable!' do
+    subject(:user) { Fabricate(:user, disabled: true) }
+
+    before do
+      user.enable!
+    end
+
+    it 'enables user' do
+      expect(user).to have_attributes(disabled: false)
+    end
+  end
+
+  describe '#confirm!' do
+    subject(:user) { Fabricate(:user, confirmed_at: confirmed_at) }
+
+    before do
+      ActionMailer::Base.deliveries.clear
+      user.confirm!
+    end
+
+    after { ActionMailer::Base.deliveries.clear }
+
+    context 'when user is new' do
+      let(:confirmed_at) { nil }
+
+      it 'confirms user' do
+        expect(user.confirmed_at).to be_present
+      end
+
+      it 'delivers mails' do
+        expect(ActionMailer::Base.deliveries.count).to eq 2
+      end
+    end
+
+    context 'when user is not new' do
+      let(:confirmed_at) { Time.zone.now }
+
+      it 'confirms user' do
+        expect(user.confirmed_at).to be_present
+      end
+
+      it 'does not deliver mail' do
+        expect(ActionMailer::Base.deliveries.count).to eq 0
+      end
+    end
+  end
+
+  describe '#promote!' do
+    subject(:user) { Fabricate(:user, admin: is_admin, moderator: is_moderator) }
+
+    before do
+      user.promote!
+    end
+
+    context 'when user is an admin' do
+      let(:is_admin) { true }
+
+      context 'when user is a moderator' do
+        let(:is_moderator) { true }
+
+        it 'changes moderator filed false' do
+          expect(user).to be_admin
+          expect(user).not_to be_moderator
+        end
+      end
+
+      context 'when user is not a moderator' do
+        let(:is_moderator) { false }
+
+        it 'does not change status' do
+          expect(user).to be_admin
+          expect(user).not_to be_moderator
+        end
+      end
+    end
+
+    context 'when user is not admin' do
+      let(:is_admin) { false }
+
+      context 'when user is a moderator' do
+        let(:is_moderator) { true }
+
+        it 'changes user into an admin' do
+          expect(user).to be_admin
+          expect(user).not_to be_moderator
+        end
+      end
+
+      context 'when user is not a moderator' do
+        let(:is_moderator) { false }
+
+        it 'changes user into a moderator' do
+          expect(user).not_to be_admin
+          expect(user).to be_moderator
+        end
+      end
+    end
+  end
+
+  describe '#demote!' do
+    subject(:user) { Fabricate(:user, admin: admin, moderator: moderator) }
+
+    before do
+      user.demote!
+    end
+
+    context 'when user is an admin' do
+      let(:admin) { true }
+
+      context 'when user is a moderator' do
+        let(:moderator) { true }
+
+        it 'changes user into a moderator' do
+          expect(user).not_to be_admin
+          expect(user).to be_moderator
+        end
+      end
+
+      context 'when user is not a moderator' do
+        let(:moderator) { false }
+
+        it 'changes user into a moderator' do
+          expect(user).not_to be_admin
+          expect(user).to be_moderator
+        end
+      end
+    end
+
+    context 'when user is not an admin' do
+      let(:admin) { false }
+
+      context 'when user is a moderator' do
+        let(:moderator) { true }
+
+        it 'changes user into a plain user' do
+          expect(user).not_to be_admin
+          expect(user).not_to be_moderator
+        end
+      end
+
+      context 'when user is not a moderator' do
+        let(:moderator) { false }
+
+        it 'does not change any fields' do
+          expect(user).not_to be_admin
+          expect(user).not_to be_moderator
+        end
+      end
+    end
+  end
+
+  describe '#active_for_authentication?' do
+    subject { user.active_for_authentication? }
+    let(:user) { Fabricate(:user, disabled: disabled, confirmed_at: confirmed_at) }
+
+    context 'when user is disabled' do
+      let(:disabled) { true }
+
+      context 'when user is confirmed' do
+        let(:confirmed_at) { Time.zone.now }
+
+        it { is_expected.to be false }
+      end
+
+      context 'when user is not confirmed' do
+        let(:confirmed_at) { nil }
+
+        it { is_expected.to be false }
+      end
+    end
+
+    context 'when user is not disabled' do
+      let(:disabled) { false }
+
+      context 'when user is confirmed' do
+        let(:confirmed_at) { Time.zone.now }
+
+        it { is_expected.to be true }
+      end
+
+      context 'when user is not confirmed' do
+        let(:confirmed_at) { nil }
+
+        it { is_expected.to be false }
+      end
+    end
+  end
 end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index dc1f32e08..c575128e4 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -50,6 +50,14 @@ RSpec.configure do |config|
     Capybara.app_host = "http#{https ? 's' : ''}://#{ENV.fetch('LOCAL_DOMAIN')}"
   end
 
+  config.before :each, type: :controller do
+    stub_jsonld_contexts!
+  end
+
+  config.before :each, type: :service do
+    stub_jsonld_contexts!
+  end
+
   config.after :each do
     Rails.cache.clear
 
@@ -69,3 +77,9 @@ end
 def attachment_fixture(name)
   File.open(File.join(Rails.root, 'spec', 'fixtures', 'files', name))
 end
+
+def stub_jsonld_contexts!
+  stub_request(:get, 'https://www.w3.org/ns/activitystreams').to_return(request_fixture('json-ld.activitystreams.txt'))
+  stub_request(:get, 'https://w3id.org/identity/v1').to_return(request_fixture('json-ld.identity.txt'))
+  stub_request(:get, 'https://w3id.org/security/v1').to_return(request_fixture('json-ld.security.txt'))
+end
diff --git a/spec/requests/host_meta_request_spec.rb b/spec/requests/host_meta_request_spec.rb
index 0c51b5f48..beb33a859 100644
--- a/spec/requests/host_meta_request_spec.rb
+++ b/spec/requests/host_meta_request_spec.rb
@@ -5,7 +5,7 @@ describe "The host_meta route" do
     it "returns an xml response" do
       get host_meta_url
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(response.content_type).to eq "application/xrd+xml"
     end
   end
diff --git a/spec/requests/webfinger_request_spec.rb b/spec/requests/webfinger_request_spec.rb
index a17d6cc22..7f9e1162e 100644
--- a/spec/requests/webfinger_request_spec.rb
+++ b/spec/requests/webfinger_request_spec.rb
@@ -7,7 +7,7 @@ describe 'The webfinger route' do
     it 'returns a json response' do
       get webfinger_url(resource: alice.to_webfinger_s)
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(response.content_type).to eq 'application/jrd+json'
     end
   end
@@ -16,7 +16,7 @@ describe 'The webfinger route' do
     it 'returns an xml response for xml format' do
       get webfinger_url(resource: alice.to_webfinger_s, format: :xml)
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(response.content_type).to eq 'application/xrd+xml'
     end
 
@@ -24,7 +24,7 @@ describe 'The webfinger route' do
       headers = { 'HTTP_ACCEPT' => 'application/xrd+xml' }
       get webfinger_url(resource: alice.to_webfinger_s), headers: headers
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(response.content_type).to eq 'application/xrd+xml'
     end
   end
@@ -33,7 +33,7 @@ describe 'The webfinger route' do
     it 'returns a json response for json format' do
       get webfinger_url(resource: alice.to_webfinger_s, format: :json)
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(response.content_type).to eq 'application/jrd+json'
     end
 
@@ -41,7 +41,7 @@ describe 'The webfinger route' do
       headers = { 'HTTP_ACCEPT' => 'application/jrd+json' }
       get webfinger_url(resource: alice.to_webfinger_s), headers: headers
 
-      expect(response).to have_http_status(:success)
+      expect(response).to have_http_status(200)
       expect(response.content_type).to eq 'application/jrd+json'
     end
   end
diff --git a/spec/services/account_search_service_spec.rb b/spec/services/account_search_service_spec.rb
index 9bb27edad..c6cbdcce1 100644
--- a/spec/services/account_search_service_spec.rb
+++ b/spec/services/account_search_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-describe AccountSearchService do
+describe AccountSearchService, type: :service do
   describe '.call' do
     describe 'with a query to ignore' do
       it 'returns empty array for missing query' do
@@ -137,5 +137,24 @@ describe AccountSearchService do
         expect(service).not_to have_received(:call)
       end
     end
+
+    describe 'should not include suspended accounts' do
+      it 'returns the fuzzy match first, and does not return suspended exacts' do
+        partial = Fabricate(:account, username: 'exactness')
+        exact = Fabricate(:account, username: 'exact', suspended: true)
+
+        results = subject.call('exact', 10)
+        expect(results.size).to eq 1
+        expect(results).to eq [partial]
+      end
+
+      it "does not return suspended remote accounts" do
+        remote = Fabricate(:account, username: 'a', domain: 'remote', display_name: 'e', suspended: true)
+
+        results = subject.call('a@example.com', 2)
+        expect(results.size).to eq 0
+        expect(results).to eq []
+      end
+    end
   end
 end
diff --git a/spec/services/activitypub/fetch_remote_account_service_spec.rb b/spec/services/activitypub/fetch_remote_account_service_spec.rb
index c50d3fb97..dba55c034 100644
--- a/spec/services/activitypub/fetch_remote_account_service_spec.rb
+++ b/spec/services/activitypub/fetch_remote_account_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe ActivityPub::FetchRemoteAccountService do
+RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
   subject { ActivityPub::FetchRemoteAccountService.new }
 
   let!(:actor) do
diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb
index a533e8413..549eb80fa 100644
--- a/spec/services/activitypub/fetch_remote_status_service_spec.rb
+++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe ActivityPub::FetchRemoteStatusService do
+RSpec.describe ActivityPub::FetchRemoteStatusService, type: :service do
   include ActionView::Helpers::TextHelper
 
   let(:sender) { Fabricate(:account) }
diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb
index 15e1f4bb2..d3318b2ed 100644
--- a/spec/services/activitypub/process_account_service_spec.rb
+++ b/spec/services/activitypub/process_account_service_spec.rb
@@ -1,14 +1,14 @@
 require 'rails_helper'
 
-RSpec.describe ActivityPub::ProcessAccountService do
+RSpec.describe ActivityPub::ProcessAccountService, type: :service do
   subject { described_class.new }
 
   context 'property values' do
     let(:payload) do
       {
-        id: 'https://foo',
+        id: 'https://foo.test',
         type: 'Actor',
-        inbox: 'https://foo/inbox',
+        inbox: 'https://foo.test/inbox',
         attachment: [
           { type: 'PropertyValue', name: 'Pronouns', value: 'They/them' },
           { type: 'PropertyValue', name: 'Occupation', value: 'Unit test' },
diff --git a/spec/services/activitypub/process_collection_service_spec.rb b/spec/services/activitypub/process_collection_service_spec.rb
index 3cea970cf..e46f0ae45 100644
--- a/spec/services/activitypub/process_collection_service_spec.rb
+++ b/spec/services/activitypub/process_collection_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe ActivityPub::ProcessCollectionService do
+RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
   let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') }
 
   let(:payload) do
diff --git a/spec/services/after_block_service_spec.rb b/spec/services/after_block_service_spec.rb
index 1b115c938..f63b2045a 100644
--- a/spec/services/after_block_service_spec.rb
+++ b/spec/services/after_block_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe AfterBlockService do
+RSpec.describe AfterBlockService, type: :service do
   subject do
     -> { described_class.new.call(account, target_account) }
   end
diff --git a/spec/services/authorize_follow_service_spec.rb b/spec/services/authorize_follow_service_spec.rb
index 6ea4d83da..562ef0041 100644
--- a/spec/services/authorize_follow_service_spec.rb
+++ b/spec/services/authorize_follow_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe AuthorizeFollowService do
+RSpec.describe AuthorizeFollowService, type: :service do
   let(:sender) { Fabricate(:account, username: 'alice') }
 
   subject { AuthorizeFollowService.new }
diff --git a/spec/services/batched_remove_status_service_spec.rb b/spec/services/batched_remove_status_service_spec.rb
index 437da2a9d..23c122e59 100644
--- a/spec/services/batched_remove_status_service_spec.rb
+++ b/spec/services/batched_remove_status_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe BatchedRemoveStatusService do
+RSpec.describe BatchedRemoveStatusService, type: :service do
   subject { BatchedRemoveStatusService.new }
 
   let!(:alice)  { Fabricate(:account) }
diff --git a/spec/services/block_domain_from_account_service_spec.rb b/spec/services/block_domain_from_account_service_spec.rb
index e7ee34372..365c0a4ad 100644
--- a/spec/services/block_domain_from_account_service_spec.rb
+++ b/spec/services/block_domain_from_account_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe BlockDomainFromAccountService do
+RSpec.describe BlockDomainFromAccountService, type: :service do
   let!(:wolf) { Fabricate(:account, username: 'wolf', domain: 'evil.org') }
   let!(:alice) { Fabricate(:account, username: 'alice') }
 
diff --git a/spec/services/block_domain_service_spec.rb b/spec/services/block_domain_service_spec.rb
index 5c2cfc8c7..7ef9e2770 100644
--- a/spec/services/block_domain_service_spec.rb
+++ b/spec/services/block_domain_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe BlockDomainService do
+RSpec.describe BlockDomainService, type: :service do
   let(:bad_account) { Fabricate(:account, username: 'badguy666', domain: 'evil.org') }
   let(:bad_status1) { Fabricate(:status, account: bad_account, text: 'You suck') }
   let(:bad_status2) { Fabricate(:status, account: bad_account, text: 'Hahaha') }
diff --git a/spec/services/block_service_spec.rb b/spec/services/block_service_spec.rb
index c69ff7804..6584bb90e 100644
--- a/spec/services/block_service_spec.rb
+++ b/spec/services/block_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe BlockService do
+RSpec.describe BlockService, type: :service do
   let(:sender) { Fabricate(:account, username: 'alice') }
 
   subject { BlockService.new }
diff --git a/spec/services/bootstrap_timeline_service_spec.rb b/spec/services/bootstrap_timeline_service_spec.rb
index 5189b1de8..a765de791 100644
--- a/spec/services/bootstrap_timeline_service_spec.rb
+++ b/spec/services/bootstrap_timeline_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe BootstrapTimelineService do
+RSpec.describe BootstrapTimelineService, type: :service do
   subject { described_class.new }
 
   describe '#call' do
diff --git a/spec/services/fan_out_on_write_service_spec.rb b/spec/services/fan_out_on_write_service_spec.rb
index 764318e34..b7fc7f7ed 100644
--- a/spec/services/fan_out_on_write_service_spec.rb
+++ b/spec/services/fan_out_on_write_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe FanOutOnWriteService do
+RSpec.describe FanOutOnWriteService, type: :service do
   let(:author)   { Fabricate(:account, username: 'tom') }
   let(:status)   { Fabricate(:status, text: 'Hello @alice #test', account: author) }
   let(:alice)    { Fabricate(:user, account: Fabricate(:account, username: 'alice')).account }
diff --git a/spec/services/favourite_service_spec.rb b/spec/services/favourite_service_spec.rb
index 5bf2c74a9..0a20ccf6e 100644
--- a/spec/services/favourite_service_spec.rb
+++ b/spec/services/favourite_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe FavouriteService do
+RSpec.describe FavouriteService, type: :service do
   let(:sender) { Fabricate(:account, username: 'alice') }
 
   subject { FavouriteService.new }
diff --git a/spec/services/fetch_atom_service_spec.rb b/spec/services/fetch_atom_service_spec.rb
index 2bd127e92..bb233c12d 100644
--- a/spec/services/fetch_atom_service_spec.rb
+++ b/spec/services/fetch_atom_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe FetchAtomService do
+RSpec.describe FetchAtomService, type: :service do
   describe '#call' do
     let(:url) { 'http://example.com' }
     subject { FetchAtomService.new.call(url) }
diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb
index edacc4425..88c5339db 100644
--- a/spec/services/fetch_link_card_service_spec.rb
+++ b/spec/services/fetch_link_card_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe FetchLinkCardService do
+RSpec.describe FetchLinkCardService, type: :service do
   subject { FetchLinkCardService.new }
 
   before do
diff --git a/spec/lib/provider_discovery_spec.rb b/spec/services/fetch_oembed_service_spec.rb
index 12e2616c9..706eb3f2a 100644
--- a/spec/lib/provider_discovery_spec.rb
+++ b/spec/services/fetch_oembed_service_spec.rb
@@ -2,12 +2,19 @@
 
 require 'rails_helper'
 
-describe ProviderDiscovery do
+describe FetchOEmbedService, type: :service do
+  subject { described_class.new }
+
+  before do
+    stub_request(:get, "https://host.test/provider.json").to_return(status: 404)
+    stub_request(:get, "https://host.test/provider.xml").to_return(status: 404)
+  end
+
   describe 'discover_provider' do
     context 'when status code is 200 and MIME type is text/html' do
       context 'Both of JSON and XML provider are discoverable' do
         before do
-          stub_request(:get, 'https://host/oembed.html').to_return(
+          stub_request(:get, 'https://host.test/oembed.html').to_return(
             status: 200,
             headers: { 'Content-Type': 'text/html' },
             body: request_fixture('oembed_json_xml.html')
@@ -15,21 +22,21 @@ describe ProviderDiscovery do
         end
 
         it 'returns new OEmbed::Provider for JSON provider if :format option is set to :json' do
-          provider = ProviderDiscovery.discover_provider('https://host/oembed.html', format: :json)
-          expect(provider.endpoint).to eq 'https://host/provider.json'
-          expect(provider.format).to eq :json
+          subject.call('https://host.test/oembed.html', format: :json)
+          expect(subject.endpoint_url).to eq 'https://host.test/provider.json'
+          expect(subject.format).to eq :json
         end
 
         it 'returns new OEmbed::Provider for XML provider if :format option is set to :xml' do
-          provider = ProviderDiscovery.discover_provider('https://host/oembed.html', format: :xml)
-          expect(provider.endpoint).to eq 'https://host/provider.xml'
-          expect(provider.format).to eq :xml
+          subject.call('https://host.test/oembed.html', format: :xml)
+          expect(subject.endpoint_url).to eq 'https://host.test/provider.xml'
+          expect(subject.format).to eq :xml
         end
       end
 
       context 'JSON provider is discoverable while XML provider is not' do
         before do
-          stub_request(:get, 'https://host/oembed.html').to_return(
+          stub_request(:get, 'https://host.test/oembed.html').to_return(
             status: 200,
             headers: { 'Content-Type': 'text/html' },
             body: request_fixture('oembed_json.html')
@@ -37,15 +44,15 @@ describe ProviderDiscovery do
         end
 
         it 'returns new OEmbed::Provider for JSON provider' do
-          provider = ProviderDiscovery.discover_provider('https://host/oembed.html')
-          expect(provider.endpoint).to eq 'https://host/provider.json'
-          expect(provider.format).to eq :json
+          subject.call('https://host.test/oembed.html')
+          expect(subject.endpoint_url).to eq 'https://host.test/provider.json'
+          expect(subject.format).to eq :json
         end
       end
 
       context 'XML provider is discoverable while JSON provider is not' do
         before do
-          stub_request(:get, 'https://host/oembed.html').to_return(
+          stub_request(:get, 'https://host.test/oembed.html').to_return(
             status: 200,
             headers: { 'Content-Type': 'text/html' },
             body: request_fixture('oembed_xml.html')
@@ -53,65 +60,65 @@ describe ProviderDiscovery do
         end
 
         it 'returns new OEmbed::Provider for XML provider' do
-          provider = ProviderDiscovery.discover_provider('https://host/oembed.html')
-          expect(provider.endpoint).to eq 'https://host/provider.xml'
-          expect(provider.format).to eq :xml
+          subject.call('https://host.test/oembed.html')
+          expect(subject.endpoint_url).to eq 'https://host.test/provider.xml'
+          expect(subject.format).to eq :xml
         end
       end
 
       context 'Invalid XML provider is discoverable while JSON provider is not' do
         before do
-          stub_request(:get, 'https://host/oembed.html').to_return(
+          stub_request(:get, 'https://host.test/oembed.html').to_return(
             status: 200,
             headers: { 'Content-Type': 'text/html' },
             body: request_fixture('oembed_invalid_xml.html')
           )
         end
 
-        it 'raises OEmbed::NotFound' do
-          expect { ProviderDiscovery.discover_provider('https://host/oembed.html') }.to raise_error OEmbed::NotFound
+        it 'returns nil' do
+          expect(subject.call('https://host.test/oembed.html')).to be_nil
         end
       end
 
       context 'Neither of JSON and XML provider is discoverable' do
         before do
-          stub_request(:get, 'https://host/oembed.html').to_return(
+          stub_request(:get, 'https://host.test/oembed.html').to_return(
             status: 200,
             headers: { 'Content-Type': 'text/html' },
             body: request_fixture('oembed_undiscoverable.html')
           )
         end
 
-        it 'raises OEmbed::NotFound' do
-          expect { ProviderDiscovery.discover_provider('https://host/oembed.html') }.to raise_error OEmbed::NotFound
+        it 'returns nil' do
+          expect(subject.call('https://host.test/oembed.html')).to be_nil
         end
       end
     end
 
     context 'when status code is not 200' do
       before do
-        stub_request(:get, 'https://host/oembed.html').to_return(
+        stub_request(:get, 'https://host.test/oembed.html').to_return(
           status: 400,
           headers: { 'Content-Type': 'text/html' },
           body: request_fixture('oembed_xml.html')
         )
       end
 
-      it 'raises OEmbed::NotFound' do
-        expect { ProviderDiscovery.discover_provider('https://host/oembed.html') }.to raise_error OEmbed::NotFound
+      it 'returns nil' do
+        expect(subject.call('https://host.test/oembed.html')).to be_nil
       end
     end
 
     context 'when MIME type is not text/html' do
       before do
-        stub_request(:get, 'https://host/oembed.html').to_return(
+        stub_request(:get, 'https://host.test/oembed.html').to_return(
           status: 200,
           body: request_fixture('oembed_xml.html')
         )
       end
 
-      it 'raises OEmbed::NotFound' do
-        expect { ProviderDiscovery.discover_provider('https://host/oembed.html') }.to raise_error OEmbed::NotFound
+      it 'returns nil' do
+        expect(subject.call('https://host.test/oembed.html')).to be_nil
       end
     end
   end
diff --git a/spec/services/fetch_remote_account_service_spec.rb b/spec/services/fetch_remote_account_service_spec.rb
index 4388d4cf4..1c3abe8f3 100644
--- a/spec/services/fetch_remote_account_service_spec.rb
+++ b/spec/services/fetch_remote_account_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe FetchRemoteAccountService do
+RSpec.describe FetchRemoteAccountService, type: :service do
   let(:url) { 'https://example.com' }
   let(:prefetched_body) { nil }
   let(:protocol) { :ostatus }
diff --git a/spec/services/fetch_remote_status_service_spec.rb b/spec/services/fetch_remote_status_service_spec.rb
index fa5782b94..0df9c329a 100644
--- a/spec/services/fetch_remote_status_service_spec.rb
+++ b/spec/services/fetch_remote_status_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe FetchRemoteStatusService do
+RSpec.describe FetchRemoteStatusService, type: :service do
   let(:account) { Fabricate(:account) }
   let(:prefetched_body) { nil }
   let(:valid_domain) { Rails.configuration.x.local_domain }
diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb
index e59a2f1a6..3c4ec59be 100644
--- a/spec/services/follow_service_spec.rb
+++ b/spec/services/follow_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe FollowService do
+RSpec.describe FollowService, type: :service do
   let(:sender) { Fabricate(:account, username: 'alice') }
 
   subject { FollowService.new }
diff --git a/spec/services/mute_service_spec.rb b/spec/services/mute_service_spec.rb
index 2b3e3e152..4bb839b8d 100644
--- a/spec/services/mute_service_spec.rb
+++ b/spec/services/mute_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe MuteService do
+RSpec.describe MuteService, type: :service do
   subject do
     -> { described_class.new.call(account, target_account) }
   end
diff --git a/spec/services/notify_service_spec.rb b/spec/services/notify_service_spec.rb
index 1435ec917..ff64eccbe 100644
--- a/spec/services/notify_service_spec.rb
+++ b/spec/services/notify_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe NotifyService do
+RSpec.describe NotifyService, type: :service do
   subject do
     -> { described_class.new.call(recipient, activity) }
   end
diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb
index 92fbc73cd..40fa8fbef 100644
--- a/spec/services/post_status_service_spec.rb
+++ b/spec/services/post_status_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe PostStatusService do
+RSpec.describe PostStatusService, type: :service do
   subject { PostStatusService.new }
 
   it 'creates a new status' do
diff --git a/spec/services/precompute_feed_service_spec.rb b/spec/services/precompute_feed_service_spec.rb
index 43340bffc..1f6b6ed88 100644
--- a/spec/services/precompute_feed_service_spec.rb
+++ b/spec/services/precompute_feed_service_spec.rb
@@ -2,7 +2,7 @@
 
 require 'rails_helper'
 
-RSpec.describe PrecomputeFeedService do
+RSpec.describe PrecomputeFeedService, type: :service do
   subject { PrecomputeFeedService.new }
 
   describe 'call' do
diff --git a/spec/services/process_feed_service_spec.rb b/spec/services/process_feed_service_spec.rb
index aca675dc6..d8b065063 100644
--- a/spec/services/process_feed_service_spec.rb
+++ b/spec/services/process_feed_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe ProcessFeedService do
+RSpec.describe ProcessFeedService, type: :service do
   subject { ProcessFeedService.new }
 
   describe 'processing a feed' do
diff --git a/spec/services/process_interaction_service_spec.rb b/spec/services/process_interaction_service_spec.rb
index 3ea7aec59..b858c19d0 100644
--- a/spec/services/process_interaction_service_spec.rb
+++ b/spec/services/process_interaction_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe ProcessInteractionService do
+RSpec.describe ProcessInteractionService, type: :service do
   let(:receiver) { Fabricate(:user, email: 'alice@example.com', account: Fabricate(:account, username: 'alice')).account }
   let(:sender)   { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
   let(:remote_sender) { Fabricate(:account, username: 'carol', domain: 'localdomain.com', uri: 'https://webdomain.com/users/carol') }
diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb
index 19a8678f0..963924fa9 100644
--- a/spec/services/process_mentions_service_spec.rb
+++ b/spec/services/process_mentions_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe ProcessMentionsService do
+RSpec.describe ProcessMentionsService, type: :service do
   let(:account) { Fabricate(:account, username: 'alice') }
   let(:status)  { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}") }
 
diff --git a/spec/services/pubsubhubbub/subscribe_service_spec.rb b/spec/services/pubsubhubbub/subscribe_service_spec.rb
index 82094117b..01c956230 100644
--- a/spec/services/pubsubhubbub/subscribe_service_spec.rb
+++ b/spec/services/pubsubhubbub/subscribe_service_spec.rb
@@ -2,7 +2,7 @@
 
 require 'rails_helper'
 
-describe Pubsubhubbub::SubscribeService do
+describe Pubsubhubbub::SubscribeService, type: :service do
   describe '#call' do
     subject { described_class.new }
     let(:user_account) { Fabricate(:account) }
diff --git a/spec/services/pubsubhubbub/unsubscribe_service_spec.rb b/spec/services/pubsubhubbub/unsubscribe_service_spec.rb
index 59054ed99..7ed9fc5af 100644
--- a/spec/services/pubsubhubbub/unsubscribe_service_spec.rb
+++ b/spec/services/pubsubhubbub/unsubscribe_service_spec.rb
@@ -2,7 +2,7 @@
 
 require 'rails_helper'
 
-describe Pubsubhubbub::UnsubscribeService do
+describe Pubsubhubbub::UnsubscribeService, type: :service do
   describe '#call' do
     subject { described_class.new }
 
diff --git a/spec/services/reblog_service_spec.rb b/spec/services/reblog_service_spec.rb
index 19d3bb6cb..2755da772 100644
--- a/spec/services/reblog_service_spec.rb
+++ b/spec/services/reblog_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe ReblogService do
+RSpec.describe ReblogService, type: :service do
   let(:alice)  { Fabricate(:account, username: 'alice') }
 
   context 'OStatus' do
diff --git a/spec/services/reject_follow_service_spec.rb b/spec/services/reject_follow_service_spec.rb
index bf49dd2c9..e5ac37ed9 100644
--- a/spec/services/reject_follow_service_spec.rb
+++ b/spec/services/reject_follow_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe RejectFollowService do
+RSpec.describe RejectFollowService, type: :service do
   let(:sender) { Fabricate(:account, username: 'alice') }
 
   subject { RejectFollowService.new }
diff --git a/spec/services/remove_status_service_spec.rb b/spec/services/remove_status_service_spec.rb
index 5bb75b820..2134f51fd 100644
--- a/spec/services/remove_status_service_spec.rb
+++ b/spec/services/remove_status_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe RemoveStatusService do
+RSpec.describe RemoveStatusService, type: :service do
   subject { RemoveStatusService.new }
 
   let!(:alice)  { Fabricate(:account) }
diff --git a/spec/services/report_service_spec.rb b/spec/services/report_service_spec.rb
index 2f926ef00..2c392d376 100644
--- a/spec/services/report_service_spec.rb
+++ b/spec/services/report_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe ReportService do
+RSpec.describe ReportService, type: :service do
   subject { described_class.new }
 
   let(:source_account) { Fabricate(:account) }
diff --git a/spec/services/resolve_account_service_spec.rb b/spec/services/resolve_account_service_spec.rb
index 5f1b4467b..f4c810f75 100644
--- a/spec/services/resolve_account_service_spec.rb
+++ b/spec/services/resolve_account_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe ResolveAccountService do
+RSpec.describe ResolveAccountService, type: :service do
   subject { described_class.new }
 
   before do
@@ -105,6 +105,20 @@ RSpec.describe ResolveAccountService do
       expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
     end
 
+    context 'with multiple types' do
+      before do
+        stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor-individual.txt'))
+      end
+
+      it 'returns new remote account' do
+        account = subject.call('foo@ap.example.com')
+
+        expect(account.activitypub?).to eq true
+        expect(account.domain).to eq 'ap.example.com'
+        expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
+      end
+    end
+
     pending
   end
 
diff --git a/spec/services/resolve_url_service_spec.rb b/spec/services/resolve_url_service_spec.rb
index 1e9be4c07..7bb5d1940 100644
--- a/spec/services/resolve_url_service_spec.rb
+++ b/spec/services/resolve_url_service_spec.rb
@@ -2,7 +2,7 @@
 
 require 'rails_helper'
 
-describe ResolveURLService do
+describe ResolveURLService, type: :service do
   subject { described_class.new }
 
   describe '#call' do
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index 957b60c7d..673de5233 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -2,7 +2,7 @@
 
 require 'rails_helper'
 
-describe SearchService do
+describe SearchService, type: :service do
   subject { described_class.new }
 
   describe '#call' do
diff --git a/spec/services/send_interaction_service_spec.rb b/spec/services/send_interaction_service_spec.rb
index ff08394b0..710d8184c 100644
--- a/spec/services/send_interaction_service_spec.rb
+++ b/spec/services/send_interaction_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe SendInteractionService do
+RSpec.describe SendInteractionService, type: :service do
   subject { SendInteractionService.new }
 
   it 'sends an XML envelope to the Salmon end point of remote user'
diff --git a/spec/services/subscribe_service_spec.rb b/spec/services/subscribe_service_spec.rb
index 835be5ec5..10bdb1ba8 100644
--- a/spec/services/subscribe_service_spec.rb
+++ b/spec/services/subscribe_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe SubscribeService do
+RSpec.describe SubscribeService, type: :service do
   let(:account) { Fabricate(:account, username: 'bob', domain: 'example.com', hub_url: 'http://hub.example.com') }
   subject { SubscribeService.new }
 
diff --git a/spec/services/suspend_account_service_spec.rb b/spec/services/suspend_account_service_spec.rb
index 1cb647e8d..fd303a9d5 100644
--- a/spec/services/suspend_account_service_spec.rb
+++ b/spec/services/suspend_account_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe SuspendAccountService do
+RSpec.describe SuspendAccountService, type: :service do
   describe '#call' do
     subject do
       -> { described_class.new.call(account) }
diff --git a/spec/services/unblock_domain_service_spec.rb b/spec/services/unblock_domain_service_spec.rb
index c32e5d655..8e8893d63 100644
--- a/spec/services/unblock_domain_service_spec.rb
+++ b/spec/services/unblock_domain_service_spec.rb
@@ -2,7 +2,7 @@
 
 require 'rails_helper'
 
-describe UnblockDomainService do
+describe UnblockDomainService, type: :service do
   subject { described_class.new }
 
   describe 'call' do
diff --git a/spec/services/unblock_service_spec.rb b/spec/services/unblock_service_spec.rb
index ca7a6b77e..5835b912b 100644
--- a/spec/services/unblock_service_spec.rb
+++ b/spec/services/unblock_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe UnblockService do
+RSpec.describe UnblockService, type: :service do
   let(:sender) { Fabricate(:account, username: 'alice') }
 
   subject { UnblockService.new }
diff --git a/spec/services/unfollow_service_spec.rb b/spec/services/unfollow_service_spec.rb
index 021e76782..c5914c818 100644
--- a/spec/services/unfollow_service_spec.rb
+++ b/spec/services/unfollow_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe UnfollowService do
+RSpec.describe UnfollowService, type: :service do
   let(:sender) { Fabricate(:account, username: 'alice') }
 
   subject { UnfollowService.new }
diff --git a/spec/services/unmute_service_spec.rb b/spec/services/unmute_service_spec.rb
index 5dc971fb1..8463eb283 100644
--- a/spec/services/unmute_service_spec.rb
+++ b/spec/services/unmute_service_spec.rb
@@ -1,5 +1,5 @@
 require 'rails_helper'
 
-RSpec.describe UnmuteService do
+RSpec.describe UnmuteService, type: :service do
   subject { UnmuteService.new }
 end
diff --git a/spec/services/unsubscribe_service_spec.rb b/spec/services/unsubscribe_service_spec.rb
index 2a02f4c75..54d4b1b53 100644
--- a/spec/services/unsubscribe_service_spec.rb
+++ b/spec/services/unsubscribe_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe UnsubscribeService do
+RSpec.describe UnsubscribeService, type: :service do
   let(:account) { Fabricate(:account, username: 'bob', domain: 'example.com', hub_url: 'http://hub.example.com') }
   subject { UnsubscribeService.new }
 
diff --git a/spec/services/update_remote_profile_service_spec.rb b/spec/services/update_remote_profile_service_spec.rb
index 64ec2dbbb..7ac3a809a 100644
--- a/spec/services/update_remote_profile_service_spec.rb
+++ b/spec/services/update_remote_profile_service_spec.rb
@@ -1,6 +1,6 @@
 require 'rails_helper'
 
-RSpec.describe UpdateRemoteProfileService do
+RSpec.describe UpdateRemoteProfileService, type: :service do
   let(:xml) { File.read(File.join(Rails.root, 'spec', 'fixtures', 'push', 'feed.atom')) }
 
   subject { UpdateRemoteProfileService.new }
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index a0466dd4b..903032937 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,3 +1,4 @@
+#require 'rspec/retry'
 require 'simplecov'
 
 GC.disable
@@ -11,6 +12,9 @@ end
 gc_counter = -1
 
 RSpec.configure do |config|
+  #config.verbose_retry = true
+  #config.display_try_failure_messages = true
+
   config.expect_with :rspec do |expectations|
     expectations.include_chain_clauses_in_custom_matcher_descriptions = true
   end
@@ -25,6 +29,10 @@ RSpec.configure do |config|
     end
   end
 
+  #config.around :each do |ex|
+  #  ex.run_with_retry retry: 3
+  #end
+
   config.before :suite do
     Chewy.strategy(:bypass)
   end
diff --git a/spec/views/stream_entries/show.html.haml_spec.rb b/spec/views/stream_entries/show.html.haml_spec.rb
index 6074bbc2e..560039ffa 100644
--- a/spec/views/stream_entries/show.html.haml_spec.rb
+++ b/spec/views/stream_entries/show.html.haml_spec.rb
@@ -24,6 +24,7 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d
     assign(:stream_entry, status.stream_entry)
     assign(:account, alice)
     assign(:type, status.stream_entry.activity_type.downcase)
+    assign(:descendant_threads, [])
 
     render
 
@@ -49,7 +50,7 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d
     assign(:account, alice)
     assign(:type, reply.stream_entry.activity_type.downcase)
     assign(:ancestors, reply.stream_entry.activity.ancestors(1, bob) )
-    assign(:descendants, reply.stream_entry.activity.descendants(bob))
+    assign(:descendant_threads, [{ statuses: reply.stream_entry.activity.descendants(1)}])
 
     render
 
@@ -75,6 +76,7 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d
     assign(:stream_entry, status.stream_entry)
     assign(:account, alice)
     assign(:type, status.stream_entry.activity_type.downcase)
+    assign(:descendant_threads, [])
 
     render