about summary refs log tree commit diff
diff options
context:
space:
mode:
authorDavid Yip <yipdw@member.fsf.org>2017-09-09 14:28:08 -0500
committerDavid Yip <yipdw@member.fsf.org>2017-09-09 14:28:08 -0500
commit514fc908a373306b32b2b6b9fc0d849161d88271 (patch)
tree0f0028e424b43dcf4a59b21ccc7170dfe883746b
parentb9f7bc149b2a6abfbdaee83e6992b617b8bdb18e (diff)
parent11bddd31ce33b654ef72b00221715e6026486e7c (diff)
Merge tag 'v1.6.0rc3' into sync/upstream
-rw-r--r--.env.production.sample13
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock15
-rw-r--r--app/controllers/accounts_controller.rb8
-rw-r--r--app/controllers/activitypub/inboxes_controller.rb8
-rw-r--r--app/controllers/api/v1/accounts_controller.rb10
-rw-r--r--app/helpers/routing_helper.rb8
-rw-r--r--app/javascript/mastodon/components/scrollable_list.js8
-rw-r--r--app/javascript/mastodon/components/video_player.js4
-rw-r--r--app/javascript/mastodon/features/favourited_statuses/index.js1
-rw-r--r--app/javascript/mastodon/features/notifications/index.js1
-rw-r--r--app/javascript/mastodon/locales/de.json22
-rw-r--r--app/javascript/mastodon/locales/fa.json10
-rw-r--r--app/javascript/mastodon/locales/hr.json55
-rw-r--r--app/javascript/mastodon/locales/oc.json8
-rw-r--r--app/javascript/styles/components.scss115
-rw-r--r--app/javascript/styles/stream_entries.scss71
-rw-r--r--app/lib/activitypub/activity/create.rb12
-rw-r--r--app/lib/ostatus/atom_serializer.rb4
-rw-r--r--app/lib/tag_manager.rb13
-rw-r--r--app/models/status.rb14
-rw-r--r--app/presenters/account_relationships_presenter.rb14
-rw-r--r--app/serializers/activitypub/actor_serializer.rb18
-rw-r--r--app/serializers/oembed_serializer.rb3
-rw-r--r--app/services/activitypub/process_account_service.rb18
-rw-r--r--app/services/process_mentions_service.rb2
-rwxr-xr-xapp/views/layouts/application.html.haml14
-rw-r--r--app/views/stream_entries/_detailed_status.html.haml10
-rw-r--r--app/workers/pubsubhubbub/distribution_worker.rb2
-rw-r--r--app/workers/pubsubhubbub/subscribe_worker.rb2
-rw-r--r--app/workers/resolve_remote_account_worker.rb11
-rw-r--r--config/application.rb1
-rw-r--r--config/initializers/ostatus.rb1
-rw-r--r--config/initializers/paperclip.rb15
-rw-r--r--config/initializers/sidekiq.rb3
-rw-r--r--config/locales/ar.yml31
-rw-r--r--config/locales/bg.yml11
-rw-r--r--config/locales/ca.yml11
-rw-r--r--config/locales/de.yml25
-rw-r--r--config/locales/doorkeeper.de.yml4
-rw-r--r--config/locales/doorkeeper.fa.yml18
-rw-r--r--config/locales/doorkeeper.oc.yml6
-rw-r--r--config/locales/eo.yml11
-rw-r--r--config/locales/es.yml11
-rw-r--r--config/locales/fa.yml11
-rw-r--r--config/locales/fi.yml11
-rw-r--r--config/locales/fr.yml11
-rw-r--r--config/locales/he.yml11
-rw-r--r--config/locales/hr.yml11
-rw-r--r--config/locales/hu.yml11
-rw-r--r--config/locales/id.yml11
-rw-r--r--config/locales/io.yml11
-rw-r--r--config/locales/it.yml11
-rw-r--r--config/locales/ko.yml118
-rw-r--r--config/locales/nl.yml11
-rw-r--r--config/locales/no.yml11
-rw-r--r--config/locales/oc.yml12
-rw-r--r--config/locales/pl.yml11
-rw-r--r--config/locales/pt-BR.yml11
-rw-r--r--config/locales/pt.yml11
-rw-r--r--config/locales/ru.yml11
-rw-r--r--config/locales/simple_form.de.yml6
-rw-r--r--config/locales/th.yml11
-rw-r--r--config/locales/tr.yml11
-rw-r--r--config/locales/uk.yml11
-rw-r--r--config/locales/zh-CN.yml11
-rw-r--r--config/locales/zh-HK.yml11
-rw-r--r--config/locales/zh-TW.yml11
-rw-r--r--db/migrate/20170905044538_add_index_id_account_id_activity_type_on_notifications.rb5
-rw-r--r--db/migrate/20170905165803_add_local_to_statuses.rb5
-rw-r--r--db/schema.rb4
-rw-r--r--lib/mastodon/unique_retry_job_middleware.rb20
-rw-r--r--lib/mastodon/version.rb6
-rw-r--r--lib/tasks/mastodon.rake13
-rw-r--r--package.json2
-rw-r--r--spec/controllers/accounts_controller_spec.rb31
-rw-r--r--spec/controllers/api/v1/accounts_controller_spec.rb7
-rw-r--r--spec/fabricators/status_fabricator.rb4
-rw-r--r--spec/lib/activitypub/activity/delete_spec.rb2
-rw-r--r--spec/lib/activitypub/activity/undo_spec.rb2
-rw-r--r--spec/lib/formatter_spec.rb4
-rw-r--r--spec/lib/ostatus/atom_serializer_spec.rb31
-rw-r--r--spec/lib/tag_manager_spec.rb15
-rw-r--r--spec/models/status_spec.rb27
-rw-r--r--spec/services/fetch_link_card_service_spec.rb2
-rw-r--r--streaming/index.js8
-rw-r--r--yarn.lock24
87 files changed, 942 insertions, 280 deletions
diff --git a/.env.production.sample b/.env.production.sample
index 1d8a177aa..3e054db84 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -26,7 +26,7 @@ LOCAL_HTTPS=true
 # ALTERNATE_DOMAINS=example1.com,example2.com
 
 # Application secrets
-# Generate each with the `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
+# Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
 PAPERCLIP_SECRET=
 SECRET_KEY_BASE=
 OTP_SECRET=
@@ -36,7 +36,7 @@ OTP_SECRET=
 # You should only generate this once per instance. If you later decide to change it, all push subscription will
 # be invalidated, requiring the users to access the website again to resubscribe.
 #
-# Generate with `rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose)
+# Generate with `RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose)
 #
 # For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
 VAPID_PRIVATE_KEY=
@@ -98,6 +98,15 @@ SMTP_FROM_ADDRESS=notifications@example.com
 # S3_ENDPOINT=
 # S3_SIGNATURE_VERSION=
 
+# Swift (optional)
+# SWIFT_ENABLED=true
+# SWIFT_USERNAME=
+# SWIFT_TENANT=
+# SWIFT_PASSWORD=
+# SWIFT_AUTH_URL=
+# SWIFT_CONTAINER=
+# SWIFT_OBJECT_URL=
+
 # Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
 # S3_CLOUDFRONT_HOST=
 
diff --git a/Gemfile b/Gemfile
index ae90697f1..486e72cc4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -15,6 +15,7 @@ gem 'pghero', '~> 1.7'
 gem 'dotenv-rails', '~> 2.2'
 
 gem 'aws-sdk', '~> 2.9'
+gem 'fog-openstack', '~> 0.1'
 gem 'paperclip', '~> 5.1'
 gem 'paperclip-av-transcoder', '~> 0.6'
 
diff --git a/Gemfile.lock b/Gemfile.lock
index 4a3f20e09..ef99e0d7b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -154,12 +154,25 @@ GEM
     erubis (2.7.0)
     et-orbi (1.0.5)
       tzinfo
+    excon (0.58.0)
     execjs (2.7.0)
     fabrication (2.16.2)
     faker (1.7.3)
       i18n (~> 0.5)
     fast_blank (1.0.0)
     ffi (1.9.18)
+    fog-core (1.45.0)
+      builder
+      excon (~> 0.58)
+      formatador (~> 0.2)
+    fog-json (1.0.2)
+      fog-core (~> 1.0)
+      multi_json (~> 1.10)
+    fog-openstack (0.1.21)
+      fog-core (>= 1.40)
+      fog-json (>= 1.0)
+      ipaddress (>= 0.8)
+    formatador (0.2.5)
     fuubar (2.2.0)
       rspec-core (~> 3.0)
       ruby-progressbar (~> 1.4)
@@ -211,6 +224,7 @@ GEM
       rainbow (~> 2.2)
       terminal-table (>= 1.5.1)
     idn-ruby (0.1.0)
+    ipaddress (0.8.3)
     jmespath (1.3.1)
     json (2.1.0)
     json-ld (2.1.5)
@@ -535,6 +549,7 @@ DEPENDENCIES
   fabrication (~> 2.16)
   faker (~> 1.7)
   fast_blank (~> 1.0)
+  fog-openstack (~> 0.1)
   fuubar (~> 2.2)
   goldfinger (~> 2.0)
   hamlit-rails (~> 0.2)
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 8dad12f11..26ab6636b 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -14,7 +14,7 @@ class AccountsController < ApplicationController
           return
         end
 
-        @pinned_statuses = cache_collection(@account.pinned_statuses, Status) unless media_requested?
+        @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
         @statuses        = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id])
         @statuses        = cache_collection(@statuses, Status)
         @next_url        = next_url unless @statuses.empty?
@@ -22,7 +22,7 @@ class AccountsController < ApplicationController
 
       format.atom do
         @entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
-        render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.to_a))
+        render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? }))
       end
 
       format.json do
@@ -33,6 +33,10 @@ class AccountsController < ApplicationController
 
   private
 
+  def show_pinned_statuses?
+    [replies_requested?, media_requested?, params[:max_id].present?, params[:since_id].present?].none?
+  end
+
   def filtered_statuses
     default_statuses.tap do |statuses|
       statuses.merge!(only_media_scope) if media_requested?
diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb
index 5fce505fd..b37910b36 100644
--- a/app/controllers/activitypub/inboxes_controller.rb
+++ b/app/controllers/activitypub/inboxes_controller.rb
@@ -26,8 +26,12 @@ class ActivityPub::InboxesController < Api::BaseController
   end
 
   def upgrade_account
-    return unless signed_request_account.subscribed?
-    Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id)
+    if signed_request_account.ostatus?
+      signed_request_account.update(last_webfingered_at: nil)
+      ResolveRemoteAccountWorker.perform_async(signed_request_account.acct)
+    end
+
+    Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed?
   end
 
   def process_payload
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index f621aa245..656cacd8a 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -14,6 +14,16 @@ class Api::V1::AccountsController < Api::BaseController
 
   def follow
     FollowService.new.call(current_user.account, @account.acct)
+
+    unless @account.locked?
+      relationships = AccountRelationshipsPresenter.new(
+        [@account.id],
+        current_user.account_id,
+        following_map: { @account.id => true },
+        requested_map: { @account.id => false }
+      )
+    end
+
     render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
   end
 
diff --git a/app/helpers/routing_helper.rb b/app/helpers/routing_helper.rb
index 1fbf77ec3..f4693358c 100644
--- a/app/helpers/routing_helper.rb
+++ b/app/helpers/routing_helper.rb
@@ -12,8 +12,14 @@ module RoutingHelper
   end
 
   def full_asset_url(source, options = {})
-    source = ActionController::Base.helpers.asset_url(source, options) unless Rails.configuration.x.use_s3
+    source = ActionController::Base.helpers.asset_url(source, options) unless use_storage?
 
     URI.join(root_url, source).to_s
   end
+
+  private
+
+  def use_storage?
+    Rails.configuration.x.use_s3 || Rails.configuration.x.use_swift
+  end
 end
diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js
index 1a122dbe5..e47b1e9aa 100644
--- a/app/javascript/mastodon/components/scrollable_list.js
+++ b/app/javascript/mastodon/components/scrollable_list.js
@@ -5,6 +5,7 @@ import IntersectionObserverArticle from './intersection_observer_article';
 import LoadMore from './load_more';
 import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
 import { throttle } from 'lodash';
+import { List as ImmutableList } from 'immutable';
 
 export default class ScrollableList extends PureComponent {
 
@@ -95,7 +96,12 @@ export default class ScrollableList extends PureComponent {
 
   getFirstChildKey (props) {
     const { children } = props;
-    const firstChild = Array.isArray(children) ? children[0] : children;
+    let firstChild = children;
+    if (children instanceof ImmutableList) {
+      firstChild = children.get(0);
+    } else if (Array.isArray(children)) {
+      firstChild = children[0];
+    }
     return firstChild && firstChild.key;
   }
 
diff --git a/app/javascript/mastodon/components/video_player.js b/app/javascript/mastodon/components/video_player.js
index 5f2447c6d..f499e3e01 100644
--- a/app/javascript/mastodon/components/video_player.js
+++ b/app/javascript/mastodon/components/video_player.js
@@ -149,7 +149,7 @@ export default class VideoPlayer extends React.PureComponent {
     if (!this.state.visible) {
       if (sensitive) {
         return (
-          <div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}>
+          <div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler__video' onClick={this.handleVisibility}>
             {spoilerButton}
             <span className='media-spoiler__warning'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
             <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
@@ -157,7 +157,7 @@ export default class VideoPlayer extends React.PureComponent {
         );
       } else {
         return (
-          <div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}>
+          <div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler__video' onClick={this.handleVisibility}>
             {spoilerButton}
             <span className='media-spoiler__warning'><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
             <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js
index 82b16b369..1e1f5873c 100644
--- a/app/javascript/mastodon/features/favourited_statuses/index.js
+++ b/app/javascript/mastodon/features/favourited_statuses/index.js
@@ -77,6 +77,7 @@ export default class Favourites extends ImmutablePureComponent {
           onClick={this.handleHeaderClick}
           pinned={pinned}
           multiColumn={multiColumn}
+          showBackButton
         />
 
         <StatusList
diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js
index b644718e3..54e58dfd0 100644
--- a/app/javascript/mastodon/features/notifications/index.js
+++ b/app/javascript/mastodon/features/notifications/index.js
@@ -124,6 +124,7 @@ export default class Notifications extends React.PureComponent {
     const scrollContainer = (
       <ScrollableList
         scrollKey={`notifications-${columnId}`}
+        trackScroll={!pinned}
         isLoading={isLoading}
         hasMore={hasMore}
         emptyMessage={emptyMessage}
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 38324e156..3133238cd 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -26,12 +26,12 @@
   "bundle_modal_error.close": "Schließen",
   "bundle_modal_error.message": "Etwas ist beim Laden schiefgelaufen.",
   "bundle_modal_error.retry": "Erneut versuchen",
-  "column.blocks": "Blockierte Benutzer",
+  "column.blocks": "Blockierte Profile",
   "column.community": "Lokale Zeitleiste",
   "column.favourites": "Favoriten",
   "column.follow_requests": "Folgeanfragen",
   "column.home": "Startseite",
-  "column.mutes": "Stummgeschaltete Benutzer",
+  "column.mutes": "Stummgeschaltete Profile",
   "column.notifications": "Mitteilungen",
   "column.public": "Gesamtes bekanntes Netz",
   "column_back_button.label": "Zurück",
@@ -46,7 +46,7 @@
   "compose_form.lock_disclaimer": "Dein Profil ist nicht {locked}. Jeder kann dir jederzeit folgen, um deine privaten Beiträge einzusehen.",
   "compose_form.lock_disclaimer.lock": "gesperrt",
   "compose_form.placeholder": "Worüber möchtest du schreiben?",
-  "compose_form.privacy_disclaimer": "Dein privater Status wird an die genannten Benutzer auf den Domains {domains} zugestellt. Vertraust du {domainsCount, plural, one {diesem Server} other {diesen Servern}}? Private Beiträge funktionieren nur auf Mastodon-Instanzen. Wenn {domains} {domainsCount, plural, one {keine Mastodon-Instanz ist} other {keine Mastodon-Instanzen sind}}, wird es dort kein Anzeichen geben, dass dein Beitrag privat ist und er könnte geteilt oder anderweitig für unerwünschte Empfänger sichtbar gemacht werden.",
+  "compose_form.privacy_disclaimer": "Dein privater Status wird an die genannten Profile auf den Domains {domains} zugestellt. Vertraust du {domainsCount, plural, one {diesem Server} other {diesen Servern}}? Private Beiträge funktionieren nur auf Mastodon-Instanzen. Wenn {domains} {domainsCount, plural, one {keine Mastodon-Instanz ist} other {keine Mastodon-Instanzen sind}}, wird es dort kein Anzeichen geben, dass dein Beitrag privat ist und er könnte geteilt oder anderweitig für unerwünschte Empfänger sichtbar gemacht werden.",
   "compose_form.publish": "Tröt",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive": "Medien als heikel markieren",
@@ -77,18 +77,18 @@
   "emoji_button.travel": "Reise und Orte",
   "empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe etwas öffentlich, um den Ball ins Rollen zu bringen!",
   "empty_column.hashtag": "Es gibt noch nichts unter diesem Hashtag.",
-  "empty_column.home": "Du folgst noch niemandem. Besuche {public} oder benutze die Suche, um zu starten oder andere Benutzer anzutreffen.",
+  "empty_column.home": "Du folgst noch niemandem. Besuche {public} oder benutze die Suche, um zu starten oder andere Profile zu finden.",
   "empty_column.home.inactivity": "Deine Zeitleiste ist leer. Falls du eine längere Zeit inaktiv gewesen bist, wird sie für dich so schnell wie möglich wiedererstellt.",
   "empty_column.home.public_timeline": "die öffentliche Zeitleiste",
   "empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um die Konversation zu starten.",
-  "empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Benutzern von anderen Instanzen, um es aufzufüllen.",
+  "empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Profilen von anderen Instanzen, um es aufzufüllen.",
   "follow_request.authorize": "Erlauben",
   "follow_request.reject": "Ablehnen",
   "getting_started.appsshort": "Anwendungen",
   "getting_started.faq": "Häufig gestellte Fragen",
   "getting_started.heading": "Erste Schritte",
   "getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.",
-  "getting_started.userguide": "Nutzeranleitung",
+  "getting_started.userguide": "Bedienungsanleitung",
   "home.column_settings.advanced": "Fortgeschritten",
   "home.column_settings.basic": "Einfach",
   "home.column_settings.filter_regex": "Filter durch reguläre Ausdrücke",
@@ -101,14 +101,14 @@
   "loading_indicator.label": "Lade…",
   "media_gallery.toggle_visible": "Sichtbarkeit einstellen",
   "missing_indicator.label": "Nicht gefunden",
-  "navigation_bar.blocks": "Blockierte Benutzer",
+  "navigation_bar.blocks": "Blockierte Profile",
   "navigation_bar.community_timeline": "Lokale Zeitleiste",
   "navigation_bar.edit_profile": "Profil bearbeiten",
   "navigation_bar.favourites": "Favoriten",
   "navigation_bar.follow_requests": "Folgeanfragen",
   "navigation_bar.info": "Erweiterte Informationen",
   "navigation_bar.logout": "Abmelden",
-  "navigation_bar.mutes": "Stummgeschaltete Benutzer",
+  "navigation_bar.mutes": "Stummgeschaltete Profile",
   "navigation_bar.preferences": "Einstellungen",
   "navigation_bar.public_timeline": "Föderierte Zeitleiste",
   "notification.favourite": "{name} favorisierte deinen Status",
@@ -132,7 +132,7 @@
   "onboarding.page_four.home": "Die Startseite zeigt dir Beiträge von Leuten, denen du folgst.",
   "onboarding.page_four.notifications": "Wenn jemand mir 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.handle": "Du bist auf der Instanz {domain}, also ist dein vollständiger Nutzername im Netzwerk {handle}",
+  "onboarding.page_one.handle": "Du bist auf der Instanz {domain}, also ist dein vollständiger Profilname im Netzwerk {handle}",
   "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…",
@@ -143,11 +143,11 @@
   "onboarding.page_six.read_guidelines": "Bitte mach dich mit den {guidelines} von {domain} vertraut!",
   "onboarding.page_six.various_app": "mobile Anwendungen",
   "onboarding.page_three.profile": "Bearbeite dein Profil, um dein Bild, deinen Namen oder deine Beschreibung anzupassen. Dort findest du auch andere Einstellungen.",
-  "onboarding.page_three.search": "Benutze die Suchfunktion, um Leute oder Themen zu finden. Zum Beispiel, die Hashtags {illustration} oder {introductions}. Um eine Person zu finden, die auf einer anderen Instanz ist, benutze den vollständigen Nutzernamen.",
+  "onboarding.page_three.search": "Benutze die Suchfunktion, um Leute oder Themen zu finden. Zum Beispiel, die Hashtags {illustration} oder {introductions}. Um eine Person zu finden, die auf einer anderen Instanz ist, benutze den vollständigen Profilnamen.",
   "onboarding.page_two.compose": "Schreibe Beiträge aus der Schreiben-Spalte. Du kannst Bilder und kurze Videos hochladen, Sichtbarkeitseinstellungen ändern und Inhaltswarnungen hinzufügen.",
   "onboarding.skip": "Überspringen",
   "privacy.change": "Privatsphäre des Status anpassen",
-  "privacy.direct.long": "Beitrag nur an erwähnte Benutzer",
+  "privacy.direct.long": "Beitrag nur an erwähnte Profile",
   "privacy.direct.short": "Direkt",
   "privacy.private.long": "Beitrag nur an Folgende",
   "privacy.private.short": "Privat",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index b51340fa7..d05b26eb9 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -63,8 +63,8 @@
   "confirmations.mute.message": "آیا واقعاً می‌خواهید {name} را بی‌صدا کنید؟",
   "confirmations.unfollow.confirm": "لغو پیگیری",
   "confirmations.unfollow.message": "آیا واقعاً می‌خواهید به پیگیری از {name} پایان دهید؟",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
+  "embed.instructions": "برای جاگذاری این نوشته در سایت خودتان، کد زیر را کپی کنید.",
+  "embed.preview": "نوشتهٔ جاگذاری‌شده این گونه به نظر خواهد رسید:",
   "emoji_button.activity": "فعالیت",
   "emoji_button.flags": "پرچم‌ها",
   "emoji_button.food": "غذا و نوشیدنی",
@@ -164,14 +164,14 @@
   "standalone.public_title": "نگاهی به کاربران این سرور...",
   "status.cannot_reblog": "این نوشته را نمی‌شود بازبوقید",
   "status.delete": "پاک‌کردن",
-  "status.embed": "Embed",
+  "status.embed": "جاگذاری",
   "status.favourite": "پسندیدن",
   "status.load_more": "بیشتر نشان بده",
   "status.media_hidden": "تصویر پنهان شده",
   "status.mention": "نام‌بردن از @{name}",
   "status.mute_conversation": "بی‌صداکردن گفتگو",
   "status.open": "این نوشته را باز کن",
-  "status.pin": "Pin on profile",
+  "status.pin": "نوشتهٔ ثابت نمایه",
   "status.reblog": "بازبوقیدن",
   "status.reblogged_by": "‫{name}‬ بازبوقید",
   "status.reply": "پاسخ",
@@ -183,7 +183,7 @@
   "status.show_less": "نهفتن",
   "status.show_more": "نمایش",
   "status.unmute_conversation": "باصداکردن گفتگو",
-  "status.unpin": "Unpin from profile",
+  "status.unpin": "برداشتن نوشتهٔ ثابت نمایه",
   "tabs_bar.compose": "بنویسید",
   "tabs_bar.federated_timeline": "همگانی",
   "tabs_bar.home": "خانه",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index 27e943bdd..f301723cf 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -1,7 +1,7 @@
 {
   "account.block": "Blokiraj @{name}",
   "account.block_domain": "Sakrij sve sa {domain}",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.disclaimer_full": "Ovaj korisnik je sa druge instance. Ovaj broj bi mogao biti veći.",
   "account.edit_profile": "Uredi profil",
   "account.follow": "Slijedi",
   "account.followers": "Sljedbenici",
@@ -15,7 +15,7 @@
   "account.requested": "Čeka pristanak",
   "account.share": "Share @{name}'s profile",
   "account.unblock": "Deblokiraj @{name}",
-  "account.unblock_domain": "Otkrij {domain}",
+  "account.unblock_domain": "Poništi sakrivanje {domain}",
   "account.unfollow": "Prestani slijediti",
   "account.unmute": "Poništi utišavanje @{name}",
   "account.view_full_profile": "View full profile",
@@ -43,7 +43,7 @@
   "column_header.unpin": "Unpin",
   "column_subheading.navigation": "Navigacija",
   "column_subheading.settings": "Postavke",
-  "compose_form.lock_disclaimer": "Tvoj račun nije {locked}. Svatko te može slijediti i vidjeti tvoje postove namijenjene samo sljedbenicima.",
+  "compose_form.lock_disclaimer": "Tvoj račun nije {locked}. Svatko te može slijediti kako bi vidio postove namijenjene samo tvojim sljedbenicima.",
   "compose_form.lock_disclaimer.lock": "zaključan",
   "compose_form.placeholder": "Što ti je na umu?",
   "compose_form.privacy_disclaimer": "Tvoj privatni status će biti dostavljen spomenutim korisnicima na {domains}. Vjeruješ li {domainsCount, plural, one {that server} drugim {those servers}}? Privatnost postova radi samo na Mastodon instancama. Ako {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, neće biti indikacije da je tvoj post privatan, i mogao bi biti podignut ili biti učinjen vidljivim na drugi način neželjenim primateljima.",
@@ -54,13 +54,14 @@
   "compose_form.spoiler_placeholder": "Upozorenje o sadržaju",
   "confirmation_modal.cancel": "Otkaži",
   "confirmations.block.confirm": "Blokiraj",
-  "confirmations.block.message": "Jesi li siguran da želiš blokirati {name}?",
+  "confirmations.block.message": "Želiš li sigurno blokirati {name}?",
   "confirmations.delete.confirm": "Obriši",
-  "confirmations.delete.message": "Jesi li siguran da želiš obrisati ovaj status?",
+  "confirmations.delete.message": "Želiš li stvarno obrisati ovaj status?",
   "confirmations.domain_block.confirm": "Sakrij cijelu domenu",
-  "confirmations.domain_block.message": "Jesi li zaista, zaista siguran da želiš blokirati sve sa {domain}? U većini slučajeva nekoliko ciljanih blokiranja ili utišavanja je dostatno i poželjnije.",
+  "confirmations.domain_block.message": "Jesi li zaista, zaista siguran da želiš potpuno blokirati {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
   "confirmations.mute.confirm": "Utišaj",
   "confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?",
+  "confirmations.mute.message": "Jesi li siguran da želiš utišati {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.",
@@ -69,16 +70,16 @@
   "emoji_button.flags": "Zastave",
   "emoji_button.food": "Hrana & Piće",
   "emoji_button.label": "Umetni smajlije",
-  "emoji_button.nature": "Nature",
+  "emoji_button.nature": "Priroda",
   "emoji_button.objects": "Objekti",
   "emoji_button.people": "Ljudi",
   "emoji_button.search": "Traži...",
   "emoji_button.symbols": "Simboli",
-  "emoji_button.travel": "Putovanja i Mjesta",
+  "emoji_button.travel": "Putovanja & Mjesta",
   "empty_column.community": "Lokalni timeline je prazan. Napiši nešto javno kako bi pokrenuo stvari!",
   "empty_column.hashtag": "Još ne postoji ništa s ovim hashtagom.",
   "empty_column.home": "Još ne slijediš nikoga. Posjeti {public} ili koristi tražilicu kako bi počeo i upoznao druge korisnike.",
-  "empty_column.home.inactivity": "Tvoj home feed je prazan. Ako si neko vrijeme bio neaktivan, regenerirat će se uskoro.",
+  "empty_column.home.inactivity": "Tvoj home feed je prazan. Ako si neko vrijeme bio neaktivan, uskoro ćese regenerirati.",
   "empty_column.home.public_timeline": "javni timeline",
   "empty_column.notifications": "Još nemaš notifikacija. Komuniciraj sa drugima kako bi započeo razgovor.",
   "empty_column.public": "Ovdje nema ništa! Napiši nešto javno, ili ručno slijedi korisnike sa drugih instanci kako bi popunio",
@@ -88,11 +89,11 @@
   "getting_started.faq": "FAQ",
   "getting_started.heading": "Počnimo",
   "getting_started.open_source_notice": "Mastodon je softver otvorenog koda. Možeš pridonijeti ili prijaviti probleme na GitHubu  {github}.",
-  "getting_started.userguide": "Vodič za korisnike",
+  "getting_started.userguide": "Upute za korištenje",
   "home.column_settings.advanced": "Napredno",
   "home.column_settings.basic": "Osnovno",
   "home.column_settings.filter_regex": "Filtriraj s regularnim izrazima",
-  "home.column_settings.show_reblogs": "Pokaži boosts",
+  "home.column_settings.show_reblogs": "Pokaži boostove",
   "home.column_settings.show_replies": "Pokaži odgovore",
   "home.settings": "Postavke Stupca",
   "lightbox.close": "Zatvori",
@@ -113,7 +114,7 @@
   "navigation_bar.public_timeline": "Federalni timeline",
   "notification.favourite": "{name} je lajkao tvoj status",
   "notification.follow": "{name} te sada slijedi",
-  "notification.mention": "{name} mentioned you",
+  "notification.mention": "{name} te je spomenuo",
   "notification.reblog": "{name} je podigao tvoj status",
   "notifications.clear": "Očisti notifikacije",
   "notifications.clear_confirmation": "Želiš li zaista obrisati sve svoje notifikacije?",
@@ -123,28 +124,28 @@
   "notifications.column_settings.mention": "Spominjanja:",
   "notifications.column_settings.push": "Push notifications",
   "notifications.column_settings.push_meta": "This device",
-  "notifications.column_settings.reblog": "Boosts:",
+  "notifications.column_settings.reblog": "Boostovi:",
   "notifications.column_settings.show": "Prikaži u stupcu",
   "notifications.column_settings.sound": "Sviraj zvuk",
   "onboarding.done": "Učinjeno",
-  "onboarding.next": "Sljedeća",
-  "onboarding.page_five.public_timelines": "The local timeline prikazuje javne postove svih na {domain}. Federalni timeline pokazuje javne postove svih sa {domain} domena koje slijediš. To je sjajan način da otkriješ nove ljude.",
-  "onboarding.page_four.home": "The home timeline prikazuje samo postove ljudi koje slijediš.",
-  "onboarding.page_four.notifications": "Stupac notifikacija pokazuje kada je netko u interakciji s tobom.",
-  "onboarding.page_one.federation": "Mastodon je mreža nezavisnih servera udruženih kako bi stvorili veću socijalnu mrežu. Te servere zovemo instance.",
-  "onboarding.page_one.handle": "Ti si na {domain}, tako da je tvoj potpuni opis {handle}",
-  "onboarding.page_one.welcome": "Dobro došli u Mastodon!",
+  "onboarding.next": "Sljedeće",
+  "onboarding.page_five.public_timelines": "Lokalni timeline prikazuje javne postove sviju od svakog na {domain}. Federalni timeline prikazuje javne postove svakog koga ljudi na {domain} slijede. To su Javni Timelineovi, sjajan način za otkriti nove ljude.",
+  "onboarding.page_four.home": "The home timeline prikazuje postove ljudi koje slijediš.",
+  "onboarding.page_four.notifications": "Stupac za notifikacije pokazuje poruke drugih upućene tebi.",
+  "onboarding.page_one.federation": "Mastodon čini mreža  neovisnih servera udruženih u jednu veću socialnu mrežu. Te servere nazivamo instancama.",
+  "onboarding.page_one.handle": "Ti si na {domain}, i tvoja puna handle je {handle}",
+  "onboarding.page_one.welcome": "Dobro došli na Mastodon!",
   "onboarding.page_six.admin": "Administrator tvoje instance je {admin}.",
   "onboarding.page_six.almost_done": "Još malo pa gotovo...",
   "onboarding.page_six.appetoot": "Živjeli!",
   "onboarding.page_six.apps_available": "Postoje {apps} dostupne za iOS, Android i druge platforme.",
-  "onboarding.page_six.github": "Mastodon je besplatan softver otvorenog koda. Možeš prijaviti greške, zahtijevati mogućnosti, ili pridonijeti kodu na {github}.",
+  "onboarding.page_six.github": "Mastodon je besplatan softver otvorenog koda. You can report bugs, request features, or contribute to the code on {github}.",
   "onboarding.page_six.guidelines": "smjernice zajednice",
-  "onboarding.page_six.read_guidelines": "Molimo, pročitaj {domain}'s {guidelines}!",
+  "onboarding.page_six.read_guidelines": "Molimo pročitaj {domain}'s {guidelines}!",
   "onboarding.page_six.various_app": "mobilne aplikacije",
-  "onboarding.page_three.profile": "Uredi svoj profil mijenjanjem avatara, biografije i imena koje će biti prikazano. Naći ćeš i druge korisne postavke.",
-  "onboarding.page_three.search": "Koristi tražilicu kako bi pronašao ljude i sadržaj sa određenim hashtagovima, kao što su {illustration} i {introductions}. Da bi našao osobu koja nije na ovoj instanci, upotrijebi njihov puni opis.",
-  "onboarding.page_two.compose": "Piši postove u stupcu za njihovo sastavljanje. Možeš uploadati slike, promijeniti postavke privatnosti, i dodati upozorenja o sadržaju s ikonama ispod.",
+  "onboarding.page_three.profile": "Uredi svoj profil promjenom svog avatara, biografije, i imena. Ovdje ćeš isto tako pronaći i druge postavke.",
+  "onboarding.page_three.search": "Koristi tražilicu kako bi pronašao ljude i tražio hashtags, kao što su {illustration} i {introductions}. Kako bi pronašao osobu koja nije na ovoj instanci, upotrijebi njen pun handle.",
+  "onboarding.page_two.compose": "Piši postove u stupcu za sastavljanje. Možeš uploadati slike, promijeniti postavke privatnosti, i dodati upozorenja o sadržaju s ikonama ispod.",
   "onboarding.skip": "Preskoči",
   "privacy.change": "Podesi status privatnosti",
   "privacy.direct.long": "Prikaži samo spomenutim korisnicima",
@@ -162,7 +163,7 @@
   "search.placeholder": "Traži",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "standalone.public_title": "A look inside...",
-  "status.cannot_reblog": "Ovaj post ne može biti podignut",
+  "status.cannot_reblog": "Ovaj post ne može biti boostan",
   "status.delete": "Obriši",
   "status.embed": "Embed",
   "status.favourite": "Označi omiljenim",
@@ -196,5 +197,5 @@
   "video_player.expand": "Proširi video",
   "video_player.toggle_sound": "Toggle zvuk",
   "video_player.toggle_visible": "Preklopi vidljivost",
-  "video_player.video_error": "Video nije mogao biti prikazan"
+  "video_player.video_error": "Video ne može biti reproduciran"
 }
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 141bff042..a86033e6f 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -12,7 +12,7 @@
   "account.mute": "Rescondre @{name}",
   "account.posts": "Estatuts",
   "account.report": "Senhalar @{name}",
-  "account.requested": "Invitacion mandada",
+  "account.requested": "Invitacion mandada. Clicatz per anullar.",
   "account.share": "Partejar lo perfil a @{name}",
   "account.unblock": "Desblocar @{name}",
   "account.unblock_domain": "Desblocar {domain}",
@@ -63,8 +63,8 @@
   "confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?",
   "confirmations.unfollow.confirm": "Quitar de sègre",
   "confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
+  "embed.instructions": "Embarcar aqueste estatut per o far veire sus un site Internet en copiar lo còdi çai-jos.",
+  "embed.preview": "Semblarà aquò : ",
   "emoji_button.activity": "Activitats",
   "emoji_button.flags": "Drapèus",
   "emoji_button.food": "Beure e manjar",
@@ -164,7 +164,7 @@
   "standalone.public_title": "Una ulhada dedins…",
   "status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat",
   "status.delete": "Escafar",
-  "status.embed": "Embed",
+  "status.embed": "Embarcar",
   "status.favourite": "Apondre als favorits",
   "status.load_more": "Cargar mai",
   "status.media_hidden": "Mèdia rescondut",
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss
index 2f2d6e1f0..557355260 100644
--- a/app/javascript/styles/components.scss
+++ b/app/javascript/styles/components.scss
@@ -1885,6 +1885,10 @@
   &:hover {
     text-decoration: underline;
   }
+
+  &:last-child {
+    padding: 0 15px 0 0;
+  }
 }
 
 .column-back-button__icon {
@@ -2712,6 +2716,22 @@ button.icon-button.active i.fa-retweet {
   }
 }
 
+.media-spoiler__video {
+  align-items: center;
+  background: $base-overlay-background;
+  color: $primary-text-color;
+  cursor: pointer;
+  display: flex;
+  flex-direction: column;
+  border: 0;
+  width: 100%;
+  height: 100%;
+  justify-content: center;
+  position: relative;
+  text-align: center;
+  z-index: 100;
+}
+
 .media-spoiler__warning {
   display: block;
   font-size: 14px;
@@ -4483,41 +4503,10 @@ noscript {
   }
 }
 
-.embed-modal__html {
-  color: $ui-secondary-color;
-  outline: 0;
-  box-sizing: border-box;
-  display: block;
-  width: 100%;
-  border: none;
-  padding: 10px;
-  font-family: 'mastodon-font-monospace', monospace;
-  background: $ui-base-color;
-  color: $ui-primary-color;
-  font-size: 14px;
-  margin: 0;
-  margin-bottom: 15px;
-
-  &::-moz-focus-inner {
-    border: 0;
-  }
-
-  &::-moz-focus-inner,
-  &:focus,
-  &:active {
-    outline: 0 !important;
-  }
-
-  &:focus {
-    background: lighten($ui-base-color, 4%);
-  }
-
-  @media screen and (max-width: 600px) {
-    font-size: 16px;
-  }
-}
-
 .embed-modal {
+  max-width: 80vw;
+  max-height: 80vh;
+
   h4 {
     padding: 30px;
     font-weight: 500;
@@ -4525,18 +4514,52 @@ noscript {
     text-align: center;
   }
 
-  .hint {
-    margin-bottom: 15px;
-  }
-}
+  .embed-modal__container {
+    padding: 10px;
 
-.embed-modal__container {
-  padding: 10px;
-}
+    .hint {
+      margin-bottom: 15px;
+    }
 
-.embed-modal__iframe {
-  width: 100%;
-  min-width: 400px;
-  overflow: hidden;
-  border: 0;
+    .embed-modal__html {
+      color: $ui-secondary-color;
+      outline: 0;
+      box-sizing: border-box;
+      display: block;
+      width: 100%;
+      border: none;
+      padding: 10px;
+      font-family: 'mastodon-font-monospace', monospace;
+      background: $ui-base-color;
+      color: $ui-primary-color;
+      font-size: 14px;
+      margin: 0;
+      margin-bottom: 15px;
+
+      &::-moz-focus-inner {
+        border: 0;
+      }
+
+      &::-moz-focus-inner,
+      &:focus,
+      &:active {
+        outline: 0 !important;
+      }
+
+      &:focus {
+        background: lighten($ui-base-color, 4%);
+      }
+
+      @media screen and (max-width: 600px) {
+        font-size: 16px;
+      }
+    }
+
+    .embed-modal__iframe {
+      width: 400px;
+      max-width: 100%;
+      overflow: hidden;
+      border: 0;
+    }
+  }
 }
diff --git a/app/javascript/styles/stream_entries.scss b/app/javascript/styles/stream_entries.scss
index 00e430184..35225c045 100644
--- a/app/javascript/styles/stream_entries.scss
+++ b/app/javascript/styles/stream_entries.scss
@@ -399,51 +399,54 @@
 
 .embed {
   .activity-stream {
-    border-radius: 4px;
     box-shadow: none;
 
     .entry {
-      &:last-child {
-        border-radius: 0 0 4px 4px;
-      }
 
-      &:first-child {
-        border-radius: 4px 4px 0 0;
+      .detailed-status.light {
+        display: flex;
+        flex-wrap: wrap;
+        justify-content: space-between;
+        align-items: flex-start;
 
-        &:last-child {
-          border-radius: 4px;
+        .detailed-status__display-name {
+          flex: 1;
+          margin: 0 5px 15px 0;
         }
-      }
-    }
-  }
-}
 
-.button.button-secondary.logo-button {
-  position: absolute;
-  right: 14px;
-  top: 14px;
-  font-size: 14px;
+        .button.button-secondary.logo-button {
+          flex: 0 auto;
+          font-size: 14px;
 
-  svg {
-    width: 20px;
-    height: auto;
-    vertical-align: middle;
-    margin-right: 5px;
+          svg {
+            width: 20px;
+            height: auto;
+            vertical-align: middle;
+            margin-right: 5px;
 
-    path:first-child {
-      fill: $ui-primary-color;
-    }
+            path:first-child {
+              fill: $ui-primary-color;
+            }
 
-    path:last-child {
-      fill: $simple-background-color;
-    }
-  }
+            path:last-child {
+              fill: $simple-background-color;
+            }
+          }
 
-  &:active,
-  &:focus,
-  &:hover {
-    svg path:first-child {
-      fill: lighten($ui-primary-color, 4%);
+          &:active,
+          &:focus,
+          &:hover {
+            svg path:first-child {
+              fill: lighten($ui-primary-color, 4%);
+            }
+          }
+        }
+
+        .status__content,
+        .detailed-status__meta {
+          flex: 100%;
+        }
+      }
     }
   }
 }
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index 081e80570..9a34484f5 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -33,7 +33,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
   def status_params
     {
       uri: @object['id'],
-      url: @object['url'] || @object['id'],
+      url: object_url || @object['id'],
       account: @account,
       text: text_from_content || '',
       language: language_from_content,
@@ -147,6 +147,16 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     @object['contentMap'].keys.first
   end
 
+  def object_url
+    return if @object['url'].blank?
+
+    value = first_of_value(@object['url'])
+
+    return value if value.is_a?(String)
+
+    value['href']
+  end
+
   def language_map?
     @object['contentMap'].is_a?(Hash) && !@object['contentMap'].empty?
   end
diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb
index 81fae4140..b8e22a381 100644
--- a/app/lib/ostatus/atom_serializer.rb
+++ b/app/lib/ostatus/atom_serializer.rb
@@ -65,7 +65,7 @@ class OStatus::AtomSerializer
 
     add_namespaces(entry) if root
 
-    append_element(entry, 'id', TagManager.instance.unique_tag(stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type))
+    append_element(entry, 'id', TagManager.instance.uri_for(stream_entry.status))
     append_element(entry, 'published', stream_entry.created_at.iso8601)
     append_element(entry, 'updated', stream_entry.updated_at.iso8601)
     append_element(entry, 'title', stream_entry&.status&.title || "#{stream_entry.account.acct} deleted status")
@@ -86,7 +86,7 @@ class OStatus::AtomSerializer
       serialize_status_attributes(entry, stream_entry.status)
     end
 
-    append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: account_stream_entry_url(stream_entry.account, stream_entry))
+    append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(stream_entry.status))
     append_element(entry, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom'))
     append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(stream_entry.thread), href: TagManager.instance.url_for(stream_entry.thread)) if stream_entry.threaded?
     append_element(entry, 'ostatus:conversation', nil, ref: conversation_uri(stream_entry.status.conversation)) unless stream_entry&.status&.conversation_id.nil?
diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb
index 5f87a2a48..f33a20c6f 100644
--- a/app/lib/tag_manager.rb
+++ b/app/lib/tag_manager.rb
@@ -49,12 +49,17 @@ class TagManager
 
   def unique_tag_to_local_id(tag, expected_type)
     return nil unless local_id?(tag)
-    matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag)
-    return matches[1] unless matches.nil?
+
+    if ActivityPub::TagManager.instance.local_uri?(tag)
+      ActivityPub::TagManager.instance.uri_to_local_id(tag)
+    else
+      matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag)
+      return matches[1] unless matches.nil?
+    end
   end
 
   def local_id?(id)
-    id.start_with?("tag:#{Rails.configuration.x.local_domain}")
+    id.start_with?("tag:#{Rails.configuration.x.local_domain}") || ActivityPub::TagManager.instance.local_uri?(id)
   end
 
   def web_domain?(domain)
@@ -92,7 +97,7 @@ class TagManager
     when :person
       account_url(target)
     when :note, :comment, :activity
-      unique_tag(target.created_at, target.id, 'Status')
+      target.uri || unique_tag(target.created_at, target.id, 'Status')
     end
   end
 
diff --git a/app/models/status.rb b/app/models/status.rb
index f44f79aaf..fdc230d8f 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -22,6 +22,7 @@
 #  reblogs_count          :integer          default(0), not null
 #  language               :string
 #  conversation_id        :integer
+#  local                  :boolean
 #
 
 class Status < ApplicationRecord
@@ -84,7 +85,7 @@ class Status < ApplicationRecord
   end
 
   def local?
-    uri.nil?
+    attributes['local'] || uri.nil?
   end
 
   def reblog?
@@ -131,11 +132,14 @@ class Status < ApplicationRecord
     !sensitive? && media_attachments.any?
   end
 
+  after_create :store_uri, if: :local?
+
   before_validation :prepare_contents, if: :local?
   before_validation :set_reblog
   before_validation :set_visibility
   before_validation :set_conversation
   before_validation :set_sensitivity
+  before_validation :set_local
 
   class << self
     def not_in_filtered_languages(account)
@@ -253,6 +257,10 @@ class Status < ApplicationRecord
 
   private
 
+  def store_uri
+    update_attribute(:uri, ActivityPub::TagManager.instance.uri_for(self)) if uri.nil?
+  end
+
   def prepare_contents
     text&.strip!
     spoiler_text&.strip!
@@ -292,4 +300,8 @@ class Status < ApplicationRecord
       thread.account_id
     end
   end
+
+  def set_local
+    self.local = account.local?
+  end
 end
diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb
index 657807863..a30558bac 100644
--- a/app/presenters/account_relationships_presenter.rb
+++ b/app/presenters/account_relationships_presenter.rb
@@ -4,12 +4,12 @@ class AccountRelationshipsPresenter
   attr_reader :following, :followed_by, :blocking,
               :muting, :requested, :domain_blocking
 
-  def initialize(account_ids, current_account_id)
-    @following       = Account.following_map(account_ids, current_account_id)
-    @followed_by     = Account.followed_by_map(account_ids, current_account_id)
-    @blocking        = Account.blocking_map(account_ids, current_account_id)
-    @muting          = Account.muting_map(account_ids, current_account_id)
-    @requested       = Account.requested_map(account_ids, current_account_id)
-    @domain_blocking = Account.domain_blocking_map(account_ids, current_account_id)
+  def initialize(account_ids, current_account_id, options = {})
+    @following       = Account.following_map(account_ids, current_account_id).merge(options[:following_map] || {})
+    @followed_by     = Account.followed_by_map(account_ids, current_account_id).merge(options[:followed_by_map] || {})
+    @blocking        = Account.blocking_map(account_ids, current_account_id).merge(options[:blocking_map] || {})
+    @muting          = Account.muting_map(account_ids, current_account_id).merge(options[:muting_map] || {})
+    @requested       = Account.requested_map(account_ids, current_account_id).merge(options[:requested_map] || {})
+    @domain_blocking = Account.domain_blocking_map(account_ids, current_account_id).merge(options[:domain_blocking_map] || {})
   end
 end
diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb
index 25521eca9..a11178f5b 100644
--- a/app/serializers/activitypub/actor_serializer.rb
+++ b/app/serializers/activitypub/actor_serializer.rb
@@ -4,7 +4,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
   include RoutingHelper
 
   attributes :id, :type, :following, :followers,
-             :inbox, :outbox, :shared_inbox,
+             :inbox, :outbox,
              :preferred_username, :name, :summary,
              :url, :manually_approves_followers
 
@@ -24,6 +24,18 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
     end
   end
 
+  class EndpointsSerializer < ActiveModel::Serializer
+    include RoutingHelper
+
+    attributes :shared_inbox
+
+    def shared_inbox
+      inbox_url
+    end
+  end
+
+  has_one :endpoints, serializer: EndpointsSerializer
+
   has_one :icon,  serializer: ImageSerializer, if: :avatar_exists?
   has_one :image, serializer: ImageSerializer, if: :header_exists?
 
@@ -51,8 +63,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
     account_outbox_url(object)
   end
 
-  def shared_inbox
-    inbox_url
+  def endpoints
+    object
   end
 
   def preferred_username
diff --git a/app/serializers/oembed_serializer.rb b/app/serializers/oembed_serializer.rb
index 4f9293043..bd05da585 100644
--- a/app/serializers/oembed_serializer.rb
+++ b/app/serializers/oembed_serializer.rb
@@ -40,8 +40,7 @@ class OEmbedSerializer < ActiveModel::Serializer
     attributes = {
       src: embed_short_account_status_url(object.account, object),
       class: 'mastodon-embed',
-      frameborder: '0',
-      scrolling: 'no',
+      style: 'max-width: 100%; border: none;',
       width: width,
       height: height,
     }
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index a26b39cb5..29eb1c2e1 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -6,7 +6,7 @@ class ActivityPub::ProcessAccountService < BaseService
   # Should be called with confirmed valid JSON
   # and WebFinger-resolved username and domain
   def call(username, domain, json)
-    return unless json['inbox'].present?
+    return if json['inbox'].blank?
 
     @json     = json
     @uri      = @json['id']
@@ -42,9 +42,9 @@ class ActivityPub::ProcessAccountService < BaseService
     @account.protocol            = :activitypub
     @account.inbox_url           = @json['inbox'] || ''
     @account.outbox_url          = @json['outbox'] || ''
-    @account.shared_inbox_url    = @json['sharedInbox'] || ''
+    @account.shared_inbox_url    = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
     @account.followers_url       = @json['followers'] || ''
-    @account.url                 = @json['url'] || @uri
+    @account.url                 = url || @uri
     @account.display_name        = @json['name'] || ''
     @account.note                = @json['summary'] || ''
     @account.avatar_remote_url   = image_url('icon')
@@ -62,7 +62,7 @@ class ActivityPub::ProcessAccountService < BaseService
     value = first_of_value(@json[key])
 
     return if value.nil?
-    return @json[key]['url'] if @json[key].is_a?(Hash)
+    return value['url'] if value.is_a?(Hash)
 
     image = fetch_resource(value)
     image['url'] if image
@@ -78,6 +78,16 @@ class ActivityPub::ProcessAccountService < BaseService
     key['publicKeyPem'] if key
   end
 
+  def url
+    return if @json['url'].blank?
+
+    value = first_of_value(@json['url'])
+
+    return value if value.is_a?(String)
+
+    value['href']
+  end
+
   def auto_suspend?
     domain_block && domain_block.suspend?
   end
diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb
index dc386c9e7..f123bf869 100644
--- a/app/services/process_mentions_service.rb
+++ b/app/services/process_mentions_service.rb
@@ -41,7 +41,7 @@ class ProcessMentionsService < BaseService
       NotifyService.new.call(mentioned_account, mention)
     elsif mentioned_account.ostatus? && (Rails.configuration.x.use_ostatus_privacy || !status.stream_entry.hidden?)
       NotificationWorker.perform_async(stream_entry_to_xml(status.stream_entry), status.account_id, mentioned_account.id)
-    elsif mentioned_account.activitypub? && !mentioned_account.following?(status.account)
+    elsif mentioned_account.activitypub?
       ActivityPub::DeliveryWorker.perform_async(build_json(mention.status), mention.status.account_id, mentioned_account.inbox_url)
     end
   end
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index a157090e0..6fd39c88e 100755
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -21,13 +21,13 @@
     = stylesheet_pack_tag 'common', media: 'all'
     = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous'
 
-    = javascript_pack_tag 'features/getting_started', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
-    = javascript_pack_tag 'features/compose', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
-    = javascript_pack_tag 'features/home_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
-    = javascript_pack_tag 'features/notifications', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
-    = javascript_pack_tag 'features/community_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
-    = javascript_pack_tag 'features/public_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
-    = javascript_pack_tag 'emojione_picker', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script'
+    %link{ href: asset_pack_path('features/getting_started.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
+    %link{ href: asset_pack_path('features/compose.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
+    %link{ href: asset_pack_path('features/home_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
+    %link{ href: asset_pack_path('features/notifications.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
+    %link{ href: asset_pack_path('features/community_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
+    %link{ href: asset_pack_path('features/public_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
+    %link{ href: asset_pack_path('emojione_picker.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
 
     = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous'
     = csrf_meta_tags
diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml
index ab803eebd..1d943a2ca 100644
--- a/app/views/stream_entries/_detailed_status.html.haml
+++ b/app/views/stream_entries/_detailed_status.html.haml
@@ -1,9 +1,4 @@
 .detailed-status.light
-  - if embedded_view?
-    = link_to "web+mastodon://follow?uri=#{status.account.local_username_and_domain}", class: 'button button-secondary logo-button', target: '_new' do
-      = render file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')
-      = t('accounts.follow')
-
   = link_to TagManager.instance.url_for(status.account), class: 'detailed-status__display-name p-author h-card', target: stream_link_target, rel: 'noopener' do
     %div
       .avatar
@@ -12,6 +7,11 @@
       %strong.p-name.emojify= display_name(status.account)
       %span= acct(status.account)
 
+  - if embedded_view?
+    = link_to "web+mastodon://follow?uri=#{status.account.local_username_and_domain}", class: 'button button-secondary logo-button', target: '_new' do
+      = render file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')
+      = t('accounts.follow')
+
   .status__content.p-name.emojify<
     - if status.spoiler_text?
       %p{ style: 'margin-bottom: 0' }<
diff --git a/app/workers/pubsubhubbub/distribution_worker.rb b/app/workers/pubsubhubbub/distribution_worker.rb
index 2a5e60fa0..524f6849f 100644
--- a/app/workers/pubsubhubbub/distribution_worker.rb
+++ b/app/workers/pubsubhubbub/distribution_worker.rb
@@ -6,7 +6,7 @@ class Pubsubhubbub::DistributionWorker
   sidekiq_options queue: 'push'
 
   def perform(stream_entry_ids)
-    stream_entries = StreamEntry.where(id: stream_entry_ids).includes(:status).reject { |e| e.status&.direct_visibility? }
+    stream_entries = StreamEntry.where(id: stream_entry_ids).includes(:status).reject { |e| e.status.nil? || e.status.direct_visibility? }
 
     return if stream_entries.empty?
 
diff --git a/app/workers/pubsubhubbub/subscribe_worker.rb b/app/workers/pubsubhubbub/subscribe_worker.rb
index 7560c2671..130c967e0 100644
--- a/app/workers/pubsubhubbub/subscribe_worker.rb
+++ b/app/workers/pubsubhubbub/subscribe_worker.rb
@@ -3,7 +3,7 @@
 class Pubsubhubbub::SubscribeWorker
   include Sidekiq::Worker
 
-  sidekiq_options queue: 'push', retry: 10, unique: :until_executed, dead: false
+  sidekiq_options queue: 'push', retry: 10, unique: :until_executed, dead: false, unique_retry: true
 
   sidekiq_retry_in do |count|
     case count
diff --git a/app/workers/resolve_remote_account_worker.rb b/app/workers/resolve_remote_account_worker.rb
new file mode 100644
index 000000000..5dd84ccb6
--- /dev/null
+++ b/app/workers/resolve_remote_account_worker.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class ResolveRemoteAccountWorker
+  include Sidekiq::Worker
+
+  sidekiq_options queue: 'pull', unique: :until_executed
+
+  def perform(uri)
+    ResolveRemoteAccountService.new.call(uri)
+  end
+end
diff --git a/config/application.rb b/config/application.rb
index eb89f0a10..49382c2b9 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -10,6 +10,7 @@ require_relative '../app/lib/exceptions'
 require_relative '../lib/paperclip/gif_transcoder'
 require_relative '../lib/paperclip/video_transcoder'
 require_relative '../lib/mastodon/version'
+require_relative '../lib/mastodon/unique_retry_job_middleware'
 
 Dotenv::Railtie.load
 
diff --git a/config/initializers/ostatus.rb b/config/initializers/ostatus.rb
index a885545f8..c00aba0de 100644
--- a/config/initializers/ostatus.rb
+++ b/config/initializers/ostatus.rb
@@ -12,6 +12,7 @@ Rails.application.configure do
   config.x.web_domain   = web_host
   config.x.use_https    = https
   config.x.use_s3       = ENV['S3_ENABLED'] == 'true'
+  config.x.use_swift    = ENV['SWIFT_ENABLED'] == 'true'
 
   config.x.alternate_domains = alternate_domains.split(/\s*,\s*/)
 
diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb
index 740c1a953..e9f455251 100644
--- a/config/initializers/paperclip.rb
+++ b/config/initializers/paperclip.rb
@@ -40,6 +40,21 @@ if ENV['S3_ENABLED'] == 'true'
     Paperclip::Attachment.default_options[:url]           = ':s3_alias_url'
     Paperclip::Attachment.default_options[:s3_host_alias] = ENV['S3_CLOUDFRONT_HOST']
   end
+elsif ENV['SWIFT_ENABLED'] == 'true'
+  Paperclip::Attachment.default_options.merge!(
+    path: ':class/:attachment/:id_partition/:style/:filename',
+    storage: :fog,
+    fog_credentials: {
+      provider: 'OpenStack',
+      openstack_username: ENV.fetch('SWIFT_USERNAME'),
+      openstack_tenant: ENV.fetch('SWIFT_TENANT'),
+      openstack_api_key: ENV.fetch('SWIFT_PASSWORD'),
+      openstack_auth_url: ENV.fetch('SWIFT_AUTH_URL'),
+    },
+    fog_directory: ENV.fetch('SWIFT_CONTAINER'),
+    fog_host: ENV.fetch('SWIFT_OBJECT_URL'),
+    fog_public: true
+  )
 else
   Paperclip::Attachment.default_options[:path] = (ENV['PAPERCLIP_ROOT_PATH'] || ':rails_root/public/system') + '/:class/:attachment/:id_partition/:style/:filename'
   Paperclip::Attachment.default_options[:url]  = (ENV['PAPERCLIP_ROOT_URL'] || '/system') + '/:class/:attachment/:id_partition/:style/:filename'
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index b70784d79..61e131336 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -13,4 +13,7 @@ end
 
 Sidekiq.configure_client do |config|
   config.redis = redis_params
+  config.client_middleware do |chain|
+    chain.add Mastodon::UniqueRetryJobMiddleware
+  end
 end
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index 575c5114c..604b09600 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -47,16 +47,16 @@ ar:
   datetime:
     distance_in_words:
       about_x_hours: "%{count}سا"
-      about_x_months: "%{count}شهر"
-      about_x_years: "%{count}سنة"
-      almost_x_years: "%{count}سنوات"
-      half_a_minute: Just now
-      less_than_x_minutes: "%{count}د"
+      about_x_months: "%{count} شهر"
+      about_x_years: "%{count} سنة"
+      almost_x_years: "%{count} سنوات"
+      half_a_minute: الآن
+      less_than_x_minutes: "%{count} د"
       less_than_x_seconds: الآن
-      over_x_years: "%{count}سنين"
-      x_days: "%{count}أيام"
+      over_x_years: "%{count} سنين"
+      x_days: "%{count} أيام"
       x_minutes: "%{count}د"
-      x_months: "%{count}شه"
+      x_months: "%{count} شه"
       x_seconds: "%{count}ث"
   exports:
     blocks: قمت بحظر
@@ -94,7 +94,7 @@ ar:
         one: "إشعار واحد منذ زيارتك الأخيرة \U0001F418"
         other: "%{count} إشعارات جديدة منذ زيارتك الأخيرة \U0001F418"
     favourite:
-      body: 'Your status was favourited by %{name}:'
+      body: 'أُعجب %{name} بمنشورك'
       subject: "%{name} favourited your status"
     follow:
       body: "%{name} من متتبعيك الآن !"
@@ -108,6 +108,17 @@ ar:
     reblog:
       body: 'Your status was boosted by %{name}:'
       subject: "%{name} boosted your status"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: التالي
     prev: السابق
@@ -148,7 +159,7 @@ ar:
     enabled_success: تم تفعيل إثبات الهوية المزدوج بنجاح
     instructions_html: "<strong>Scan this QR code into Google Authenticator or a similiar TOTP app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in."
     manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:'
-    setup: Set up
+    setup: تنشيط
     wrong_code: الرمز الذي أدخلته غير صالح. تحقق من صحة الوقت على الخادم و الجهاز.
   users:
     invalid_email: عنوان البريد الإلكتروني غير صالح
diff --git a/config/locales/bg.yml b/config/locales/bg.yml
index e7c3e1ef6..13d0394a3 100644
--- a/config/locales/bg.yml
+++ b/config/locales/bg.yml
@@ -108,6 +108,17 @@ bg:
     reblog:
       body: 'Твоята публикация беше споделена от %{name}:'
       subject: "%{name} сподели публикацията ти"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Напред
     prev: Назад
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index b6bff8288..6a92b7f1b 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -340,6 +340,17 @@ ca:
     reblog:
       body: "%{name} ha retootejat el teu estat"
       subject: "%{name} ha retootejat el teu estat"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Pròxim
     prev: Anterior
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 1f3675f47..de6c86737 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -12,15 +12,15 @@ de:
     source_code: Quellcode
     status_count_after: Beiträge verfassten
     status_count_before: die
-    user_count_after: Benutzer
+    user_count_after: Profile
     user_count_before: Heimat für
   accounts:
     follow: Folgen
     followers: Folgende
     following: Folgt
     nothing_here: Hier gibt es nichts!
-    people_followed_by: Nutzer, denen %{name} folgt
-    people_who_follow: Nutzer, die %{name} folgen
+    people_followed_by: Profile, denen %{name} folgt
+    people_who_follow: Profile, die %{name} folgen
     posts: Beiträge
     remote_follow: Folgen
     unfollow: Entfolgen
@@ -67,7 +67,7 @@ de:
       title: Konten
       undo_silenced: Stummschaltung zurücknehmen
       undo_suspension: Sperre zurücknehmen
-      username: Benutzername
+      username: Profilname
       web: Web
     domain_blocks:
       add_new: Neu hinzufügen
@@ -124,7 +124,7 @@ de:
     settings:
       contact_information:
         email: Eine öffentliche E-Mail-Adresse angeben
-        username: Einen Benutzernamen angeben
+        username: Einen Profilnamen angeben
       registrations:
         closed_message:
           desc_html: Wird auf der Frontseite angezeigt, wenn die Registrierung geschlossen ist<br>Du kannst HTML-Tags benutzen
@@ -208,7 +208,7 @@ de:
       following: Folgeliste
       muting: Stummschaltungsliste
     upload: Hochladen
-  landing_strip_html: "<strong>%{name}</strong> ist ein Benutzer auf %{link_to_root_path}. Du kannst ihm folgen oder mit ihm interagieren, sofern du ein Konto irgendwo in der Fediverse hast."
+  landing_strip_html: "<strong>%{name}</strong> hat ein Profil auf %{link_to_root_path}. Du kannst folgen oder interagieren, sofern du ein Konto irgendwo im Fediversum hast."
   landing_strip_signup_html: Wenn nicht, kannst du dich <a href="%{sign_up_path}">hier anmelden</a>.
   media_attachments:
     validations:
@@ -239,12 +239,23 @@ de:
     reblog:
       body: 'Dein Beitrag wurde von %{name} geteilt:'
       subject: "%{name} teilte deinen Beitrag."
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Vorwärts
     prev: Zurück
     truncate: "&hellip;"
   remote_follow:
-    acct: Dein Nutzername@Domain, von dem aus du dieser Person folgen möchtest.
+    acct: Dein Profilname@Domain, von dem aus du dieser Person folgen möchtest.
     missing_resource: Die erforderliche Weiterleitungs-URL konnte leider in deinem Profil nicht gefunden werden.
     proceed: Weiter
     prompt: 'Du wirst dieser Person folgen:'
diff --git a/config/locales/doorkeeper.de.yml b/config/locales/doorkeeper.de.yml
index b37ba1dbe..b0ba2fb98 100644
--- a/config/locales/doorkeeper.de.yml
+++ b/config/locales/doorkeeper.de.yml
@@ -77,7 +77,7 @@ de:
         invalid_grant: Die bereitgestellte Autorisierung ist inkorrekt, abgelaufen, widerrufen, ist mit einem anderen Client verknüpft oder der Redirection URI stimmt nicht mit der Autorisierungs-Anfrage überein.
         invalid_redirect_uri: Der Redirect-URI in der Anfrage ist ungültig.
         invalid_request: Die Anfrage enthält einen nicht-unterstützten Parameter, ein Parameter fehlt oder sie ist anderweitig fehlerhaft.
-        invalid_resource_owner: Die angegebenen Zugangsdaten für den "Resource Owner" sind inkorrekt oder dieser Benutzer existiert nicht.
+        invalid_resource_owner: Die angegebenen Zugangsdaten für den "Resource Owner" sind inkorrekt oder dieses Profil existiert nicht.
         invalid_scope: Der angeforderte Scope ist inkorrekt, unbekannt oder fehlerhaft.
         invalid_token:
           expired: Der Zugriffstoken ist abgelaufen
@@ -108,6 +108,6 @@ de:
       application:
         title: OAuth-Autorisierung nötig
     scopes:
-      follow: Nutzer folgen, blocken, entblocken und entfolgen
+      follow: Profil folgen, blocken, entblocken und entfolgen
       read: deine Daten lesen
       write: Beiträge von deinem Konto aus veröffentlichen
diff --git a/config/locales/doorkeeper.fa.yml b/config/locales/doorkeeper.fa.yml
index 33f453a3f..343580530 100644
--- a/config/locales/doorkeeper.fa.yml
+++ b/config/locales/doorkeeper.fa.yml
@@ -3,8 +3,10 @@ fa:
   activerecord:
     attributes:
       doorkeeper/application:
-        name: Name
+        name: Application name
         redirect_uri: Redirect URI
+        scopes: Scopes
+        website: Application website
     errors:
       models:
         doorkeeper/application:
@@ -33,18 +35,22 @@ fa:
         redirect_uri: Use one line per URI
         scopes: Separate scopes with spaces. Leave blank to use the default scopes.
       index:
+        application: Application
         callback_url: Callback URL
+        delete: Delete
         name: Name
-        new: New Application
+        new: New application
+        scopes: Scopes
+        show: Show
         title: Your applications
       new:
-        title: New Application
+        title: New application
       show:
         actions: Actions
-        application_id: Application Id
-        callback_urls: Callback urls
+        application_id: Client key
+        callback_urls: Callback URLs
         scopes: Scopes
-        secret: Secret
+        secret: Client secret
         title: 'Application: %{name}'
     authorizations:
       buttons:
diff --git a/config/locales/doorkeeper.oc.yml b/config/locales/doorkeeper.oc.yml
index 3d12c9588..b6aebea48 100644
--- a/config/locales/doorkeeper.oc.yml
+++ b/config/locales/doorkeeper.oc.yml
@@ -5,6 +5,8 @@ oc:
       doorkeeper/application:
         name: Nom
         redirect_uri: URL de redireccion
+        scopes: Encastres
+        website: Aplicacion web
     errors:
       models:
         doorkeeper/application:
@@ -33,9 +35,13 @@ oc:
         redirect_uri: Utilizatz una linha per URI
         scopes: Separatz los encastres amb d’espacis. Daissatz void per utilizar l’encastre per defaut.
       index:
+        application: Aplicacion
         callback_url: URL de rapèl
+        delete: Suprimir
         name: Nom
         new: Nòva aplicacion
+        scopes: Encastres
+        show: Veire
         title: Vòstras aplicacions
       new:
         title: Nòva aplicacion
diff --git a/config/locales/eo.yml b/config/locales/eo.yml
index f8b5ec0ac..21def0c5f 100644
--- a/config/locales/eo.yml
+++ b/config/locales/eo.yml
@@ -103,6 +103,17 @@ eo:
     reblog:
       body: "%{name} diskonigis vian mesaĝon:"
       subject: "%{name} diskonigis vian mesaĝon"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Sekva
     prev: Malsekva
diff --git a/config/locales/es.yml b/config/locales/es.yml
index d2d1de14f..a02330521 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -108,6 +108,17 @@ es:
     reblog:
       body: "%{name} ha retooteado tu estado"
       subject: "%{name} ha retooteado tu estado"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Próximo
     prev: Anterior
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index 08ffb4484..ba726fc75 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -339,6 +339,17 @@ fa:
     reblog:
       body: "%{name} نوشتهٔ شما را بازبوقید:"
       subject: "%{name} نوشتهٔ شما را بازبوقید"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: بعدی
     prev: قبلی
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index b748f7184..08ae90447 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -103,6 +103,17 @@ fi:
     reblog:
       body: 'Sinun statustasi boostasi %{name}:'
       subject: "%{name} boostasi statustasi"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Seuraava
     prev: Edellinen
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 8029d8bd5..6198a5454 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -358,6 +358,17 @@ fr:
     reblog:
       body: "%{name} a partagé votre statut :"
       subject: "%{name} a partagé votre statut"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Suivant
     prev: Précédent
diff --git a/config/locales/he.yml b/config/locales/he.yml
index f04e8ad62..84d6d8468 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -264,6 +264,17 @@ he:
     reblog:
       body: 'חצרוצך הודהד על ידי %{name}:'
       subject: חצרוצך הודהד על ידי%{name}
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: הבא
     prev: הקודם
diff --git a/config/locales/hr.yml b/config/locales/hr.yml
index 52a8bd35f..581912420 100644
--- a/config/locales/hr.yml
+++ b/config/locales/hr.yml
@@ -105,6 +105,17 @@ hr:
     reblog:
       body: 'Tvoj status je potaknut od %{name}:'
       subject: "%{name} je potakao tvoj status"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Sljedeći
     prev: Prošli
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index 53319a673..77551223f 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -45,6 +45,17 @@ hu:
     reblog:
       body: 'Az állapotod reblogolta %{name}:'
       subject: "%{name} reblogolta az állapotod"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Következő
     prev: Előző
diff --git a/config/locales/id.yml b/config/locales/id.yml
index c76b3d6bb..f3a6649d1 100644
--- a/config/locales/id.yml
+++ b/config/locales/id.yml
@@ -254,6 +254,17 @@ id:
     reblog:
       body: 'Status anda di-boost oleh %{name}:'
       subject: "%{name} mem-boost status anda"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Selanjutnya
     prev: Sebelumnya
diff --git a/config/locales/io.yml b/config/locales/io.yml
index 112771ee4..4114e5231 100644
--- a/config/locales/io.yml
+++ b/config/locales/io.yml
@@ -239,6 +239,17 @@ io:
     reblog:
       body: "%{name} diskonocigis tua mesajo:"
       subject: "%{name} diskonocigis tua mesajo"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Sequanta
     prev: Preiranta
diff --git a/config/locales/it.yml b/config/locales/it.yml
index 75d56362a..ec0209bc1 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -108,6 +108,17 @@ it:
     reblog:
       body: 'Il tuo status è stato condiviso da %{name}:'
       subject: "%{name} ha condiviso il tuo status"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Avanti
     prev: Indietro
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index f98059526..6fdc3b985 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -1,29 +1,52 @@
 ---
 ko:
   about:
-    about_mastodon_html: Mastodon 은<em>자유로운 오픈 소스</em>소셜 네트워크입니다. 상용 플랫폼의 대체로써 <em>분산형 구조</em>를 채택해, 여러분의 대화가 한 회사에 독점되는 것을 방지합니다. 신뢰할 수 있는 인스턴스를 선택하세요 &mdash; 어떤 인스턴스를 고르더라도, 누구와도 대화할 수 있습니다. 누구나 자신만의 Mastodon 인스턴스를 만들 수 있으며, Seamless하게 <em>소셜 네트워크</em>에 참가할 수 있습니다.
+    about_mastodon_html: Mastodon은 <em>오픈 소스 기반의</em> 소셜 네트워크 서비스 입니다. 상용 플랫폼의 대체로서 <em>분산형 구조</em>를 채택해, 여러분의 대화가 한 회사에 독점되는 것을 방지합니다. 신뢰할 수 있는 인스턴스를 선택하세요 &mdash; 어떤 인스턴스를 고르더라도, 누구와도 대화할 수 있습니다. 누구나 자신만의 Mastodon 인스턴스를 만들 수 있으며, Seamless하게 <em>소셜 네트워크</em>에 참가할 수 있습니다.
     about_this: 이 인스턴스에 대해서
     closed_registrations: 현재 이 인스턴스에서는 신규 등록을 받고 있지 않습니다.
     contact: 연락처
-    description_headline: "%{domain} 는 무엇인가요?"
+    contact_missing: 미설정
+    contact_unavailable: N/A
+    description_headline: "%{domain} (은)는 무엇인가요?"
     domain_count_after: 개의 인스턴스
-    domain_count_before: 연결됨
+    domain_count_before: 연결된
+    extended_description_html: |
+      <h3>룰을 작성하는 장소</h3>
+      <p>아직 설명이 작성되지 않았습니다.</p>
+    features:
+      humane_approach_body: 다른 SNS의 실패를 교훈삼아, Mastodon은 소셜미디어가 잘못 사용되는 것을 막기 위하여 윤리적인 설계를 추구합니다.
+      humane_approach_title: 보다 배려를 의식한 설계를 추구
+      not_a_product_body: Mastodon은 이익을 추구하는 SNS가 아닙니다. 그러므로 광고와 데이터의 수집 및 분석이 존재하지 않고, 유저를 구속하지도 않습니다.
+      not_a_product_title: 여러분은 사람이며, 상품이 아닙니다.
+      real_conversation_body: 자유롭게 사용할 수 있는 500문자의 메세지와 미디어 경고 내용을 바탕으로, 자기자신을 자유롭게 표현할 수 있습니다.
+      real_conversation_title: 진정한 커뮤니케이션을 위하여
+      within_reach_body: 개발자 친화적인 API에 의해서 실현된 iOS나 Android, 그 외의 여러 Platform들 덕분에 어디서든 친구들과 자유롭게 메세지를 주고 받을 수 있습니다.
+      within_reach_title: 언제나 유저의 곁에서
+    find_another_instance: 다른 인스턴스 찾기
+    generic_description: "%{domain} 은 Mastodon의 인스턴스 입니다."
+    hosted_on: Mastodon hosted on %{domain}
+    learn_more: 자세히
     other_instances: 다른 인스턴스
     source_code: 소스 코드
     status_count_after: Toot
     status_count_before: Toot 수
     user_count_after: 명
     user_count_before: 사용자 수
+    what_is_mastodon: Mastodon이란?
   accounts:
     follow: 팔로우
     followers: 팔로워
     following: 팔로잉
+    media: 미디어
     nothing_here: 아무 것도 없습니다.
     people_followed_by: "%{name} 님이 팔로우 중인 계정"
     people_who_follow: "%{name} 님을 팔로우 중인 계정"
-    posts: 포스트
+    posts: Toot
+    posts_with_replies: Toot와 답장
     remote_follow: 리모트 팔로우
     reserved_username: 이 아이디는 예약되어 있습니다.
+    roles:
+      admin: Admin
     unfollow: 팔로우 해제
   admin:
     accounts:
@@ -38,6 +61,7 @@ ko:
       feed_url: 피드 URL
       followers: 팔로워 수
       follows: 팔로잉 수
+      inbox_url: Inbox URL
       ip: IP
       location:
         all: 전체
@@ -57,8 +81,10 @@ ko:
         alphabetic: 알파벳 순
         most_recent: 최근 활동 순
         title: 순서
+      outbox_url: Outbox URL
       perform_full_suspension: 완전히 정지시키기
       profile_url: 프로필 URL
+      protocol: Protocol
       public: 전체 공개
       push_subscription_expires: PuSH 구독 기간 만료
       redownload: 아바타 업데이트
@@ -90,12 +116,14 @@ ko:
         hint: 도메인 차단은 내부 데이터베이스에 계정이 생성되는 것까지는 막을 수 없지만, 그 도메인에서 생성된 계정에 자동적으로 특정한 모더레이션을 적용하게 할 수 있습니다.
         severity:
           desc_html: "<strong>침묵</strong>은 계정을 팔로우 하지 않고 있는 사람들에겐 계정의 Toot을 보이지 않게 합니다. <strong>정지</strong>는 계정의 컨텐츠, 미디어, 프로필 데이터를 삭제합니다."
+          noop: 없음
           silence: 침묵
           suspend: 정지
         title: 새로운 도메인 차단
       reject_media: 미디어 파일 거부하기
       reject_media_hint: 로컬에 저장된 미디어 파일을 삭제하고, 이후로도 다운로드를 거부합니다. 정지하고는 관계 없습니다.
       severities:
+        noop: 없음
         silence: 침묵
         suspend: 정지
       severity: 심각도
@@ -146,16 +174,41 @@ ko:
         closed_message:
           desc_html: 신규 등록을 받지 않을 때 프론트 페이지에 표시됩니다. <br>HTML 태그를 사용할 수 있습니다.
           title: 신규 등록 정지 시 메시지
+        deletion:
+          desc_html: 유저가 자신의 계정을 삭제할 수 있도록 설정합니다.
+          title: 계정 삭제를 허가함
         open:
-          title: 신규 등록을 받음
+          desc_html: 유저가 자신의 계정을 생성할 수 있도록 설정합니다.
+          title: 신규 계정 등록을 받음
       site_description:
         desc_html: 탑 페이지와 meta 태그에 사용됩니다.<br>HTML 태그, 예를 들어<code>&lt;a&gt;</code> 태그와 <code>&lt;em&gt;</code> 태그를 사용할 수 있습니다.
         title: 사이트 설명
       site_description_extended:
         desc_html: 인스턴스 정보 페이지에 표시됩니다.<br>HTML 태그를 사용할 수 있습니다.
         title: 사이트 상세 설명
+      site_terms:
+        desc_html: 당신은 독자적인 개인정보 취급 방침이나 이용약관, 그 외의 법적 근거를 작성할 수 있습니다. 또한 HTML태그를 사용할 수 있습니다.
+        title: 커스텀 서비스 이용 약관
       site_title: 사이트 이름
+      timeline_preview:
+        desc_html: Landing page에 공개 타임라인을 표시합니다.
+        title: 타임라인 프리뷰
       title: 사이트 설정
+    statuses:
+      back_to_account: 계정으로 돌아가기
+      batch:
+        delete: 삭제
+        nsfw_off: NSFW 끄기
+        nsfw_on: NSFW 켜기
+      execute: 실행
+      failed_to_execute: 실행이 실패하였습니다.
+      media:
+        hide: 미디어 숨기기
+        show: 미디어 보여주기
+        title: 미디어
+      no_media: 미디어 없음
+      title: 계정 Toot
+      with_media: 미디어 있음
     subscriptions:
       callback_url: 콜백 URL
       confirmed: 확인됨
@@ -173,13 +226,21 @@ ko:
     signature: Mastodon %{instance} 인스턴스로에서 알림
     view: 'View:'
   applications:
+    created: 어플리케이션이 작성되었습니다.
+    destroyed: 어플리케이션이 삭제되었습니다.
     invalid_url: 올바르지 않은 URL입니다
+    regenerate_token: 토큰 재생성
+    token_regenerated: 액세스 토큰이 재생성되었습니다.
+    warning: 이 데이터는 다른 사람들과 절대로 공유하지 마세요.
+    your_token: 액세스 토큰
   auth:
+    agreement_html: 이 등록으로 <a href="%{rules_path}">이용규약</a> 과 <a href="%{terms_path}">개인정보 취급 방침</a>에 동의하는 것으로 간주됩니다.
     change_password: 보안
     delete_account: 계정 삭제
     delete_account_html: 계정을 삭제하고 싶은 경우, <a href="%{path}">여기서</a> 삭제할 수 있습니다. 삭제 전 확인 화면이 표시됩니다.
     didnt_get_confirmation: 확인 메일을 받지 못하셨습니까?
     forgot_password: 비밀번호를 잊어버리셨습니까?
+    invalid_reset_password_token: 비밀번호 리셋 토큰이 올바르지 못하거나 기간이 만료되었습니다. 다시 요청해주세요.
     login: 로그인
     logout: 로그아웃
     register: 등록하기
@@ -189,6 +250,12 @@ ko:
   authorize_follow:
     error: 리모트 팔로우 도중 오류가 발생했습니다.
     follow: 팔로우
+    follow_request: '당신은 다음 계정에 팔로우 신청을 했습니다:'
+    following: '성공! 당신은 다음 계정을 팔로우 하고 있습니다:'
+    post_follow:
+      close: 혹은, 당신은 이 윈도우를 닫을 수 있습니다
+      return: 유저 프로필로 돌아가기
+      web: 웹으로 가기
     title: "%{acct} 를 팔로우"
   datetime:
     distance_in_words:
@@ -271,8 +338,8 @@ ko:
         one: "1건의 새로운 알림 \U0001F418"
         other: "%{count}건의 새로운 알림 \U0001F418"
     favourite:
-      body: "%{name} 님이 내 Toot을 즐겨찾기에 등록했습니다."
-      subject: "%{name} 님이 내 Toot을 즐겨찾기에 등록했습니다"
+      body: "%{name} 님이 내 Toot를 즐겨찾기에 등록했습니다."
+      subject: "%{name} 님이 내 Toot를 즐겨찾기에 등록했습니다"
     follow:
       body: "%{name} 님이 나를 팔로우 했습니다"
       subject: "%{name} 님이 나를 팔로우 했습니다"
@@ -285,10 +352,35 @@ ko:
     reblog:
       body: "%{name} 님이 내 Toot을 부스트 했습니다:"
       subject: "%{name} 님이 내 Toot을 부스트 했습니다"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: 다음
     prev: 이전
     truncate: "&hellip;"
+  push_notifications:
+    favourite:
+      title: "%{name} 님이 당신의 Toot를 즐겨찾기에 등록했습니다."
+    follow:
+      title: "%{name} 님이 나를 팔로우 하고 있습니다."
+    group:
+      title: "%{count} 건의 알림"
+    mention:
+      action_boost: 부스트
+      action_expand: 더보기
+      action_favourite: 즐겨찾기
+      title: "%{name} 님이 답장을 보냈습니다"
+    reblog:
+      title: "%{name} 님이 당신의 Toot를 부스트 했습니다."
   remote_follow:
     acct: 아이디@도메인을 입력해 주십시오
     missing_resource: 리디렉션 대상을 찾을 수 없습니다
@@ -330,11 +422,14 @@ ko:
       windows: Windows
       windows_mobile: Windows Mobile
       windows_phone: Windows Phone
+    revoke: 삭제
+    revoke_success: 세션이 삭제되었습니다.
     title: 세션
   settings:
     authorized_apps: 인증된 어플리케이션
     back: 돌아가기
     delete: 계정 삭제
+    development: 개발
     edit_profile: 프로필 편집
     export: 데이터 내보내기
     followers: 신뢰 중인 인스턴스
@@ -342,9 +437,14 @@ ko:
     preferences: 사용자 설정
     settings: 설정
     two_factor_authentication: 2단계 인증
+    your_apps: 애플리케이션
   statuses:
     open_in_web: Web으로 열기
     over_character_limit: 최대 %{max}자까지 입력할 수 있습니다
+    pin_errors:
+      ownership: 다른 사람의 Toot는 고정될 수 없습니다.
+      private: 비공개 Toot는 고정될 수 없습니다.
+      reblog: 부스트는 고정될 수 없습니다.
     show_more: 더 보기
     visibilities:
       private: 비공개
@@ -355,8 +455,11 @@ ko:
       unlisted_long: 누구나 볼 수 있지만, 공개 타임라인에는 표시되지 않습니다
   stream_entries:
     click_to_show: 클릭해서 표시
+    pinned: 고정된 Toot
     reblogged: 님이 부스트 했습니다
     sensitive_content: 민감한 컨텐츠
+  terms:
+    title: "%{instance} 이용약관과 개인정보 취급 방침"
   time:
     formats:
       default: "%Y년 %m월 %d일 %H:%M"
@@ -379,3 +482,4 @@ ko:
   users:
     invalid_email: 메일 주소가 올바르지 않습니다
     invalid_otp_token: 2단계 인증 코드가 올바르지 않습니다
+    signed_in_as: '다음과 같이 로그인 중:'
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 50ae5508b..2b7a1a511 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -337,6 +337,17 @@ nl:
     reblog:
       body: 'Jouw toot werd door %{name} geboost:'
       subject: "%{name} boostte jouw toot"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Volgende
     prev: Vorige
diff --git a/config/locales/no.yml b/config/locales/no.yml
index 996ea1d97..207f86afc 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -257,6 +257,17 @@
     reblog:
       body: 'Din status ble fremhevd av %{name}:'
       subject: "%{name} fremhevde din status"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Neste
     prev: Forrige
diff --git a/config/locales/oc.yml b/config/locales/oc.yml
index 019d3b196..c3807428b 100644
--- a/config/locales/oc.yml
+++ b/config/locales/oc.yml
@@ -103,6 +103,7 @@ oc:
       title: Comptes
       undo_silenced: Levar lo silenci
       undo_suspension: Levar la suspension
+      unsubscribe: Se desabonar
       username: Nom d’utilizaire
       web: Web
     domain_blocks:
@@ -430,6 +431,17 @@ oc:
     reblog:
       body: "%{name} a tornat partejar vòstre estatut :"
       subject: "%{name} a tornat partejar vòstre estatut"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Seguent
     prev: Precedent
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 246028f9b..842baef45 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -355,6 +355,17 @@ pl:
     reblog:
       body: 'Twój wpis został podbity przez %{name}:'
       subject: Twój wpis został podbity przez %{name}
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Następna
     prev: Poprzednia
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 6dec2b50a..750120299 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -255,6 +255,17 @@ pt-BR:
     reblog:
       body: 'O seu post foi reblogado por %{name}:'
       subject: "%{name} reblogou o seu post"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Next
     prev: Prev
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index f6dd32200..140f6b71b 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -182,6 +182,17 @@ pt:
     reblog:
       body: 'O teu post foi partilhado por %{name}:'
       subject: "%{name} partilhou o teu post"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Seguinte
     prev: Anterior
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index 52cb71c60..9ca08831e 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -262,6 +262,17 @@ ru:
     reblog:
       body: 'Ваш статус был продвинут %{name}:'
       subject: "%{name} продвинул(а) Ваш статус"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: След
     prev: Пред
diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml
index 85ec0e4fc..c07dc2846 100644
--- a/config/locales/simple_form.de.yml
+++ b/config/locales/simple_form.de.yml
@@ -6,7 +6,7 @@ de:
         avatar: PNG, GIF oder JPG. Maximal 2MB. Wird auf 120x120px herunterskaliert
         display_name: '<span class="name-counter">%{count}</span> Zeichen verbleiben'
         header: PNG, GIF oder JPG. Maximal 2MB. Wird auf 700x335px herunterskaliert
-        locked: Erlaubt dir, Nutzer zu überprüfen, bevor sie dir folgen können
+        locked: Erlaubt dir, Profile zu überprüfen, bevor sie dir folgen können
         note: '<span class="note-counter">%{count}</span> Zeichen verbleiben'
       imports:
         data: CSV-Datei, die von einer anderen Mastodon-Instanz exportiert wurde
@@ -33,10 +33,10 @@ de:
         setting_default_privacy: Beitragsprivatspäre
         severity: Gewichtung
         type: Importtyp
-        username: Nutzername
+        username: Profilname
       interactions:
         must_be_follower: Benachrichtigungen von Nicht-Folgern blockieren
-        must_be_following: Benachrichtigungen von Nutzern blockieren, denen ich nicht folge
+        must_be_following: Benachrichtigungen von Profilen blockieren, denen ich nicht folge
       notification_emails:
         digest: Schicke Übersichts-E-Mails
         favourite: E-Mail senden, wenn jemand meinen Beitrag favorisiert
diff --git a/config/locales/th.yml b/config/locales/th.yml
index 9d0887928..2db3aee8a 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -257,6 +257,17 @@ th:
     reblog:
       body: 'Your status was boosted by %{name}:'
       subject: "%{name} boosted your status"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: ต่อไป
     prev: ย้อนกลับ
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index 91ef9544c..6aff78fa1 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -255,6 +255,17 @@ tr:
     reblog:
       body: "%{name} durumunuzu boost etti:"
       subject: "%{name} durumunuzu boost etti"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Sonraki
     prev: Önceki
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 4d12ddf4e..995a682a7 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -250,6 +250,17 @@ uk:
     reblog:
       body: 'Ваш статус було передмухнуто %{name}:'
       subject: "%{name} передмухнув ваш статус"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: Далі
     prev: Назад
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index 0672202a2..95c24d0bc 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -261,6 +261,17 @@ zh-CN:
     reblog:
       body: 你的嘟文得到 %{name} 的转嘟
       subject: "%{name} 转嘟(嘟嘟滴)了你的嘟文"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: 下一页
     prev: 上一页
diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml
index 9d6c74008..aa6b1ea6a 100644
--- a/config/locales/zh-HK.yml
+++ b/config/locales/zh-HK.yml
@@ -256,6 +256,17 @@ zh-HK:
     reblog:
       body: 你的文章得到 %{name} 的轉推
       subject: "%{name} 轉推了你的文章"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: 下一頁
     prev: 上一頁
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 7065acf9a..299a92da7 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -211,6 +211,17 @@ zh-TW:
     reblog:
       body: 您的文章被 %{name} 轉推
       subject: "%{name} 轉推了您的文章"
+  number:
+    human:
+      decimal_units:
+        format: "%n%u"
+        units:
+          billion: B
+          million: M
+          quadrillion: Q
+          thousand: K
+          trillion: T
+          unit: ''
   pagination:
     next: 下一頁
     prev: 上一頁
diff --git a/db/migrate/20170905044538_add_index_id_account_id_activity_type_on_notifications.rb b/db/migrate/20170905044538_add_index_id_account_id_activity_type_on_notifications.rb
new file mode 100644
index 000000000..c47cea9e2
--- /dev/null
+++ b/db/migrate/20170905044538_add_index_id_account_id_activity_type_on_notifications.rb
@@ -0,0 +1,5 @@
+class AddIndexIdAccountIdActivityTypeOnNotifications < ActiveRecord::Migration[5.1]
+  def change
+    add_index :notifications, [:id, :account_id, :activity_type], order: { id: :desc }
+  end
+end
diff --git a/db/migrate/20170905165803_add_local_to_statuses.rb b/db/migrate/20170905165803_add_local_to_statuses.rb
new file mode 100644
index 000000000..fb4e7019d
--- /dev/null
+++ b/db/migrate/20170905165803_add_local_to_statuses.rb
@@ -0,0 +1,5 @@
+class AddLocalToStatuses < ActiveRecord::Migration[5.1]
+  def change
+    add_column :statuses, :local, :boolean, null: true, default: nil
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c3a2581e3..d8af0a1f8 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20170901142658) do
+ActiveRecord::Schema.define(version: 20170905165803) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -180,6 +180,7 @@ ActiveRecord::Schema.define(version: 20170901142658) do
     t.integer "from_account_id"
     t.index ["account_id", "activity_id", "activity_type"], name: "account_activity", unique: true
     t.index ["activity_id", "activity_type"], name: "index_notifications_on_activity_id_and_activity_type"
+    t.index ["id", "account_id", "activity_type"], name: "index_notifications_on_id_and_account_id_and_activity_type", order: { id: :desc }
   end
 
   create_table "oauth_access_grants", id: :serial, force: :cascade do |t|
@@ -314,6 +315,7 @@ ActiveRecord::Schema.define(version: 20170901142658) do
     t.integer "reblogs_count", default: 0, null: false
     t.string "language"
     t.bigint "conversation_id"
+    t.boolean "local"
     t.index ["account_id", "id"], name: "index_statuses_on_account_id_id"
     t.index ["conversation_id"], name: "index_statuses_on_conversation_id"
     t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id"
diff --git a/lib/mastodon/unique_retry_job_middleware.rb b/lib/mastodon/unique_retry_job_middleware.rb
new file mode 100644
index 000000000..75da8a0c9
--- /dev/null
+++ b/lib/mastodon/unique_retry_job_middleware.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class Mastodon::UniqueRetryJobMiddleware
+  def call(_worker_class, item, _queue, _redis_pool)
+    return if item['unique_retry'] && retried?(item)
+    yield
+  end
+
+  private
+
+  def retried?(item)
+    # Use unique digest key of SidekiqUniqueJobs
+    unique_key = SidekiqUniqueJobs::UNIQUE_DIGEST_KEY
+    unique_digest = item[unique_key]
+    class_name = item['class']
+    retries = Sidekiq::RetrySet.new
+
+    retries.any? { |job| job.item['class'] == class_name && job.item[unique_key] == unique_digest }
+  end
+end
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index fcca875d9..de2516d6c 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -9,11 +9,11 @@ module Mastodon
     end
 
     def minor
-      5
+      6
     end
 
     def patch
-      1
+      0
     end
 
     def pre
@@ -21,7 +21,7 @@ module Mastodon
     end
 
     def flags
-      ''
+      'rc2'
     end
 
     def to_a
diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake
index f04201a3c..307bc240d 100644
--- a/lib/tasks/mastodon.rake
+++ b/lib/tasks/mastodon.rake
@@ -273,10 +273,17 @@ namespace :mastodon do
 
     desc 'Remove deprecated preview cards'
     task remove_deprecated_preview_cards: :environment do
-      return unless ActiveRecord::Base.connection.table_exists? 'deprecated_preview_cards'
+      next unless ActiveRecord::Base.connection.table_exists? 'deprecated_preview_cards'
 
-      class DeprecatedPreviewCard < PreviewCard
-        self.table_name = 'deprecated_preview_cards'
+      class DeprecatedPreviewCard < ActiveRecord::Base
+        self.inheritance_column = false
+
+        path = '/preview_cards/:attachment/:id_partition/:style/:filename'
+        if ENV['S3_ENABLED'] != 'true'
+          path = (ENV['PAPERCLIP_ROOT_PATH'] || ':rails_root/public/system') + path
+        end
+
+        has_attached_file :image, styles: { original: '280x120>' }, convert_options: { all: '-quality 80 -strip' }, path: path
       end
 
       puts 'Delete records and associated files from deprecated preview cards? [y/N]: '
diff --git a/package.json b/package.json
index 5fdd491ee..228dd1f25 100644
--- a/package.json
+++ b/package.json
@@ -85,7 +85,7 @@
     "react-dom": "^15.6.1",
     "react-immutable-proptypes": "^2.1.0",
     "react-immutable-pure-component": "^1.0.0",
-    "react-intl": "^2.3.0",
+    "react-intl": "^2.4.0",
     "react-motion": "^0.5.0",
     "react-notification": "^6.7.1",
     "react-redux": "^5.0.4",
diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb
index 4e37b1b5f..92f888590 100644
--- a/spec/controllers/accounts_controller_spec.rb
+++ b/spec/controllers/accounts_controller_spec.rb
@@ -61,7 +61,29 @@ RSpec.describe AccountsController, type: :controller do
       end
     end
 
-    context 'html' do
+    context 'html without since_id nor max_id' do
+      before do
+        get :show, params: { username: alice.username }
+      end
+
+      it 'assigns @account' do
+        expect(assigns(:account)).to eq alice
+      end
+
+      it 'assigns @pinned_statuses' do
+        pinned_statuses = assigns(:pinned_statuses).to_a
+        expect(pinned_statuses.size).to eq 3
+        expect(pinned_statuses[0]).to eq status7
+        expect(pinned_statuses[1]).to eq status5
+        expect(pinned_statuses[2]).to eq status6
+      end
+
+      it 'returns http success' do
+        expect(response).to have_http_status(:success)
+      end
+    end
+
+    context 'html with since_id and max_id' do
       before do
         get :show, params: { username: alice.username, max_id: status4.id, since_id: status1.id }
       end
@@ -77,12 +99,9 @@ RSpec.describe AccountsController, type: :controller do
         expect(statuses[1]).to eq status2
       end
 
-      it 'assigns @pinned_statuses' do
+      it 'assigns an empty array to @pinned_statuses' do
         pinned_statuses = assigns(:pinned_statuses).to_a
-        expect(pinned_statuses.size).to eq 3
-        expect(pinned_statuses[0]).to eq status7
-        expect(pinned_statuses[1]).to eq status5
-        expect(pinned_statuses[2]).to eq status6
+        expect(pinned_statuses.size).to eq 0
       end
 
       it 'returns http success' do
diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb
index c13509e7b..05df2f844 100644
--- a/spec/controllers/api/v1/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts_controller_spec.rb
@@ -28,6 +28,13 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
       expect(response).to have_http_status(:success)
     end
 
+    it 'returns JSON with following=true and requested=false' do
+      json = body_as_json
+
+      expect(json[:following]).to be true
+      expect(json[:requested]).to be false
+    end
+
     it 'creates a following relation between user and target user' do
       expect(user.account.following?(other_account)).to be true
     end
diff --git a/spec/fabricators/status_fabricator.rb b/spec/fabricators/status_fabricator.rb
index 8ec5f4ba7..04bbbcf4b 100644
--- a/spec/fabricators/status_fabricator.rb
+++ b/spec/fabricators/status_fabricator.rb
@@ -1,4 +1,8 @@
 Fabricator(:status) do
   account
   text "Lorem ipsum dolor sit amet"
+
+  after_build do |status|
+    status.uri = Faker::Internet.device_token if !status.account.local? && status.uri.nil?
+  end
 end
diff --git a/spec/lib/activitypub/activity/delete_spec.rb b/spec/lib/activitypub/activity/delete_spec.rb
index 65e743abb..38254e31c 100644
--- a/spec/lib/activitypub/activity/delete_spec.rb
+++ b/spec/lib/activitypub/activity/delete_spec.rb
@@ -1,7 +1,7 @@
 require 'rails_helper'
 
 RSpec.describe ActivityPub::Activity::Delete do
-  let(:sender)    { Fabricate(:account) }
+  let(:sender)    { Fabricate(:account, domain: 'example.com') }
   let(:status)    { Fabricate(:status, account: sender, uri: 'foobar') }
 
   let(:json) do
diff --git a/spec/lib/activitypub/activity/undo_spec.rb b/spec/lib/activitypub/activity/undo_spec.rb
index 4629a033f..14c68efe5 100644
--- a/spec/lib/activitypub/activity/undo_spec.rb
+++ b/spec/lib/activitypub/activity/undo_spec.rb
@@ -1,7 +1,7 @@
 require 'rails_helper'
 
 RSpec.describe ActivityPub::Activity::Undo do
-  let(:sender) { Fabricate(:account) }
+  let(:sender) { Fabricate(:account, domain: 'example.com') }
 
   let(:json) do
     {
diff --git a/spec/lib/formatter_spec.rb b/spec/lib/formatter_spec.rb
index dfe1d8b8f..ab04ccbab 100644
--- a/spec/lib/formatter_spec.rb
+++ b/spec/lib/formatter_spec.rb
@@ -178,7 +178,7 @@ RSpec.describe Formatter do
     end
 
     context 'with remote status' do
-      let(:status) { Fabricate(:status, text: 'Beep boop', uri: 'beepboop') }
+      let(:status) { Fabricate(:status, account: remote_account, text: 'Beep boop') }
 
       it 'reformats' do
         is_expected.to eq 'Beep boop'
@@ -226,7 +226,7 @@ RSpec.describe Formatter do
     end
 
     context 'with remote status' do
-      let(:status)  { Fabricate(:status, text: '<script>alert("Hello")</script>', uri: 'beep boop') }
+      let(:status)  { Fabricate(:status, account: remote_account, text: '<script>alert("Hello")</script>') }
 
       it 'returns tag-stripped text' do
         is_expected.to eq ''
diff --git a/spec/lib/ostatus/atom_serializer_spec.rb b/spec/lib/ostatus/atom_serializer_spec.rb
index 301a0ce30..0451eceeb 100644
--- a/spec/lib/ostatus/atom_serializer_spec.rb
+++ b/spec/lib/ostatus/atom_serializer_spec.rb
@@ -403,8 +403,7 @@ RSpec.describe OStatus::AtomSerializer do
 
       it 'returns element whose rendered view triggers creation when processed' do
         remote_account = Account.create!(username: 'username')
-        remote_status = Fabricate(:status, account: remote_account)
-        remote_status.stream_entry.update!(created_at: '2000-01-01T00:00:00Z')
+        remote_status = Fabricate(:status, account: remote_account, created_at: '2000-01-01T00:00:00Z')
 
         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
@@ -421,7 +420,7 @@ RSpec.describe OStatus::AtomSerializer do
 
         ProcessFeedService.new.call(xml, account)
 
-        expect(Status.find_by(uri: "tag:remote,2000-01-01:objectId=#{remote_status.id}:objectType=Status")).to be_instance_of Status
+        expect(Status.find_by(uri: "https://remote/users/#{remote_status.account.to_param}/statuses/#{remote_status.id}")).to be_instance_of Status
       end
     end
 
@@ -465,12 +464,11 @@ RSpec.describe OStatus::AtomSerializer do
     end
 
     it 'appends id element with unique tag' do
-      status = Fabricate(:status, reblog_of_id: nil)
-      status.stream_entry.update!(created_at: '2000-01-01T00:00:00Z')
+      status = Fabricate(:status, reblog_of_id: nil, created_at: '2000-01-01T00:00:00Z')
 
       entry = OStatus::AtomSerializer.new.entry(status.stream_entry)
 
-      expect(entry.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+      expect(entry.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
     end
 
     it 'appends published element with created date' do
@@ -515,7 +513,7 @@ RSpec.describe OStatus::AtomSerializer do
       entry = OStatus::AtomSerializer.new.entry(reblog.stream_entry)
 
       object = entry.nodes.find { |node| node.name == 'activity:object' }
-      expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{reblogged.id}:objectType=Status"
+      expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{reblogged.account.to_param}/statuses/#{reblogged.id}"
     end
 
     it 'does not append activity:object element if target is not present' do
@@ -532,7 +530,7 @@ RSpec.describe OStatus::AtomSerializer do
 
       link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' }
       expect(link[:type]).to eq 'text/html'
-      expect(link[:href]).to eq "https://cb6e6126.ngrok.io/users/username/updates/#{status.stream_entry.id}"
+      expect(link[:href]).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}"
     end
 
     it 'appends link element for itself' do
@@ -553,7 +551,7 @@ RSpec.describe OStatus::AtomSerializer do
       entry = OStatus::AtomSerializer.new.entry(reply_status.stream_entry)
 
       in_reply_to = entry.nodes.find { |node| node.name == 'thr:in-reply-to' }
-      expect(in_reply_to[:ref]).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{in_reply_to_status.id}:objectType=Status"
+      expect(in_reply_to[:ref]).to eq "https://cb6e6126.ngrok.io/users/#{in_reply_to_status.account.to_param}/statuses/#{in_reply_to_status.id}"
     end
 
     it 'does not append thr:in-reply-to element if not threaded' do
@@ -934,7 +932,7 @@ RSpec.describe OStatus::AtomSerializer do
       favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite)
 
       object = favourite_salmon.nodes.find { |node| node.name == 'activity:object' }
-      expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+      expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
     end
 
     it 'appends thr:in-reply-to element for status' do
@@ -945,7 +943,7 @@ RSpec.describe OStatus::AtomSerializer do
       favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite)
 
       in_reply_to = favourite_salmon.nodes.find { |node| node.name == 'thr:in-reply-to' }
-      expect(in_reply_to.ref).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+      expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
       expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}"
     end
 
@@ -1034,7 +1032,7 @@ RSpec.describe OStatus::AtomSerializer do
       unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite)
 
       object = unfavourite_salmon.nodes.find { |node| node.name == 'activity:object' }
-      expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+      expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
     end
 
     it 'appends thr:in-reply-to element for status' do
@@ -1045,7 +1043,7 @@ RSpec.describe OStatus::AtomSerializer do
       unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite)
 
       in_reply_to = unfavourite_salmon.nodes.find { |node| node.name == 'thr:in-reply-to' }
-      expect(in_reply_to.ref).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+      expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
       expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}"
     end
 
@@ -1453,7 +1451,7 @@ RSpec.describe OStatus::AtomSerializer do
     it 'appends id element with URL for status' do
       status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z')
       object = OStatus::AtomSerializer.new.object(status)
-      expect(object.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.id}:objectType=Status"
+      expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}"
     end
 
     it 'appends published element with created date' do
@@ -1463,7 +1461,8 @@ RSpec.describe OStatus::AtomSerializer do
     end
 
     it 'appends updated element with updated date' do
-      status = Fabricate(:status, updated_at: '2000-01-01T00:00:00Z')
+      status = Fabricate(:status)
+      status.updated_at = '2000-01-01T00:00:00Z'
       object = OStatus::AtomSerializer.new.object(status)
       expect(object.updated.text).to eq '2000-01-01T00:00:00Z'
     end
@@ -1523,7 +1522,7 @@ RSpec.describe OStatus::AtomSerializer do
       entry = OStatus::AtomSerializer.new.object(reply)
 
       in_reply_to = entry.nodes.find { |node| node.name == 'thr:in-reply-to' }
-      expect(in_reply_to.ref).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{thread.id}:objectType=Status"
+      expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{thread.account.to_param}/statuses/#{thread.id}"
       expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{thread.id}"
     end
 
diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb
index 1fae6bec4..1cd6e0a6f 100644
--- a/spec/lib/tag_manager_spec.rb
+++ b/spec/lib/tag_manager_spec.rb
@@ -157,23 +157,12 @@ RSpec.describe TagManager do
   describe '#uri_for' do
     subject { TagManager.instance.uri_for(target) }
 
-    context 'activity object' do
-      let(:target) { Fabricate(:status, reblog: Fabricate(:status)).stream_entry }
-
-      before { target.update!(created_at: '2000-01-01T00:00:00Z') }
-
-      it 'returns the unique tag for status' do
-        expect(target.object_type).to eq :activity
-        is_expected.to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{target.id}:objectType=Status"
-      end
-    end
-
     context 'comment object' do
       let(:target) { Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reply: true) }
 
       it 'returns the unique tag for status' do
         expect(target.object_type).to eq :comment
-        is_expected.to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{target.id}:objectType=Status"
+        is_expected.to eq target.uri
       end
     end
 
@@ -182,7 +171,7 @@ RSpec.describe TagManager do
 
       it 'returns the unique tag for status' do
         expect(target.object_type).to eq :note
-        is_expected.to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{target.id}:objectType=Status"
+        is_expected.to eq target.uri
       end
     end
 
diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb
index 626fc3f98..484effd5e 100644
--- a/spec/models/status_spec.rb
+++ b/spec/models/status_spec.rb
@@ -13,9 +13,15 @@ RSpec.describe Status, type: :model do
     end
 
     it 'returns false if a remote URI is set' do
-      subject.uri = 'a'
+      alice.update(domain: 'example.com')
+      subject.save
       expect(subject.local?).to be false
     end
+
+    it 'returns true if a URI is set and `local` is true' do
+      subject.update(uri: 'example.com', local: true)
+      expect(subject.local?).to be true
+    end
   end
 
   describe '#reblog?' do
@@ -495,7 +501,7 @@ RSpec.describe Status, type: :model do
     end
   end
 
-  describe 'before_create' do
+  describe 'before_validation' do
     it 'sets account being replied to correctly over intermediary nodes' do
       first_status = Fabricate(:status, account: bob)
       intermediary = Fabricate(:status, thread: first_status, account: alice)
@@ -512,5 +518,22 @@ RSpec.describe Status, type: :model do
       parent = Fabricate(:status, text: 'First')
       expect(Status.create(account: alice, thread: parent, text: 'Response').conversation_id).to eq parent.conversation_id
     end
+
+    it 'sets `local` to true for status by local account' do
+      expect(Status.create(account: alice, text: 'foo').local).to be true
+    end
+
+    it 'sets `local` to false for status by remote account' do
+      alice.update(domain: 'example.com')
+      expect(Status.create(account: alice, text: 'foo').local).to be false
+    end
+  end
+
+  describe 'after_create' do
+    it 'saves ActivityPub uri as uri for local status' do
+      status = Status.create(account: alice, text: 'foo')
+      status.reload
+      expect(status.uri).to start_with('https://')
+    end
   end
 end
diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb
index 3a0786d03..b0aa740ac 100644
--- a/spec/services/fetch_link_card_service_spec.rb
+++ b/spec/services/fetch_link_card_service_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe FetchLinkCardService do
   end
 
   context 'in a remote status' do
-    let(:status) { Fabricate(:status, uri: 'abc', text: 'Habt ihr ein paar gute Links zu #<span class="tag"><a href="https://quitter.se/tag/wannacry" target="_blank" rel="tag noopener" title="https://quitter.se/tag/wannacry">Wannacry</a></span> herumfliegen?   Ich will mal unter <br> <a href="https://github.com/qbi/WannaCry" target="_blank" rel="noopener" title="https://github.com/qbi/WannaCry">https://github.com/qbi/WannaCry</a> was sammeln. !<a href="http://sn.jonkman.ca/group/416/id" target="_blank" rel="noopener" title="http://sn.jonkman.ca/group/416/id">security</a>&nbsp;') }
+    let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu #<span class="tag"><a href="https://quitter.se/tag/wannacry" target="_blank" rel="tag noopener" title="https://quitter.se/tag/wannacry">Wannacry</a></span> herumfliegen?   Ich will mal unter <br> <a href="https://github.com/qbi/WannaCry" target="_blank" rel="noopener" title="https://github.com/qbi/WannaCry">https://github.com/qbi/WannaCry</a> was sammeln. !<a href="http://sn.jonkman.ca/group/416/id" target="_blank" rel="noopener" title="http://sn.jonkman.ca/group/416/id">security</a>&nbsp;') }
 
     it 'parses out URLs' do
       expect(a_request(:head, 'https://github.com/qbi/WannaCry')).to have_been_made.at_least_once
diff --git a/streaming/index.js b/streaming/index.js
index c7e0de96c..3e80c8b30 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -403,11 +403,11 @@ const startWorker = (workerId) => {
   });
 
   app.get('/api/v1/streaming/hashtag', (req, res) => {
-    streamFrom(`timeline:hashtag:${req.query.tag}`, req, streamToHttp(req, res), streamHttpEnd(req), true);
+    streamFrom(`timeline:hashtag:${req.query.tag.toLowerCase()}`, req, streamToHttp(req, res), streamHttpEnd(req), true);
   });
 
   app.get('/api/v1/streaming/hashtag/local', (req, res) => {
-    streamFrom(`timeline:hashtag:${req.query.tag}:local`, req, streamToHttp(req, res), streamHttpEnd(req), true);
+    streamFrom(`timeline:hashtag:${req.query.tag.toLowerCase()}:local`, req, streamToHttp(req, res), streamHttpEnd(req), true);
   });
 
   const wss    = new WebSocket.Server({ server, verifyClient: wsVerifyClient });
@@ -438,10 +438,10 @@ const startWorker = (workerId) => {
       streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
       break;
     case 'hashtag':
-      streamFrom(`timeline:hashtag:${location.query.tag}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true);
+      streamFrom(`timeline:hashtag:${location.query.tag.toLowerCase()}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true);
       break;
     case 'hashtag:local':
-      streamFrom(`timeline:hashtag:${location.query.tag}:local`, req, streamToWs(req, ws), streamWsEnd(req, ws), true);
+      streamFrom(`timeline:hashtag:${location.query.tag.toLowerCase()}:local`, req, streamToWs(req, ws), streamWsEnd(req, ws), true);
       break;
     default:
       ws.close();
diff --git a/yarn.lock b/yarn.lock
index cfb0f5175..c1c27a615 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3130,23 +3130,17 @@ intl-messageformat-parser@^1.2.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-1.3.0.tgz#c5d26ffb894c7d9c2b9fa444c67f417ab2594268"
 
-intl-messageformat@1.3.0, intl-messageformat@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-1.3.0.tgz#f7d926aded7a3ab19b2dc601efd54e99a4bd4eae"
-  dependencies:
-    intl-messageformat-parser "1.2.0"
-
 intl-messageformat@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-2.0.0.tgz#3d56982583425aee23b76c8b985fb9b0aae5be3c"
   dependencies:
     intl-messageformat-parser "1.2.0"
 
-intl-relativeformat@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/intl-relativeformat/-/intl-relativeformat-1.3.0.tgz#893dc7076fccd380cf091a2300c380fa57ace45b"
+intl-messageformat@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-2.1.0.tgz#1c51da76f02a3f7b360654cdc51bbc4d3fa6c72c"
   dependencies:
-    intl-messageformat "1.3.0"
+    intl-messageformat-parser "1.2.0"
 
 intl-relativeformat@^2.0.0:
   version "2.0.0"
@@ -5312,13 +5306,13 @@ react-intl-translations-manager@^5.0.0:
     json-stable-stringify "^1.0.1"
     mkdirp "^0.5.1"
 
-react-intl@^2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-2.3.0.tgz#e1df6af5667fdf01cbe4aab20e137251e2ae5142"
+react-intl@^2.4.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-2.4.0.tgz#66c14dc9df9a73b2fbbfbd6021726e80a613eb15"
   dependencies:
     intl-format-cache "^2.0.5"
-    intl-messageformat "^1.3.0"
-    intl-relativeformat "^1.3.0"
+    intl-messageformat "^2.1.0"
+    intl-relativeformat "^2.0.0"
     invariant "^2.1.1"
 
 react-motion@^0.5.0: