about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Dockerfile11
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.lock18
-rw-r--r--app/controllers/accounts_controller.rb16
-rw-r--r--app/controllers/api/v1/accounts/statuses_controller.rb5
-rw-r--r--app/controllers/oauth/authorized_applications_controller.rb5
-rw-r--r--app/controllers/settings/featured_tags_controller.rb51
-rw-r--r--app/controllers/settings/profiles_controller.rb2
-rw-r--r--app/controllers/settings/sessions_controller.rb1
-rw-r--r--app/javascript/mastodon/actions/alerts.js4
-rw-r--r--app/javascript/mastodon/actions/compose.js38
-rw-r--r--app/javascript/mastodon/components/intersection_observer_article.js2
-rw-r--r--app/javascript/mastodon/containers/compose_container.js3
-rw-r--r--app/javascript/mastodon/features/compose/components/upload_button.js2
-rw-r--r--app/javascript/mastodon/features/getting_started/index.js2
-rw-r--r--app/javascript/mastodon/features/introduction/index.js2
-rw-r--r--app/javascript/mastodon/features/public_timeline/index.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/report_modal.js2
-rw-r--r--app/javascript/mastodon/features/ui/index.js2
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json19
-rw-r--r--app/javascript/mastodon/locales/en.json7
-rw-r--r--app/javascript/mastodon/locales/ja.json9
-rw-r--r--app/javascript/mastodon/locales/pl.json1
-rw-r--r--app/javascript/mastodon/utils/resize_image.js2
-rw-r--r--app/javascript/styles/mastodon/accounts.scss4
-rw-r--r--app/javascript/styles/mastodon/admin.scss7
-rw-r--r--app/javascript/styles/mastodon/components.scss2
-rw-r--r--app/javascript/styles/mastodon/widgets.scss7
-rw-r--r--app/lib/activity_tracker.rb6
-rw-r--r--app/lib/activitypub/activity.rb5
-rw-r--r--app/lib/feed_manager.rb9
-rw-r--r--app/lib/formatter.rb49
-rw-r--r--app/lib/ostatus/activity/base.rb6
-rw-r--r--app/lib/potential_friendship_tracker.rb8
-rw-r--r--app/models/account_domain_block.rb1
-rw-r--r--app/models/concerns/account_associations.rb1
-rw-r--r--app/models/concerns/account_avatar.rb2
-rw-r--r--app/models/concerns/account_header.rb2
-rw-r--r--app/models/concerns/domain_normalizable.rb2
-rw-r--r--app/models/concerns/redisable.rb11
-rw-r--r--app/models/export.rb1
-rw-r--r--app/models/featured_tag.rb46
-rw-r--r--app/models/feed.rb6
-rw-r--r--app/models/import.rb14
-rw-r--r--app/models/media_attachment.rb12
-rw-r--r--app/models/preview_card.rb2
-rw-r--r--app/models/tag.rb2
-rw-r--r--app/models/trending_tags.rb6
-rw-r--r--app/serializers/manifest_serializer.rb10
-rw-r--r--app/services/activitypub/process_account_service.rb2
-rw-r--r--app/services/batched_remove_status_service.rb5
-rw-r--r--app/services/follow_service.rb6
-rw-r--r--app/services/import_service.rb90
-rw-r--r--app/services/post_status_service.rb6
-rw-r--r--app/services/process_hashtags_service.rb12
-rw-r--r--app/services/remove_status_service.rb23
-rw-r--r--app/views/accounts/show.html.haml13
-rw-r--r--app/views/admin/change_emails/show.html.haml2
-rw-r--r--app/views/settings/featured_tags/index.html.haml27
-rw-r--r--app/views/settings/imports/show.html.haml7
-rw-r--r--app/workers/import/relationship_worker.rb8
-rw-r--r--app/workers/import_worker.rb38
-rw-r--r--app/workers/scheduler/feed_cleanup_scheduler.rb5
-rw-r--r--config/i18n-tasks.yml5
-rw-r--r--config/initializers/twitter_regex.rb4
-rw-r--r--config/locales/devise.en.yml6
-rw-r--r--config/locales/devise.ja.yml6
-rw-r--r--config/locales/en.yml45
-rw-r--r--config/locales/ja.yml59
-rw-r--r--config/locales/pl.yml11
-rw-r--r--config/locales/simple_form.en.yml6
-rw-r--r--config/locales/simple_form.ja.yml8
-rw-r--r--config/locales/simple_form.pl.yml6
-rw-r--r--config/navigation.rb1
-rw-r--r--config/routes.rb2
-rw-r--r--db/migrate/20171005102658_create_account_moderation_notes.rb1
-rw-r--r--db/migrate/20190201012802_add_overwrite_to_imports.rb17
-rw-r--r--db/migrate/20190203180359_create_featured_tags.rb12
-rw-r--r--db/schema.rb16
-rw-r--r--spec/fabricators/featured_tag_fabricator.rb6
-rw-r--r--spec/lib/formatter_spec.rb84
-rw-r--r--spec/models/concerns/account_interactions_spec.rb4
-rw-r--r--spec/models/featured_tag_spec.rb4
83 files changed, 748 insertions, 232 deletions
diff --git a/Dockerfile b/Dockerfile
index aefcb44bd..eca90094c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 FROM node:8.15-alpine as node
-FROM ruby:2.6-alpine3.8
+FROM ruby:2.6-alpine3.9
 
 LABEL maintainer="https://github.com/tootsuite/mastodon" \
       description="Your self-hosted, globally interconnected microblogging community"
@@ -24,19 +24,18 @@ COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
 COPY --from=node /usr/local/bin/npm /usr/local/bin/npm
 COPY --from=node /opt/yarn-* /opt/yarn
 
-RUN apk -U upgrade \
- && apk add -t build-dependencies \
+RUN apk add --no-cache -t build-dependencies \
     build-base \
     icu-dev \
     libidn-dev \
-    libressl \
+    openssl \
     libtool \
     libxml2-dev \
     libxslt-dev \
     postgresql-dev \
     protobuf-dev \
     python \
- && apk add \
+ && apk add --no-cache \
     ca-certificates \
     ffmpeg \
     file \
@@ -64,7 +63,7 @@ RUN apk -U upgrade \
  && make install \
  && libtool --finish /usr/local/lib \
  && cd /mastodon \
- && rm -rf /tmp/* /var/cache/apk/*
+ && rm -rf /tmp/*
 
 COPY Gemfile Gemfile.lock package.json yarn.lock .yarnclean /mastodon/
 COPY stack-fix.c /lib
diff --git a/Gemfile b/Gemfile
index 51595a758..fcf97b028 100644
--- a/Gemfile
+++ b/Gemfile
@@ -108,15 +108,15 @@ group :production, :test do
 end
 
 group :test do
-  gem 'capybara', '~> 3.12'
+  gem 'capybara', '~> 3.13'
   gem 'climate_control', '~> 0.2'
   gem 'faker', '~> 1.9'
-  gem 'microformats', '~> 4.0'
+  gem 'microformats', '~> 4.1'
   gem 'rails-controller-testing', '~> 1.0'
   gem 'rspec-sidekiq', '~> 3.0'
   gem 'simplecov', '~> 0.16', require: false
   gem 'webmock', '~> 3.5'
-  gem 'parallel_tests', '~> 2.27'
+  gem 'parallel_tests', '~> 2.28'
 end
 
 group :development do
diff --git a/Gemfile.lock b/Gemfile.lock
index 76bcdeda5..957ca70a6 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -126,7 +126,7 @@ GEM
       sshkit (~> 1.3)
     capistrano-yarn (2.0.2)
       capistrano (~> 3.0)
-    capybara (3.12.0)
+    capybara (3.13.2)
       addressable
       mini_mime (>= 0.1.3)
       nokogiri (~> 1.8)
@@ -268,7 +268,7 @@ GEM
       domain_name (~> 0.5)
     http-form_data (2.1.1)
     http_accept_language (2.1.1)
-    httplog (1.2.0)
+    httplog (1.2.1)
       rack (>= 1.0)
       rainbow (>= 2.0.0)
     i18n (1.5.3)
@@ -337,9 +337,9 @@ GEM
       redis (>= 3.0.5)
     memory_profiler (0.9.12)
     method_source (0.9.2)
-    microformats (4.0.7)
-      json
-      nokogiri
+    microformats (4.1.0)
+      json (~> 2.1)
+      nokogiri (~> 1.8, >= 1.8.3)
     mime-types (3.2.2)
       mime-types-data (~> 3.2015)
     mime-types-data (3.2018.0812)
@@ -392,7 +392,7 @@ GEM
       av (~> 0.9.0)
       paperclip (>= 2.5.2)
     parallel (1.13.0)
-    parallel_tests (2.27.1)
+    parallel_tests (2.28.0)
       parallel
     parser (2.6.0.0)
       ast (~> 2.4.0)
@@ -671,7 +671,7 @@ DEPENDENCIES
   capistrano-rails (~> 1.4)
   capistrano-rbenv (~> 2.1)
   capistrano-yarn (~> 2.0)
-  capybara (~> 3.12)
+  capybara (~> 3.13)
   charlock_holmes (~> 0.7.6)
   chewy (~> 5.0)
   cld3 (~> 3.2.3)
@@ -712,7 +712,7 @@ DEPENDENCIES
   makara (~> 0.4)
   mario-redis-lock (~> 1.2)
   memory_profiler
-  microformats (~> 4.0)
+  microformats (~> 4.1)
   mime-types (~> 3.2)
   net-ldap (~> 0.10)
   nokogiri (~> 1.10)
@@ -725,7 +725,7 @@ DEPENDENCIES
   ox (~> 2.10)
   paperclip (~> 6.0)
   paperclip-av-transcoder (~> 0.6)
-  parallel_tests (~> 2.27)
+  parallel_tests (~> 2.28)
   pg (~> 1.1)
   pghero (~> 2.2)
   pkg-config (~> 1.3)
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 3a4382850..442e99089 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -53,11 +53,12 @@ class AccountsController < ApplicationController
   private
 
   def show_pinned_statuses?
-    [replies_requested?, media_requested?, params[:max_id].present?, params[:min_id].present?].none?
+    [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
   end
 
   def filtered_statuses
     default_statuses.tap do |statuses|
+      statuses.merge!(hashtag_scope)    if tag_requested?
       statuses.merge!(only_media_scope) if media_requested?
       statuses.merge!(no_replies_scope) unless replies_requested?
     end
@@ -79,12 +80,15 @@ class AccountsController < ApplicationController
     Status.without_replies
   end
 
+  def hashtag_scope
+    Status.tagged_with(Tag.find_by(name: params[:tag].downcase)&.id)
+  end
+
   def set_account
     @account = Account.find_local!(params[:username])
   end
 
   def older_url
-    ::Rails.logger.info("older: max_id #{@statuses.last.id}, url #{pagination_url(max_id: @statuses.last.id)}")
     pagination_url(max_id: @statuses.last.id)
   end
 
@@ -93,7 +97,9 @@ class AccountsController < ApplicationController
   end
 
   def pagination_url(max_id: nil, min_id: nil)
-    if media_requested?
+    if tag_requested?
+      short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id)
+    elsif media_requested?
       short_account_media_url(@account, max_id: max_id, min_id: min_id)
     elsif replies_requested?
       short_account_with_replies_url(@account, max_id: max_id, min_id: min_id)
@@ -110,6 +116,10 @@ class AccountsController < ApplicationController
     request.path.ends_with?('/with_replies')
   end
 
+  def tag_requested?
+    request.path.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
+  end
+
   def filtered_status_page(params)
     if params[:min_id].present?
       filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse
diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb
index 6c2a5c141..6fdc827cb 100644
--- a/app/controllers/api/v1/accounts/statuses_controller.rb
+++ b/app/controllers/api/v1/accounts/statuses_controller.rb
@@ -33,6 +33,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
     statuses.merge!(only_media_scope) if truthy_param?(:only_media)
     statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
     statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
+    statuses.merge!(hashtag_scope)    if params[:tagged].present?
 
     statuses
   end
@@ -67,6 +68,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
     Status.without_reblogs
   end
 
+  def hashtag_scope
+    Status.tagged_with(Tag.find_by(name: params[:tagged])&.id)
+  end
+
   def pagination_params(core_params)
     params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
   end
diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb
index 1e420b3e7..4e45445df 100644
--- a/app/controllers/oauth/authorized_applications_controller.rb
+++ b/app/controllers/oauth/authorized_applications_controller.rb
@@ -6,6 +6,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
   before_action :store_current_location
   before_action :authenticate_resource_owner!
   before_action :set_pack
+  before_action :set_body_classes
 
   include Localized
 
@@ -16,6 +17,10 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
 
   private
 
+  def set_body_classes
+    @body_classes = 'admin'
+  end
+
   def store_current_location
     store_location_for(:user, request.url)
   end
diff --git a/app/controllers/settings/featured_tags_controller.rb b/app/controllers/settings/featured_tags_controller.rb
new file mode 100644
index 000000000..3a3241425
--- /dev/null
+++ b/app/controllers/settings/featured_tags_controller.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+class Settings::FeaturedTagsController < Settings::BaseController
+  layout 'admin'
+
+  before_action :authenticate_user!
+  before_action :set_featured_tags, only: :index
+  before_action :set_featured_tag, except: [:index, :create]
+  before_action :set_most_used_tags, only: :index
+
+  def index
+    @featured_tag = FeaturedTag.new
+  end
+
+  def create
+    @featured_tag = current_account.featured_tags.new(featured_tag_params)
+    @featured_tag.reset_data
+
+    if @featured_tag.save
+      redirect_to settings_featured_tags_path
+    else
+      set_featured_tags
+      set_most_used_tags
+
+      render :index
+    end
+  end
+
+  def destroy
+    @featured_tag.destroy!
+    redirect_to settings_featured_tags_path
+  end
+
+  private
+
+  def set_featured_tag
+    @featured_tag = current_account.featured_tags.find(params[:id])
+  end
+
+  def set_featured_tags
+    @featured_tags = current_account.featured_tags.order(statuses_count: :desc).reject(&:new_record?)
+  end
+
+  def set_most_used_tags
+    @most_used_tags = Tag.most_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10)
+  end
+
+  def featured_tag_params
+    params.require(:featured_tag).permit(:name)
+  end
+end
diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb
index 1a0b73d16..76d599f08 100644
--- a/app/controllers/settings/profiles_controller.rb
+++ b/app/controllers/settings/profiles_controller.rb
@@ -29,6 +29,6 @@ class Settings::ProfilesController < Settings::BaseController
   end
 
   def set_account
-    @account = current_user.account
+    @account = current_account
   end
 end
diff --git a/app/controllers/settings/sessions_controller.rb b/app/controllers/settings/sessions_controller.rb
index 780ea64b4..d74db6000 100644
--- a/app/controllers/settings/sessions_controller.rb
+++ b/app/controllers/settings/sessions_controller.rb
@@ -2,6 +2,7 @@
 
 #  Intentionally does not inherit from BaseController
 class Settings::SessionsController < ApplicationController
+  before_action :authenticate_user!
   before_action :set_session, only: :destroy
 
   def destroy
diff --git a/app/javascript/mastodon/actions/alerts.js b/app/javascript/mastodon/actions/alerts.js
index 3f5d7ef46..50cd48a9e 100644
--- a/app/javascript/mastodon/actions/alerts.js
+++ b/app/javascript/mastodon/actions/alerts.js
@@ -22,7 +22,7 @@ export function clearAlert() {
   };
 };
 
-export function showAlert(title, message) {
+export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) {
   return {
     type: ALERT_SHOW,
     title,
@@ -44,6 +44,6 @@ export function showAlertForError(error) {
     return showAlert(title, message);
   } else {
     console.error(error);
-    return showAlert(messages.unexpectedTitle, messages.unexpectedMessage);
+    return showAlert();
   }
 }
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index a4352faab..0be2a5cd4 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -8,6 +8,8 @@ import resizeImage from '../utils/resize_image';
 import { importFetchedAccounts } from './importer';
 import { updateTimeline } from './timelines';
 import { showAlertForError } from './alerts';
+import { showAlert } from './alerts';
+import { defineMessages } from 'react-intl';
 
 let cancelFetchComposeSuggestionsAccounts;
 
@@ -49,6 +51,10 @@ export const COMPOSE_UPLOAD_CHANGE_REQUEST     = 'COMPOSE_UPLOAD_UPDATE_REQUEST'
 export const COMPOSE_UPLOAD_CHANGE_SUCCESS     = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
 export const COMPOSE_UPLOAD_CHANGE_FAIL        = 'COMPOSE_UPLOAD_UPDATE_FAIL';
 
+const messages = defineMessages({
+  uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
+});
+
 export function changeCompose(text) {
   return {
     type: COMPOSE_CHANGE,
@@ -184,20 +190,32 @@ export function submitComposeFail(error) {
 
 export function uploadCompose(files) {
   return function (dispatch, getState) {
-    if (getState().getIn(['compose', 'media_attachments']).size > 3) {
+    const uploadLimit = 4;
+    const media  = getState().getIn(['compose', 'media_attachments']);
+    const total = Array.from(files).reduce((a, v) => a + v.size, 0);
+    const progress = new Array(files.length).fill(0);
+
+    if (files.length + media.size > uploadLimit) {
+      dispatch(showAlert(undefined, messages.uploadErrorLimit));
       return;
     }
-
     dispatch(uploadComposeRequest());
 
-    resizeImage(files[0]).then(file => {
-      const data = new FormData();
-      data.append('file', file);
-
-      return api(getState).post('/api/v1/media', data, {
-        onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)),
-      }).then(({ data }) => dispatch(uploadComposeSuccess(data)));
-    }).catch(error => dispatch(uploadComposeFail(error)));
+    for (const [i, f] of Array.from(files).entries()) {
+      if (media.size + i > 3) break;
+
+      resizeImage(f).then(file => {
+        const data = new FormData();
+        data.append('file', file);
+
+        return api(getState).post('/api/v1/media', data, {
+          onUploadProgress: function({ loaded }){
+            progress[i] = loaded;
+            dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
+          },
+        }).then(({ data }) => dispatch(uploadComposeSuccess(data)));
+      }).catch(error => dispatch(uploadComposeFail(error)));
+    };
   };
 };
 
diff --git a/app/javascript/mastodon/components/intersection_observer_article.js b/app/javascript/mastodon/components/intersection_observer_article.js
index de2203a4b..e453730ba 100644
--- a/app/javascript/mastodon/components/intersection_observer_article.js
+++ b/app/javascript/mastodon/components/intersection_observer_article.js
@@ -65,7 +65,7 @@ export default class IntersectionObserverArticle extends React.Component {
   }
 
   updateStateAfterIntersection = (prevState) => {
-    if (prevState.isIntersecting && !this.entry.isIntersecting) {
+    if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
       scheduleIdleTask(this.hideIfNotIntersecting);
     }
     return {
diff --git a/app/javascript/mastodon/containers/compose_container.js b/app/javascript/mastodon/containers/compose_container.js
index 5ee1d2f14..7bc7bbaa4 100644
--- a/app/javascript/mastodon/containers/compose_container.js
+++ b/app/javascript/mastodon/containers/compose_container.js
@@ -7,6 +7,7 @@ import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from '../locales';
 import Compose from '../features/standalone/compose';
 import initialState from '../initial_state';
+import { fetchCustomEmojis } from '../actions/custom_emojis';
 
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
@@ -17,6 +18,8 @@ if (initialState) {
   store.dispatch(hydrateStore(initialState));
 }
 
+store.dispatch(fetchCustomEmojis());
+
 export default class TimelineContainer extends React.PureComponent {
 
   static propTypes = {
diff --git a/app/javascript/mastodon/features/compose/components/upload_button.js b/app/javascript/mastodon/features/compose/components/upload_button.js
index b6fe770ea..db55ad70b 100644
--- a/app/javascript/mastodon/features/compose/components/upload_button.js
+++ b/app/javascript/mastodon/features/compose/components/upload_button.js
@@ -63,7 +63,7 @@ class UploadButton extends ImmutablePureComponent {
             key={resetFileKey}
             ref={this.setRef}
             type='file'
-            multiple={false}
+            multiple
             accept={acceptContentTypes.toArray().join(',')}
             onChange={this.handleChange}
             disabled={disabled}
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index e277a73c7..e1f84de27 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -160,7 +160,7 @@ class GettingStarted extends ImmutablePureComponent {
               {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
               {multiColumn && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
               <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
-              <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this instance' /></a> · </li>
+              <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
               <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
               <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
               <li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
diff --git a/app/javascript/mastodon/features/introduction/index.js b/app/javascript/mastodon/features/introduction/index.js
index e712b2f7d..754477bb9 100644
--- a/app/javascript/mastodon/features/introduction/index.js
+++ b/app/javascript/mastodon/features/introduction/index.js
@@ -89,7 +89,7 @@ const FrameInteractions = ({ onNext }) => (
     </div>
 
     <div className='introduction__action'>
-      <button className='button' onClick={onNext}><FormattedMessage id='introduction.interactions.action' defaultMessage='Finish tutorial!' /></button>
+      <button className='button' onClick={onNext}><FormattedMessage id='introduction.interactions.action' defaultMessage='Finish toot-orial!' /></button>
     </div>
   </div>
 );
diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js
index d640033eb..2b7d9c56f 100644
--- a/app/javascript/mastodon/features/public_timeline/index.js
+++ b/app/javascript/mastodon/features/public_timeline/index.js
@@ -124,7 +124,7 @@ class PublicTimeline extends React.PureComponent {
           onLoadMore={this.handleLoadMore}
           trackScroll={!pinned}
           scrollKey={`public_timeline-${columnId}`}
-          emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />}
+          emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />}
           shouldUpdateScroll={shouldUpdateScroll}
         />
       </Column>
diff --git a/app/javascript/mastodon/features/ui/components/report_modal.js b/app/javascript/mastodon/features/ui/components/report_modal.js
index bc6b18664..2e41f784d 100644
--- a/app/javascript/mastodon/features/ui/components/report_modal.js
+++ b/app/javascript/mastodon/features/ui/components/report_modal.js
@@ -97,7 +97,7 @@ class ReportModal extends ImmutablePureComponent {
 
         <div className='report-modal__container'>
           <div className='report-modal__comment'>
-            <p><FormattedMessage id='report.hint' defaultMessage='The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:' /></p>
+            <p><FormattedMessage id='report.hint' defaultMessage='The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:' /></p>
 
             <textarea
               className='setting-text light'
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index f01c2bf24..93e45678f 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -263,7 +263,7 @@ class UI extends React.PureComponent {
     this.setState({ draggingOver: false });
     this.dragTargets = [];
 
-    if (e.dataTransfer && e.dataTransfer.files.length === 1) {
+    if (e.dataTransfer && e.dataTransfer.files.length >= 1) {
       this.props.dispatch(uploadCompose(e.dataTransfer.files));
     }
   }
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 60f481076..eb3a7bbde 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -15,6 +15,15 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "File upload limit exceeded.",
+        "id": "upload_error.limit"
+      }
+    ],
+    "path": "app/javascript/mastodon/actions/compose.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "{name} mentioned you",
         "id": "notification.mention"
       },
@@ -1275,7 +1284,7 @@
         "id": "getting_started.security"
       },
       {
-        "defaultMessage": "About this instance",
+        "defaultMessage": "About this server",
         "id": "navigation_bar.info"
       },
       {
@@ -1448,7 +1457,7 @@
         "id": "introduction.interactions.favourite.text"
       },
       {
-        "defaultMessage": "Finish tutorial!",
+        "defaultMessage": "Finish toot-orial!",
         "id": "introduction.interactions.action"
       }
     ],
@@ -1828,7 +1837,7 @@
         "id": "column.public"
       },
       {
-        "defaultMessage": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
+        "defaultMessage": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up",
         "id": "empty_column.public"
       }
     ],
@@ -2188,7 +2197,7 @@
         "id": "report.target"
       },
       {
-        "defaultMessage": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+        "defaultMessage": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:",
         "id": "report.hint"
       },
       {
@@ -2298,4 +2307,4 @@
     ],
     "path": "app/javascript/mastodon/features/video/index.json"
   }
-]
\ No newline at end of file
+]
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 3bb157aeb..b079f256d 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -132,7 +132,7 @@
   "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
   "empty_column.mutes": "You haven't muted any users yet.",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
-  "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
+  "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up",
   "follow_request.authorize": "Authorize",
   "follow_request.reject": "Reject",
   "getting_started.developers": "Developers",
@@ -228,7 +228,7 @@
   "navigation_bar.favourites": "Favourites",
   "navigation_bar.filters": "Muted words",
   "navigation_bar.follow_requests": "Follow requests",
-  "navigation_bar.info": "About this instance",
+  "navigation_bar.info": "About this server",
   "navigation_bar.keyboard_shortcuts": "Hotkeys",
   "navigation_bar.lists": "Lists",
   "navigation_bar.misc": "Misc",
@@ -281,7 +281,7 @@
   "reply_indicator.cancel": "Cancel",
   "report.forward": "Forward to {target}",
   "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
-  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
   "report.target": "Reporting {target}",
@@ -347,6 +347,7 @@
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
   "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)",
+  "upload_error.limit": "File upload limit exceeded.",
   "upload_form.description": "Describe for the visually impaired",
   "upload_form.focus": "Change preview",
   "upload_form.undo": "Delete",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index e0cba7764..4bcc11d70 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -132,7 +132,7 @@
   "empty_column.lists": "ただリストがありたせん。リストを䜜るずここに衚瀺されたす。",
   "empty_column.mutes": "ただ誰もミュヌトしおいたせん。",
   "empty_column.notifications": "ただ通知がありたせん。他の人ずふれ合っお䌚話を始めたしょう。",
-  "empty_column.public": "ここにはただ䜕もありたせん 公開で䜕かを投皿したり、他のむンスタンスのナヌザヌをフォロヌしたりしおいっぱいにしたしょう",
+  "empty_column.public": "ここにはただ䜕もありたせん 公開で䜕かを投皿したり、他のサヌバヌのナヌザヌをフォロヌしたりしおいっぱいにしたしょう",
   "follow_request.authorize": "蚱可",
   "follow_request.reject": "拒吊",
   "getting_started.developers": "開発",
@@ -228,7 +228,7 @@
   "navigation_bar.favourites": "お気に入り",
   "navigation_bar.filters": "フィルタヌ蚭定",
   "navigation_bar.follow_requests": "フォロヌリク゚スト",
-  "navigation_bar.info": "このむンスタンスに぀いお",
+  "navigation_bar.info": "このサヌバヌに぀いお",
   "navigation_bar.keyboard_shortcuts": "ホットキヌ",
   "navigation_bar.lists": "リスト",
   "navigation_bar.logout": "ログアりト",
@@ -280,8 +280,8 @@
   "relative_time.seconds": "{number}秒前",
   "reply_indicator.cancel": "キャンセル",
   "report.forward": "{target} に転送する",
-  "report.forward_hint": "このアカりントは別のむンスタンスに所属しおいたす。通報内容を匿名で転送したすか",
-  "report.hint": "通報内容はあなたのむンスタンスのモデレヌタヌぞ送信されたす。通報理由を入力しおください。:",
+  "report.forward_hint": "このアカりントは別のサヌバヌに所属しおいたす。通報内容を匿名で転送したすか",
+  "report.hint": "通報内容はあなたのサヌバヌのモデレヌタヌぞ送信されたす。通報理由を入力しおください。:",
   "report.placeholder": "远加コメント",
   "report.submit": "通報する",
   "report.target": "{target}さんを通報する",
@@ -347,6 +347,7 @@
   "ui.beforeunload": "Mastodonから離れるず送信前の投皿は倱われたす。",
   "upload_area.title": "ドラッグドロップでアップロヌド",
   "upload_button.label": "メディアを远加 (JPEG, PNG, GIF, WebM, MP4, MOV)",
+  "upload_error.limit": "アップロヌドできる䞊限を超えおいたす。",
   "upload_form.description": "芖芚障害者のための説明",
   "upload_form.focus": "焊点",
   "upload_form.undo": "削陀",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 9f78c430f..e9e0c768d 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -347,6 +347,7 @@
   "ui.beforeunload": "Utracisz tworzony wpis, jeÅŒeli opuścisz Mastodona.",
   "upload_area.title": "Przeciągnij i upuść aby wysłać",
   "upload_button.label": "Dodaj zawartość multimedialną (JPEG, PNG, GIF, WebM, MP4, MOV)",
+  "upload_error.limit": "Przekroczono limit plików do wysłania.",
   "upload_form.description": "Wprowadź opis dla niewidomych i niedowidzących",
   "upload_form.focus": "Dopasuj podgląd",
   "upload_form.undo": "Usuń",
diff --git a/app/javascript/mastodon/utils/resize_image.js b/app/javascript/mastodon/utils/resize_image.js
index d1608094f..bbdbc865e 100644
--- a/app/javascript/mastodon/utils/resize_image.js
+++ b/app/javascript/mastodon/utils/resize_image.js
@@ -31,7 +31,7 @@ const loadImage = inputFile => new Promise((resolve, reject) => {
 });
 
 const getOrientation = (img, type = 'image/png') => new Promise(resolve => {
-  if (type !== 'image/jpeg') {
+  if (!['image/jpeg', 'image/webp'].includes(type)) {
     resolve(1);
     return;
   }
diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss
index 63a5c61b8..f4f458cf4 100644
--- a/app/javascript/styles/mastodon/accounts.scss
+++ b/app/javascript/styles/mastodon/accounts.scss
@@ -288,3 +288,7 @@
     border-bottom: 0;
   }
 }
+
+.directory__tag .trends__item__current {
+  width: auto;
+}
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 4e969601b..4dbbaa1e8 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -153,10 +153,15 @@ $content-width: 840px;
       font-weight: 500;
     }
 
-    .directory__tag a {
+    .directory__tag > a,
+    .directory__tag > div {
       box-shadow: none;
     }
 
+    .directory__tag .table-action-link .fa {
+      color: inherit;
+    }
+
     .directory__tag h4 {
       font-size: 18px;
       font-weight: 700;
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 8c1115e76..e29abf4f3 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -638,7 +638,6 @@
   font-weight: 400;
   overflow: hidden;
   text-overflow: ellipsis;
-  white-space: pre-wrap;
   padding-top: 2px;
   color: $primary-text-color;
 
@@ -662,6 +661,7 @@
 
   p {
     margin-bottom: 20px;
+    white-space: pre-wrap;
 
     &:last-child {
       margin-bottom: 0;
diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss
index c97337e4e..1eaf30c5b 100644
--- a/app/javascript/styles/mastodon/widgets.scss
+++ b/app/javascript/styles/mastodon/widgets.scss
@@ -269,7 +269,8 @@
     box-sizing: border-box;
     margin-bottom: 10px;
 
-    a {
+    & > a,
+    & > div {
       display: flex;
       align-items: center;
       justify-content: space-between;
@@ -279,7 +280,9 @@
       text-decoration: none;
       color: inherit;
       box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
+    }
 
+    & > a {
       &:hover,
       &:active,
       &:focus {
@@ -287,7 +290,7 @@
       }
     }
 
-    &.active a {
+    &.active > a {
       background: $ui-highlight-color;
       cursor: default;
     }
diff --git a/app/lib/activity_tracker.rb b/app/lib/activity_tracker.rb
index 5b4972674..ae3c11b6a 100644
--- a/app/lib/activity_tracker.rb
+++ b/app/lib/activity_tracker.rb
@@ -4,6 +4,8 @@ class ActivityTracker
   EXPIRE_AFTER = 90.days.seconds
 
   class << self
+    include Redisable
+
     def increment(prefix)
       key = [prefix, current_week].join(':')
 
@@ -20,10 +22,6 @@ class ActivityTracker
 
     private
 
-    def redis
-      Redis.current
-    end
-
     def current_week
       Time.zone.today.cweek
     end
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 87318fb1c..919678618 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -2,6 +2,7 @@
 
 class ActivityPub::Activity
   include JsonLdHelper
+  include Redisable
 
   def initialize(json, account, **options)
     @json    = json
@@ -70,10 +71,6 @@ class ActivityPub::Activity
     @object_uri ||= value_or_id(@object)
   end
 
-  def redis
-    Redis.current
-  end
-
   def distribute(status)
     crawl_links(status)
 
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index a1b186f1c..4bc75dae8 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -4,6 +4,7 @@ require 'singleton'
 
 class FeedManager
   include Singleton
+  include Redisable
 
   MAX_ITEMS = 400
 
@@ -35,7 +36,7 @@ class FeedManager
 
   def unpush_from_home(account, status)
     return false unless remove_from_feed(:home, account.id, status)
-    Redis.current.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s))
+    redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s))
     true
   end
 
@@ -54,7 +55,7 @@ class FeedManager
 
   def unpush_from_list(list, status)
     return false unless remove_from_feed(:list, list.id, status)
-    Redis.current.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s))
+    redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s))
     true
   end
 
@@ -143,10 +144,6 @@ class FeedManager
 
   private
 
-  def redis
-    Redis.current
-  end
-
   def push_update_required?(timeline_id)
     redis.exists("subscribed:#{timeline_id}")
   end
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index 05fd9eeb1..0653214f5 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -99,7 +99,7 @@ class Formatter
   end
 
   def encode_and_link_urls(html, accounts = nil, options = {})
-    entities = Extractor.extract_entities_with_indices(html, extract_url_without_protocol: false)
+    entities = utf8_friendly_extractor(html, extract_url_without_protocol: false)
 
     if accounts.is_a?(Hash)
       options  = accounts
@@ -199,6 +199,53 @@ class Formatter
     result.flatten.join
   end
 
+  UNICODE_ESCAPE_BLACKLIST_RE = /\p{Z}|\p{P}/
+
+  def utf8_friendly_extractor(text, options = {})
+    old_to_new_index = [0]
+
+    escaped = text.chars.map do |c|
+      output = begin
+        if c.ord.to_s(16).length > 2 && UNICODE_ESCAPE_BLACKLIST_RE.match(c).nil?
+          CGI.escape(c)
+        else
+          c
+        end
+      end
+
+      old_to_new_index << old_to_new_index.last + output.length
+
+      output
+    end.join
+
+    # Note: I couldn't obtain list_slug with @user/list-name format
+    # for mention so this requires additional check
+    special = Extractor.extract_urls_with_indices(escaped, options).map do |extract|
+      # exactly one of :url, :hashtag, :screen_name, :cashtag keys is present
+      key = (extract.keys & [:url, :hashtag, :screen_name, :cashtag]).first
+
+      new_indices = [
+        old_to_new_index.find_index(extract[:indices].first),
+        old_to_new_index.find_index(extract[:indices].last),
+      ]
+
+      has_prefix_char = [:hashtag, :screen_name, :cashtag].include?(key)
+      value_indices = [
+        new_indices.first + (has_prefix_char ? 1 : 0), # account for #, @ or $
+        new_indices.last - 1,
+      ]
+
+      next extract.merge(
+        :indices => new_indices,
+        key => text[value_indices.first..value_indices.last]
+      )
+    end
+
+    standard = Extractor.extract_entities_with_indices(text, options)
+
+    Extractor.remove_overlapping_entities(special + standard)
+  end
+
   def link_to_url(entity, options = {})
     url        = Addressable::URI.parse(entity[:url])
     html_attrs = { target: '_blank', rel: 'nofollow noopener' }
diff --git a/app/lib/ostatus/activity/base.rb b/app/lib/ostatus/activity/base.rb
index c5933f3ad..db70f1998 100644
--- a/app/lib/ostatus/activity/base.rb
+++ b/app/lib/ostatus/activity/base.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class OStatus::Activity::Base
+  include Redisable
+
   def initialize(xml, account = nil, **options)
     @xml     = xml
     @account = account
@@ -66,8 +68,4 @@ class OStatus::Activity::Base
       Status.find_by(uri: uri)
     end
   end
-
-  def redis
-    Redis.current
-  end
 end
diff --git a/app/lib/potential_friendship_tracker.rb b/app/lib/potential_friendship_tracker.rb
index 017a9748d..188aa4a27 100644
--- a/app/lib/potential_friendship_tracker.rb
+++ b/app/lib/potential_friendship_tracker.rb
@@ -11,6 +11,8 @@ class PotentialFriendshipTracker
   }.freeze
 
   class << self
+    include Redisable
+
     def record(account_id, target_account_id, action)
       return if account_id == target_account_id
 
@@ -31,11 +33,5 @@ class PotentialFriendshipTracker
       return [] if account_ids.empty?
       Account.searchable.where(id: account_ids)
     end
-
-    private
-
-    def redis
-      Redis.current
-    end
   end
 end
diff --git a/app/models/account_domain_block.rb b/app/models/account_domain_block.rb
index e352000c3..7c0d60379 100644
--- a/app/models/account_domain_block.rb
+++ b/app/models/account_domain_block.rb
@@ -12,6 +12,7 @@
 
 class AccountDomainBlock < ApplicationRecord
   include Paginable
+  include DomainNormalizable
 
   belongs_to :account
   validates :domain, presence: true, uniqueness: { scope: :account_id }
diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb
index 4e730451a..3ab8a0daa 100644
--- a/app/models/concerns/account_associations.rb
+++ b/app/models/concerns/account_associations.rb
@@ -56,5 +56,6 @@ module AccountAssociations
 
     # Hashtags
     has_and_belongs_to_many :tags
+    has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account
   end
 end
diff --git a/app/models/concerns/account_avatar.rb b/app/models/concerns/account_avatar.rb
index 2d5ebfca3..5fff3ef5d 100644
--- a/app/models/concerns/account_avatar.rb
+++ b/app/models/concerns/account_avatar.rb
@@ -3,7 +3,7 @@
 module AccountAvatar
   extend ActiveSupport::Concern
 
-  IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
+  IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
   LIMIT = 2.megabytes
 
   class_methods do
diff --git a/app/models/concerns/account_header.rb b/app/models/concerns/account_header.rb
index 067e166eb..a748fdff7 100644
--- a/app/models/concerns/account_header.rb
+++ b/app/models/concerns/account_header.rb
@@ -3,7 +3,7 @@
 module AccountHeader
   extend ActiveSupport::Concern
 
-  IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
+  IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
   LIMIT = 2.megabytes
   MAX_PIXELS = 750_000 # 1500x500px
 
diff --git a/app/models/concerns/domain_normalizable.rb b/app/models/concerns/domain_normalizable.rb
index dff3e5414..fb84058fc 100644
--- a/app/models/concerns/domain_normalizable.rb
+++ b/app/models/concerns/domain_normalizable.rb
@@ -10,6 +10,6 @@ module DomainNormalizable
   private
 
   def normalize_domain
-    self.domain = TagManager.instance.normalize_domain(domain)
+    self.domain = TagManager.instance.normalize_domain(domain&.strip)
   end
 end
diff --git a/app/models/concerns/redisable.rb b/app/models/concerns/redisable.rb
new file mode 100644
index 000000000..c6cf97359
--- /dev/null
+++ b/app/models/concerns/redisable.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Redisable
+  extend ActiveSupport::Concern
+
+  private
+
+  def redis
+    Redis.current
+  end
+end
diff --git a/app/models/export.rb b/app/models/export.rb
index a2520e9c2..fc4bb6964 100644
--- a/app/models/export.rb
+++ b/app/models/export.rb
@@ -1,4 +1,5 @@
 # frozen_string_literal: true
+
 require 'csv'
 
 class Export
diff --git a/app/models/featured_tag.rb b/app/models/featured_tag.rb
new file mode 100644
index 000000000..b5a10ad2d
--- /dev/null
+++ b/app/models/featured_tag.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: featured_tags
+#
+#  id             :bigint(8)        not null, primary key
+#  account_id     :bigint(8)
+#  tag_id         :bigint(8)
+#  statuses_count :bigint(8)        default(0), not null
+#  last_status_at :datetime
+#  created_at     :datetime         not null
+#  updated_at     :datetime         not null
+#
+
+class FeaturedTag < ApplicationRecord
+  belongs_to :account, inverse_of: :featured_tags, required: true
+  belongs_to :tag, inverse_of: :featured_tags, required: true
+
+  delegate :name, to: :tag, allow_nil: true
+
+  validates :name, presence: true
+  validate :validate_featured_tags_limit, on: :create
+
+  def name=(str)
+    self.tag = Tag.find_or_initialize_by(name: str.delete('#').mb_chars.downcase.to_s)
+  end
+
+  def increment(timestamp)
+    update(statuses_count: statuses_count + 1, last_status_at: timestamp)
+  end
+
+  def decrement(deleted_status_id)
+    update(statuses_count: [0, statuses_count - 1].max, last_status_at: account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).where.not(id: deleted_status_id).select(:created_at).first&.created_at)
+  end
+
+  def reset_data
+    self.statuses_count = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).count
+    self.last_status_at = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).select(:created_at).first&.created_at
+  end
+
+  private
+
+  def validate_featured_tags_limit
+    errors.add(:base, I18n.t('featured_tags.errors.limit')) if account.featured_tags.count >= 10
+  end
+end
diff --git a/app/models/feed.rb b/app/models/feed.rb
index 5bce88f25..0e8943ff8 100644
--- a/app/models/feed.rb
+++ b/app/models/feed.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class Feed
+  include Redisable
+
   def initialize(type, id)
     @type = type
     @id   = id
@@ -27,8 +29,4 @@ class Feed
   def key
     FeedManager.instance.key(@type, @id)
   end
-
-  def redis
-    Redis.current
-  end
 end
diff --git a/app/models/import.rb b/app/models/import.rb
index 55e970b0d..a7a0d8065 100644
--- a/app/models/import.rb
+++ b/app/models/import.rb
@@ -13,20 +13,30 @@
 #  data_file_size    :integer
 #  data_updated_at   :datetime
 #  account_id        :bigint(8)        not null
+#  overwrite         :boolean          default(FALSE), not null
 #
 
 class Import < ApplicationRecord
-  FILE_TYPES = ['text/plain', 'text/csv'].freeze
+  FILE_TYPES = %w(text/plain text/csv).freeze
+  MODES = %i(merge overwrite).freeze
 
   self.inheritance_column = false
 
   belongs_to :account
 
-  enum type: [:following, :blocking, :muting]
+  enum type: [:following, :blocking, :muting, :domain_blocking]
 
   validates :type, presence: true
 
   has_attached_file :data
   validates_attachment_content_type :data, content_type: FILE_TYPES
   validates_attachment_presence :data
+
+  def mode
+    overwrite? ? :overwrite : :merge
+  end
+
+  def mode=(str)
+    self.overwrite = str.to_sym == :overwrite
+  end
 end
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 601b14223..81397a18e 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -25,11 +25,11 @@ class MediaAttachment < ApplicationRecord
 
   enum type: [:image, :gifv, :video, :audio, :unknown]
 
-  IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif'].freeze
+  IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].freeze
   VIDEO_FILE_EXTENSIONS = ['.webm', '.mp4', '.m4v', '.mov'].freeze
   AUDIO_FILE_EXTENSIONS = ['.mp3', '.m4a', '.wav', '.ogg'].freeze
 
-  IMAGE_MIME_TYPES             = ['image/jpeg', 'image/png', 'image/gif'].freeze
+  IMAGE_MIME_TYPES             = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
   VIDEO_MIME_TYPES             = ['video/webm', 'video/mp4', 'video/quicktime'].freeze
   VIDEO_CONVERTIBLE_MIME_TYPES = ['video/webm', 'video/quicktime'].freeze
   AUDIO_MIME_TYPES             = ['audio/mpeg', 'audio/mp4', 'audio/vnd.wav', 'audio/wav', 'audio/x-wav', 'audio/x-wave', 'audio/ogg',].freeze
@@ -105,8 +105,8 @@ class MediaAttachment < ApplicationRecord
                     convert_options: { all: '-quality 90 -strip' }
 
   validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES
-  validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :video?
-  validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :video?
+  validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :video_or_gifv?
+  validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :video_or_gifv?
   remotable_attachment :file, VIDEO_LIMIT
 
   include Attachmentable
@@ -129,6 +129,10 @@ class MediaAttachment < ApplicationRecord
     file.blank? && remote_url.present?
   end
 
+  def video_or_gifv?
+    video? || gifv?
+  end
+
   def to_param
     shortcode
   end
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index a792b352b..f26ea0c74 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -25,7 +25,7 @@
 #
 
 class PreviewCard < ApplicationRecord
-  IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
+  IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
   LIMIT = 1.megabytes
 
   self.inheritance_column = false
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 99830ae92..4373e967b 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -14,6 +14,7 @@ class Tag < ApplicationRecord
   has_and_belongs_to_many :accounts
   has_and_belongs_to_many :sample_accounts, -> { searchable.discoverable.popular.limit(3) }, class_name: 'Account'
 
+  has_many :featured_tags, dependent: :destroy, inverse_of: :tag
   has_one :account_tag_stat, dependent: :destroy
 
   HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_·][[:word:]_]*'
@@ -23,6 +24,7 @@ class Tag < ApplicationRecord
 
   scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(Arel.sql('account_tag_stats.accounts_count desc')) }
   scope :hidden, -> { where(account_tag_stats: { hidden: true }) }
+  scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) }
 
   delegate :accounts_count,
            :accounts_count=,
diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb
index 3a8be2164..148535c21 100644
--- a/app/models/trending_tags.rb
+++ b/app/models/trending_tags.rb
@@ -7,6 +7,8 @@ class TrendingTags
   THRESHOLD            = 5
 
   class << self
+    include Redisable
+
     def record_use!(tag, account, at_time = Time.now.utc)
       return if disallowed_hashtags.include?(tag.name) || account.silenced? || account.bot?
 
@@ -59,9 +61,5 @@ class TrendingTags
       @disallowed_hashtags = @disallowed_hashtags.split(' ') if @disallowed_hashtags.is_a? String
       @disallowed_hashtags = @disallowed_hashtags.map(&:downcase)
     end
-
-    def redis
-      Redis.current
-    end
   end
 end
diff --git a/app/serializers/manifest_serializer.rb b/app/serializers/manifest_serializer.rb
index 859ef0d14..cc8b9a4d4 100644
--- a/app/serializers/manifest_serializer.rb
+++ b/app/serializers/manifest_serializer.rb
@@ -52,6 +52,14 @@ class ManifestSerializer < ActiveModel::Serializer
   end
 
   def share_target
-    { url_template: 'share?title={title}&text={text}&url={url}' }
+    {
+      url_template: 'share?title={title}&text={text}&url={url}',
+      action: 'share',
+      params: {
+        title: 'title',
+        text: 'text',
+        url: 'url',
+      },
+    }
   end
 end
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index 487456f3a..5e3308428 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -212,7 +212,7 @@ class ActivityPub::ProcessAccountService < BaseService
   end
 
   def clear_tombstones!
-    Tombstone.delete_all(account_id: @account.id)
+    Tombstone.where(account_id: @account.id).delete_all
   end
 
   def protocol_changed?
diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb
index 61c408926..d78f506c6 100644
--- a/app/services/batched_remove_status_service.rb
+++ b/app/services/batched_remove_status_service.rb
@@ -2,6 +2,7 @@
 
 class BatchedRemoveStatusService < BaseService
   include StreamEntryRenderer
+  include Redisable
 
   # Delete given statuses and reblogs of them
   # Dispatch PuSH updates of the deleted statuses, but only local ones
@@ -120,10 +121,6 @@ class BatchedRemoveStatusService < BaseService
     end
   end
 
-  def redis
-    Redis.current
-  end
-
   def build_xml(stream_entry)
     return @activity_xml[stream_entry.id] if @activity_xml.key?(stream_entry.id)
 
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index 9d36a1449..92d8c864a 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class FollowService < BaseService
+  include Redisable
+
   # Follow a remote user, notify remote user about the follow
   # @param [Account] source_account From which to follow
   # @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
@@ -67,10 +69,6 @@ class FollowService < BaseService
     follow
   end
 
-  def redis
-    Redis.current
-  end
-
   def build_follow_request_xml(follow_request)
     OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_request_salmon(follow_request))
   end
diff --git a/app/services/import_service.rb b/app/services/import_service.rb
new file mode 100644
index 000000000..3f558626e
--- /dev/null
+++ b/app/services/import_service.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'csv'
+
+class ImportService < BaseService
+  ROWS_PROCESSING_LIMIT = 20_000
+
+  def call(import)
+    @import  = import
+    @account = @import.account
+    @data    = CSV.new(import_data).reject(&:blank?)
+
+    case @import.type
+    when 'following'
+      import_follows!
+    when 'blocking'
+      import_blocks!
+    when 'muting'
+      import_mutes!
+    when 'domain_blocking'
+      import_domain_blocks!
+    end
+  end
+
+  private
+
+  def import_follows!
+    import_relationships!('follow', 'unfollow', @account.following, follow_limit)
+  end
+
+  def import_blocks!
+    import_relationships!('block', 'unblock', @account.blocking, ROWS_PROCESSING_LIMIT)
+  end
+
+  def import_mutes!
+    import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT)
+  end
+
+  def import_domain_blocks!
+    items = @data.take(ROWS_PROCESSING_LIMIT).map { |row| row.first.strip }
+
+    if @import.overwrite?
+      presence_hash = items.each_with_object({}) { |id, mapping| mapping[id] = true }
+
+      @account.domain_blocks.find_each do |domain_block|
+        if presence_hash[domain_block.domain]
+          items.delete(domain_block.domain)
+        else
+          @account.unblock_domain!(domain_block.domain)
+        end
+      end
+    end
+
+    items.each do |domain|
+      @account.block_domain!(domain)
+    end
+
+    AfterAccountDomainBlockWorker.push_bulk(items) do |domain|
+      [@account.id, domain]
+    end
+  end
+
+  def import_relationships!(action, undo_action, overwrite_scope, limit)
+    items = @data.take(limit).map { |row| row.first.strip }
+
+    if @import.overwrite?
+      presence_hash = items.each_with_object({}) { |id, mapping| mapping[id] = true }
+
+      overwrite_scope.find_each do |target_account|
+        if presence_hash[target_account.acct]
+          items.delete(target_account.acct)
+        else
+          Import::RelationshipWorker.perform_async(@account.id, target_account.acct, undo_action)
+        end
+      end
+    end
+
+    Import::RelationshipWorker.push_bulk(items) do |acct|
+      [@account.id, acct, action]
+    end
+  end
+
+  def import_data
+    Paperclip.io_adapters.for(@import.data).read
+  end
+
+  def follow_limit
+    FollowLimitValidator.limit_for_account(@account)
+  end
+end
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 5d431c42a..cfb266fbb 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class PostStatusService < BaseService
+  include Redisable
+
   MIN_SCHEDULE_OFFSET = 5.minutes.freeze
 
   # Post a text status update, fetch and notify remote users mentioned
@@ -115,10 +117,6 @@ class PostStatusService < BaseService
     ProcessHashtagsService.new
   end
 
-  def redis
-    Redis.current
-  end
-
   def scheduled?
     @scheduled_at.present?
   end
diff --git a/app/services/process_hashtags_service.rb b/app/services/process_hashtags_service.rb
index cf7471c98..d5ec076a8 100644
--- a/app/services/process_hashtags_service.rb
+++ b/app/services/process_hashtags_service.rb
@@ -2,12 +2,22 @@
 
 class ProcessHashtagsService < BaseService
   def call(status, tags = [])
-    tags = Extractor.extract_hashtags(status.text) if status.local?
+    tags    = Extractor.extract_hashtags(status.text) if status.local?
+    records = []
 
     tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |name|
       tag = Tag.where(name: name).first_or_create(name: name)
+
       status.tags << tag
+      records << tag
+
       TrendingTags.record_use!(tag, status.account, status.created_at) if status.public_visibility?
     end
+
+    return unless status.public_visibility? || status.unlisted_visibility?
+
+    status.account.featured_tags.where(tag_id: records.map(&:id)).each do |featured_tag|
+      featured_tag.increment(status.created_at)
+    end
   end
 end
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index 4bee86c8a..99c8e6cbb 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -2,6 +2,7 @@
 
 class RemoveStatusService < BaseService
   include StreamEntryRenderer
+  include Redisable
 
   def call(status, **options)
     @payload      = Oj.dump(event: :delete, payload: status.id.to_s)
@@ -56,7 +57,7 @@ class RemoveStatusService < BaseService
 
   def remove_from_affected
     @mentions.map(&:account).select(&:local?).each do |account|
-      Redis.current.publish("timeline:#{account.id}", @payload)
+      redis.publish("timeline:#{account.id}", @payload)
     end
   end
 
@@ -131,26 +132,30 @@ class RemoveStatusService < BaseService
   end
 
   def remove_from_hashtags
+    @account.featured_tags.where(tag_id: @status.tags.pluck(:id)).each do |featured_tag|
+      featured_tag.decrement(@status.id)
+    end
+
     return unless @status.public_visibility?
 
     @tags.each do |hashtag|
-      Redis.current.publish("timeline:hashtag:#{hashtag}", @payload)
-      Redis.current.publish("timeline:hashtag:#{hashtag}:local", @payload) if @status.local?
+      redis.publish("timeline:hashtag:#{hashtag}", @payload)
+      redis.publish("timeline:hashtag:#{hashtag}:local", @payload) if @status.local?
     end
   end
 
   def remove_from_public
     return unless @status.public_visibility?
 
-    Redis.current.publish('timeline:public', @payload)
-    Redis.current.publish('timeline:public:local', @payload) if @status.local?
+    redis.publish('timeline:public', @payload)
+    redis.publish('timeline:public:local', @payload) if @status.local?
   end
 
   def remove_from_media
     return unless @status.public_visibility?
 
-    Redis.current.publish('timeline:public:media', @payload)
-    Redis.current.publish('timeline:public:local:media', @payload) if @status.local?
+    redis.publish('timeline:public:media', @payload)
+    redis.publish('timeline:public:local:media', @payload) if @status.local?
   end
 
   def remove_from_direct
@@ -159,8 +164,4 @@ class RemoveStatusService < BaseService
     end
     Redis.current.publish("timeline:direct:#{@account.id}", @payload) if @account.local?
   end
-
-  def redis
-    Redis.current
-  end
 end
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index 0ee9dd7de..0da69728f 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -63,4 +63,17 @@
         - @endorsed_accounts.each do |account|
           = account_link_to account
 
+    - @account.featured_tags.order(statuses_count: :desc).each do |featured_tag|
+      .directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil }
+        = link_to short_account_tag_path(@account, featured_tag.tag) do
+          %h4
+            = fa_icon 'hashtag'
+            = featured_tag.name
+            %small
+              - if featured_tag.last_status_at.nil?
+                = t('accounts.nothing_here')
+              - else
+                %time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
+          .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true
+
     = render 'application/sidebar'
diff --git a/app/views/admin/change_emails/show.html.haml b/app/views/admin/change_emails/show.html.haml
index 6febef9b1..6ff0d785e 100644
--- a/app/views/admin/change_emails/show.html.haml
+++ b/app/views/admin/change_emails/show.html.haml
@@ -3,7 +3,7 @@
 
 = simple_form_for @user, url: admin_account_change_email_path(@account.id) do |f|
   .fields-group
-    = f.input :email, wrapper: :with_label, disabled: true, label: t('admin.accounts.change_email.current_email')
+    = f.input :email, wrapper: :with_label, hint: false, disabled: true, label: t('admin.accounts.change_email.current_email')
 
   .fields-group
     = f.input :unconfirmed_email, wrapper: :with_label, label: t('admin.accounts.change_email.new_email')
diff --git a/app/views/settings/featured_tags/index.html.haml b/app/views/settings/featured_tags/index.html.haml
new file mode 100644
index 000000000..5f69517f3
--- /dev/null
+++ b/app/views/settings/featured_tags/index.html.haml
@@ -0,0 +1,27 @@
+- content_for :page_title do
+  = t('settings.featured_tags')
+
+= simple_form_for @featured_tag, url: settings_featured_tags_path do |f|
+  = render 'shared/error_messages', object: @featured_tag
+
+  .fields-group
+    = f.input :name, wrapper: :with_block_label, hint: safe_join([t('simple_form.hints.featured_tag.name'), safe_join(@most_used_tags.map { |tag| link_to("##{tag.name}", settings_featured_tags_path(featured_tag: { name: tag.name }), method: :post) }, ', ')], ' ')
+
+  .actions
+    = f.button :button, t('featured_tags.add_new'), type: :submit
+
+%hr.spacer/
+
+- @featured_tags.each do |featured_tag|
+  .directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil }
+    %div
+      %h4
+        = fa_icon 'hashtag'
+        = featured_tag.name
+        %small
+          - if featured_tag.last_status_at.nil?
+            = t('accounts.nothing_here')
+          - else
+            %time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
+          = table_link_to 'trash', t('filters.index.delete'), settings_featured_tag_path(featured_tag), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
+      .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true
diff --git a/app/views/settings/imports/show.html.haml b/app/views/settings/imports/show.html.haml
index 4512fc714..7bb4beb01 100644
--- a/app/views/settings/imports/show.html.haml
+++ b/app/views/settings/imports/show.html.haml
@@ -5,8 +5,11 @@
   .field-group
     = f.input :type, collection: Import.types.keys, wrapper: :with_block_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }, hint: t('imports.preface')
 
-  .field-group
-    = f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data')
+  .fields-row
+    .fields-group.fields-row__column.fields-row__column-6
+      = f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data')
+    .fields-group.fields-row__column.fields-row__column-6
+      = f.input :mode, as: :radio_buttons, collection: Import::MODES, label_method: lambda { |mode| safe_join([I18n.t("imports.modes.#{mode}"), content_tag(:span, I18n.t("imports.modes.#{mode}_long"), class: 'hint')]) }, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
 
   .actions
     = f.button :button, t('imports.upload'), type: :submit
diff --git a/app/workers/import/relationship_worker.rb b/app/workers/import/relationship_worker.rb
index 1dd8bf8fb..e9db20a46 100644
--- a/app/workers/import/relationship_worker.rb
+++ b/app/workers/import/relationship_worker.rb
@@ -13,11 +13,17 @@ class Import::RelationshipWorker
 
     case relationship
     when 'follow'
-      FollowService.new.call(from_account, target_account.acct)
+      FollowService.new.call(from_account, target_account)
+    when 'unfollow'
+      UnfollowService.new.call(from_account, target_account)
     when 'block'
       BlockService.new.call(from_account, target_account)
+    when 'unblock'
+      UnblockService.new.call(from_account, target_account)
     when 'mute'
       MuteService.new.call(from_account, target_account)
+    when 'unmute'
+      UnmuteService.new.call(from_account, target_account)
     end
   rescue ActiveRecord::RecordNotFound
     true
diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb
index aeb221cf6..dfa71b29e 100644
--- a/app/workers/import_worker.rb
+++ b/app/workers/import_worker.rb
@@ -1,44 +1,14 @@
 # frozen_string_literal: true
 
-require 'csv'
-
 class ImportWorker
   include Sidekiq::Worker
 
   sidekiq_options queue: 'pull', retry: false
 
-  attr_reader :import
-
   def perform(import_id)
-    @import = Import.find(import_id)
-
-    Import::RelationshipWorker.push_bulk(import_rows) do |row|
-      [@import.account_id, row.first, relationship_type]
-    end
-
-    @import.destroy
-  end
-
-  private
-
-  def import_contents
-    Paperclip.io_adapters.for(@import.data).read
-  end
-
-  def relationship_type
-    case @import.type
-    when 'following'
-      'follow'
-    when 'blocking'
-      'block'
-    when 'muting'
-      'mute'
-    end
-  end
-
-  def import_rows
-    rows = CSV.new(import_contents).reject(&:blank?)
-    rows = rows.take(FollowLimitValidator.limit_for_account(@import.account)) if @import.type == 'following'
-    rows
+    import = Import.find(import_id)
+    ImportService.new.call(import)
+  ensure
+    import&.destroy
   end
 end
diff --git a/app/workers/scheduler/feed_cleanup_scheduler.rb b/app/workers/scheduler/feed_cleanup_scheduler.rb
index cd2273418..bf5e20757 100644
--- a/app/workers/scheduler/feed_cleanup_scheduler.rb
+++ b/app/workers/scheduler/feed_cleanup_scheduler.rb
@@ -2,6 +2,7 @@
 
 class Scheduler::FeedCleanupScheduler
   include Sidekiq::Worker
+  include Redisable
 
   sidekiq_options unique: :until_executed, retry: 0
 
@@ -57,8 +58,4 @@ class Scheduler::FeedCleanupScheduler
   def feed_manager
     FeedManager.instance
   end
-
-  def redis
-    Redis.current
-  end
 end
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index e9564692f..b2a621e85 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -35,11 +35,8 @@ ignore_missing:
   - 'activemodel.errors.*'
   - 'activerecord.attributes.*'
   - 'activerecord.errors.*'
-  - '{devise,pagination,doorkeeper}.*'
+  - '{pagination,doorkeeper}.*'
   - '{date,datetime,time,number}.*'
-  - 'simple_form.{yes,no}'
-  - 'simple_form.{placeholders,hints,labels}.*'
-  - 'simple_form.{error_notification,required}.:'
   - 'errors.messages.*'
   - 'activerecord.errors.models.doorkeeper/*'
   - 'sessions.{browsers,platforms}.*'
diff --git a/config/initializers/twitter_regex.rb b/config/initializers/twitter_regex.rb
index 0e8f5bfeb..0ddbbee98 100644
--- a/config/initializers/twitter_regex.rb
+++ b/config/initializers/twitter_regex.rb
@@ -1,7 +1,7 @@
 module Twitter
   class Regex
-    REGEXEN[:valid_general_url_path_chars] = /[^\p{White_Space}\(\)\?]/iou
-    REGEXEN[:valid_url_path_ending_chars] = /[^\p{White_Space}\(\)\?!\*';:=\,\.\$%\[\]~&\|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou
+    REGEXEN[:valid_general_url_path_chars] = /[^\p{White_Space}<>\(\)\?]/iou
+    REGEXEN[:valid_url_path_ending_chars] = /[^\p{White_Space}\(\)\?!\*"'「」<>;:=\,\.\$%\[\]~&\|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou
     REGEXEN[:valid_url_balanced_parens] = /
       \(
         (?:
diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml
index bd0642b25..726c0504e 100644
--- a/config/locales/devise.en.yml
+++ b/config/locales/devise.en.yml
@@ -20,17 +20,17 @@ en:
         action: Verify email address
         action_with_app: Confirm and return to %{app}
         explanation: You have created an account on %{host} with this email address. You are one click away from activating it. If this wasn't you, please ignore this email.
-        extra_html: Please also check out <a href="%{terms_path}">the rules of the instance</a> and <a href="%{policy_path}">our terms of service</a>.
+        extra_html: Please also check out <a href="%{terms_path}">the rules of the server</a> and <a href="%{policy_path}">our terms of service</a>.
         subject: 'Mastodon: Confirmation instructions for %{instance}'
         title: Verify email address
       email_changed:
         explanation: 'The email address for your account is being changed to:'
-        extra: If you did not change your email, it is likely that someone has gained access to your account. Please change your password immediately or contact the instance admin if you're locked out of your account.
+        extra: If you did not change your email, it is likely that someone has gained access to your account. Please change your password immediately or contact the server admin if you're locked out of your account.
         subject: 'Mastodon: Email changed'
         title: New email address
       password_change:
         explanation: The password for your account has been changed.
-        extra: If you did not change your password, it is likely that someone has gained access to your account. Please change your password immediately or contact the instance admin if you're locked out of your account.
+        extra: If you did not change your password, it is likely that someone has gained access to your account. Please change your password immediately or contact the server admin if you're locked out of your account.
         subject: 'Mastodon: Password changed'
         title: Password changed
       reconfirmation_instructions:
diff --git a/config/locales/devise.ja.yml b/config/locales/devise.ja.yml
index cae76d493..bd2bb71bb 100644
--- a/config/locales/devise.ja.yml
+++ b/config/locales/devise.ja.yml
@@ -20,17 +20,17 @@ ja:
         action: メヌルアドレスの確認
         action_with_app: 確認し %{app} に戻る
         explanation: このメヌルアドレスで%{host}にアカりントを䜜成したした。有効にするたであず䞀歩です。もし心圓たりがない堎合、申し蚳ありたせんがこのメヌルを無芖しおください。
-        extra_html: たた <a href="%{terms_path}">むンスタンスのルヌル</a> ず <a href="%{policy_path}">利甚芏玄</a> もお読みください。
+        extra_html: たた <a href="%{terms_path}">サヌバヌのルヌル</a> ず <a href="%{policy_path}">利甚芏玄</a> もお読みください。
         subject: 'Mastodon: メヌルアドレスの確認 %{instance}'
         title: メヌルアドレスの確認
       email_changed:
         explanation: 'アカりントのメヌルアドレスは以䞋のように倉曎されたす:'
-        extra: メヌルアドレスの倉曎を行っおいない堎合、他の誰かがあなたのアカりントにアクセスした可胜性がありたす。すぐにパスワヌドを倉曎するか、アカりントがロックされおいる堎合はむンスタンス管理者に連絡しおください。
+        extra: メヌルアドレスの倉曎を行っおいない堎合、他の誰かがあなたのアカりントにアクセスした可胜性がありたす。すぐにパスワヌドを倉曎するか、アカりントがロックされおいる堎合はサヌバヌ管理者に連絡しおください。
         subject: 'Mastodon: メヌルアドレスの倉曎'
         title: 新しいメヌルアドレス
       password_change:
         explanation: パスワヌドが倉曎されたした。
-        extra: パスワヌドの倉曎を行っおいない堎合、他の誰かがあなたのアカりントにアクセスした可胜性がありたす。すぐにパスワヌドを倉曎するか、アカりントがロックされおいる堎合はむンスタンス管理者に連絡しおください。
+        extra: パスワヌドの倉曎を行っおいない堎合、他の誰かがあなたのアカりントにアクセスした可胜性がありたす。すぐにパスワヌドを倉曎するか、アカりントがロックされおいる堎合はサヌバヌ管理者に連絡しおください。
         subject: 'Mastodon: パスワヌドが倉曎されたした'
         title: パスワヌドの倉曎
       reconfirmation_instructions:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index b8d80a748..a9553aace 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -7,7 +7,7 @@ en:
     administered_by: 'Administered by:'
     api: API
     apps: Mobile apps
-    closed_registrations: Registrations are currently closed on this instance. However! You can find a different instance to make an account on and get access to the very same network from there.
+    closed_registrations: Registrations are currently closed on this server. However! You can find a different server to make an account on and get access to the very same network from there.
     contact: Contact
     contact_missing: Not set
     contact_unavailable: N/A
@@ -27,7 +27,7 @@ en:
     generic_description: "%{domain} is one server in the network"
     hosted_on: Mastodon hosted on %{domain}
     learn_more: Learn more
-    other_instances: Instance list
+    other_instances: Server list
     privacy_policy: Privacy policy
     source_code: Source code
     status_count_after:
@@ -386,7 +386,7 @@ en:
         desc_html: Modify the look with CSS loaded on every page
         title: Custom CSS
       hero:
-        desc_html: Displayed on the frontpage. At least 600x100px recommended. When not set, falls back to instance thumbnail
+        desc_html: Displayed on the frontpage. At least 600x100px recommended. When not set, falls back to server thumbnail
         title: Hero image
       hide_followers_count:
         desc_html: Do not show followers count on user profiles
@@ -395,8 +395,8 @@ en:
         desc_html: Displayed on multiple pages. At least 293×205px recommended. When not set, falls back to default mascot
         title: Mascot image
       peers_api_enabled:
-        desc_html: Domain names this instance has encountered in the fediverse
-        title: Publish list of discovered instances
+        desc_html: Domain names this server has encountered in the fediverse
+        title: Publish list of discovered servers
       preview_sensitive_media:
         desc_html: Link previews on other websites will display a thumbnail even if the media is marked as sensitive
         title: Show sensitive media in OpenGraph previews
@@ -424,20 +424,20 @@ en:
         title: Show staff badge
       site_description:
         desc_html: Introductory paragraph on the frontpage. Describe what makes this Mastodon server special and anything else important. You can use HTML tags, in particular <code>&lt;a&gt;</code> and <code>&lt;em&gt;</code>.
-        title: Instance description
+        title: Server description
       site_description_extended:
-        desc_html: A good place for your code of conduct, rules, guidelines and other things that set your instance apart. You can use HTML tags
+        desc_html: A good place for your code of conduct, rules, guidelines and other things that set your server apart. You can use HTML tags
         title: Custom extended information
       site_short_description:
-        desc_html: Displayed in sidebar and meta tags. Describe what Mastodon is and what makes this server special in a single paragraph. If empty, defaults to instance description.
-        title: Short instance description
+        desc_html: Displayed in sidebar and meta tags. Describe what Mastodon is and what makes this server special in a single paragraph. If empty, defaults to server description.
+        title: Short server description
       site_terms:
         desc_html: You can write your own privacy policy, terms of service or other legalese. You can use HTML tags
         title: Custom terms of service
-      site_title: Instance name
+      site_title: Server name
       thumbnail:
         desc_html: Used for previews via OpenGraph and API. 1200x630px recommended
-        title: Instance thumbnail
+        title: Server thumbnail
       timeline_preview:
         desc_html: Display public timeline on landing page
         title: Timeline preview
@@ -498,7 +498,7 @@ en:
     warning: Be very careful with this data. Never share it with anyone!
     your_token: Your access token
   auth:
-    agreement_html: By clicking "Sign up" below you agree to follow <a href="%{rules_path}">the rules of the instance</a> and <a href="%{terms_path}">our terms of service</a>.
+    agreement_html: By clicking "Sign up" below you agree to follow <a href="%{rules_path}">the rules of the server</a> and <a href="%{terms_path}">our terms of service</a>.
     change_password: Password
     confirm_email: Confirm email
     delete_account: Delete account
@@ -552,7 +552,7 @@ en:
     description_html: This will <strong>permanently, irreversibly</strong> remove content from your account and deactivate it. Your username will remain reserved to prevent future impersonations.
     proceed: Delete account
     success_msg: Your account was successfully deleted
-    warning_html: Only deletion of content from this particular instance is guaranteed. Content that has been widely shared is likely to leave traces. Offline servers and servers that have unsubscribed from your updates will not update their databases.
+    warning_html: Only deletion of content from this particular server is guaranteed. Content that has been widely shared is likely to leave traces. Offline servers and servers that have unsubscribed from your updates will not update their databases.
     warning_title: Disseminated content availability
   directories:
     directory: Profile directory
@@ -591,6 +591,10 @@ en:
     lists: Lists
     mutes: You mute
     storage: Media storage
+  featured_tags:
+    add_new: Add new
+    errors:
+      limit: You have already featured the maximum amount of hashtags
   filters:
     contexts:
       home: Home timeline
@@ -609,7 +613,7 @@ en:
       title: Add new filter
   followers:
     domain: Domain
-    explanation_html: If you want to ensure the privacy of your statuses, you must be aware of who is following you. <strong>Your private statuses are delivered to all instances where you have followers</strong>. You may wish to review them, and remove followers if you do not trust your privacy to be respected by the staff or software of those instances.
+    explanation_html: If you want to ensure the privacy of your statuses, you must be aware of who is following you. <strong>Your private statuses are delivered to all servers where you have followers</strong>. You may wish to review them, and remove followers if you do not trust your privacy to be respected by the staff or software of those servers.
     followers_count: Number of followers
     lock_link: Lock your account
     purge: Remove from followers
@@ -632,10 +636,16 @@ en:
       one: Something isn't quite right yet! Please review the error below
       other: Something isn't quite right yet! Please review %{count} errors below
   imports:
-    preface: You can import data that you have exported from another instance, such as a list of the people you are following or blocking.
+    modes:
+      merge: Merge
+      merge_long: Keep existing records and add new ones
+      overwrite: Overwrite
+      overwrite_long: Replace current records with the new ones
+    preface: You can import data that you have exported from another server, such as a list of the people you are following or blocking.
     success: Your data was successfully uploaded and will now be processed in due time
     types:
       blocking: Blocking list
+      domain_blocking: Domain blocking list
       following: Following list
       muting: Muting list
     upload: Upload
@@ -657,7 +667,7 @@ en:
       one: 1 use
       other: "%{count} uses"
     max_uses_prompt: No limit
-    prompt: Generate and share links with others to grant access to this instance
+    prompt: Generate and share links with others to grant access to this server
     table:
       expires_at: Expires
       uses: Uses
@@ -805,6 +815,7 @@ en:
     development: Development
     edit_profile: Edit profile
     export: Data export
+    featured_tags: Featured hashtags
     flavours: Flavours
     followers: Authorized followers
     import: Import
@@ -985,7 +996,7 @@ en:
       final_action: Start posting
       final_step: 'Start posting! Even without followers your public messages may be seen by others, for example on the local timeline and in hashtags. You may want to introduce yourself on the #introductions hashtag.'
       full_handle: Your full handle
-      full_handle_hint: This is what you would tell your friends so they can message or follow you from another instance.
+      full_handle_hint: This is what you would tell your friends so they can message or follow you from another server.
       review_preferences_action: Change preferences
       review_preferences_step: Make sure to set your preferences, such as which emails you'd like to receive, or what privacy level you’d like your posts to default to. If you don’t have motion sickness, you could choose to enable GIF autoplay.
       subject: Welcome to Mastodon
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 2b1b70639..e16ce31a9 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -7,7 +7,7 @@ ja:
     administered_by: '管理者:'
     api: API
     apps: アプリ
-    closed_registrations: 珟圚このむンスタンスでの新芏登録は受け付けおいたせん。しかし、他のむンスタンスにアカりントを䜜成しおも党く同じネットワヌクに参加するこずができたす。
+    closed_registrations: 珟圚このサヌバヌでの新芏登録は受け付けおいたせん。しかし、他のサヌバヌにアカりントを䜜成しおも党く同じネットワヌクに参加するこずができたす。
     contact: 連絡先
     contact_missing: 未蚭定
     contact_unavailable: N/A
@@ -24,10 +24,10 @@ ja:
       real_conversation_title: 本圓のコミュニケヌションのために
       within_reach_body: デベロッパヌフレンドリヌな API により実珟された、iOS や Android、その他様々なプラットフォヌムのためのアプリでどこでも友人ずやりずりできたす。
       within_reach_title: い぀でも身近に
-    generic_description: "%{domain} は、Mastodon むンスタンスの䞀぀です"
+    generic_description: "%{domain} は、Mastodon サヌバヌの䞀぀です"
     hosted_on: Mastodon hosted on %{domain}
     learn_more: もっず詳しく
-    other_instances: 他のむンスタンス
+    other_instances: 他のサヌバヌ
     privacy_policy: プラむバシヌポリシヌ
     source_code: ゜ヌスコヌド
     status_count_after:
@@ -310,7 +310,7 @@ ja:
         all: すべお
         limited: 制限あり
         title: モデレヌション
-      title: 既知のむンスタンス
+      title: 既知のサヌバヌ
       total_blocked_by_us: ブロック合蚈
       total_followed_by_them: 被フォロヌ合蚈
       total_followed_by_us: フォロヌ合蚈
@@ -392,8 +392,8 @@ ja:
         desc_html: 耇数のペヌゞに衚瀺されたす。サむズは293x205px以䞊掚奚です。未蚭定の堎合、暙準のマスコットが䜿甚されたす
         title: マスコットむメヌゞ
       peers_api_enabled:
-        desc_html: 連合内でこのむンスタンスが遭遇したドメむンの名前
-        title: 接続しおいるむンスタンスのリストを公開する
+        desc_html: 連合内でこのサヌバヌが遭遇したドメむンの名前
+        title: 接続しおいるサヌバヌのリストを公開する
       preview_sensitive_media:
         desc_html: 他のりェブサむトにリンクを貌った際、メディアが閲芧泚意ずしおマヌクされおいおもサムネむルが衚瀺されたす
         title: OpenGraphによるプレビュヌで閲芧泚意のメディアも衚瀺する
@@ -420,21 +420,21 @@ ja:
         desc_html: ナヌザヌペヌゞにスタッフのバッゞを衚瀺したす
         title: スタッフバッゞを衚瀺する
       site_description:
-        desc_html: フロントペヌゞぞの衚瀺に䜿甚される玹介文です。このMastodonむンスタンスを特城付けるこずやその他重芁なこずを蚘述しおください。HTMLタグ、特に<code>&lt;a&gt;</code> ず <code>&lt;em&gt;</code>が䜿えたす。
-        title: むンスタンスの説明
+        desc_html: フロントペヌゞぞの衚瀺に䜿甚される玹介文です。このMastodonサヌバヌを特城付けるこずやその他重芁なこずを蚘述しおください。HTMLタグ、特に<code>&lt;a&gt;</code> ず <code>&lt;em&gt;</code>が䜿えたす。
+        title: サヌバヌの説明
       site_description_extended:
-        desc_html: あなたのむンスタンスにおける行動芏範やルヌル、ガむドラむン、そのほかの蚘述をする際に最適な堎所です。HTMLタグが䜿えたす
+        desc_html: あなたのサヌバヌにおける行動芏範やルヌル、ガむドラむン、そのほかの蚘述をする際に最適な堎所です。HTMLタグが䜿えたす
         title: カスタム詳现説明
       site_short_description:
-        desc_html: サむドバヌず meta タグに衚瀺されたす。Mastodon ずは䜕か、そしおこのサヌバヌの特別な䜕かを1段萜で蚘述しおください。空欄の堎合、むンスタンスの説明が䜿甚されたす。
-        title: 短いむンスタンスの説明
+        desc_html: サむドバヌず meta タグに衚瀺されたす。Mastodon ずは䜕か、そしおこのサヌバヌの特別な䜕かを1段萜で蚘述しおください。空欄の堎合、サヌバヌの説明が䜿甚されたす。
+        title: 短いサヌバヌの説明
       site_terms:
         desc_html: あなたは独自のプラむバシヌポリシヌや利甚芏玄、そのほかの法的根拠を曞くこずができたす。HTMLタグが䜿えたす
         title: カスタム利甚芏玄
-      site_title: むンスタンスの名前
+      site_title: サヌバヌの名前
       thumbnail:
         desc_html: OpenGraphずAPIによるプレビュヌに䜿甚されたす。サむズは1200×630px掚奚です
-        title: むンスタンスのサムネむル
+        title: サヌバヌのサムネむル
       timeline_preview:
         desc_html: ランディングペヌゞに公開タむムラむンを衚瀺したす
         title: タむムラむンプレビュヌ
@@ -495,7 +495,7 @@ ja:
     warning: このデヌタは気を぀けお取り扱っおください。他の人ず共有しないでください
     your_token: アクセストヌクン
   auth:
-    agreement_html: 登録するをクリックするず <a href="%{rules_path}">むンスタンスのルヌル</a> ず <a href="%{terms_path}">プラむバシヌポリシヌ</a> に埓うこずに同意したこずになりたす。
+    agreement_html: 登録するをクリックするず <a href="%{rules_path}">サヌバヌのルヌル</a> ず <a href="%{terms_path}">プラむバシヌポリシヌ</a> に埓うこずに同意したこずになりたす。
     change_password: パスワヌド
     confirm_email: メヌルアドレスの確認
     delete_account: アカりントの削陀
@@ -513,7 +513,7 @@ ja:
       cas: CAS
       saml: SAML
     register: 登録する
-    register_elsewhere: 他のむンスタンスで新芏登録
+    register_elsewhere: 他のサヌバヌで新芏登録
     resend_confirmation: 確認メヌルを再送する
     reset_password: パスワヌドを再発行
     security: セキュリティ
@@ -549,7 +549,7 @@ ja:
     description_html: あなたのアカりントに含たれるコンテンツは党お削陀され、アカりントは無効化されたす。これは恒久的なもので、<strong>取り消すこずはできたせん</strong>。なりすたしを防ぐために、同じナヌザヌ名で再床登録するこずはできなくなりたす。
     proceed: アカりントを削陀する
     success_msg: アカりントは正垞に削陀されたした
-    warning_html: 削陀が保蚌されるのはこのむンスタンス䞊のコンテンツのみです。他のむンスタンス等、倖郚に広く共有されたコンテンツに぀いおは痕跡が残るこずがありたす。たた、珟圚接続できないサヌバヌや、あなたの曎新を受け取らなくなったサヌバヌに察しおは、削陀は反映されたせん。
+    warning_html: 削陀が保蚌されるのはこのサヌバヌ䞊のコンテンツのみです。他のサヌバヌ等、倖郚に広く共有されたコンテンツに぀いおは痕跡が残るこずがありたす。たた、珟圚接続できないサヌバヌや、あなたの曎新を受け取らなくなったサヌバヌに察しおは、削陀は反映されたせん。
     warning_title: 共有されたコンテンツに぀いお
   directories:
     directory: ディレクトリ
@@ -588,6 +588,10 @@ ja:
     lists: リスト
     mutes: ミュヌト
     storage: メディア
+  featured_tags:
+    add_new: 远加
+    errors:
+      limit: 泚目のハッシュタグの䞊限に達したした
   filters:
     contexts:
       home: ホヌムタむムラむン
@@ -606,7 +610,7 @@ ja:
       title: 新芏フィルタヌを远加
   followers:
     domain: ドメむン
-    explanation_html: あなたの投皿のプラむバシヌを確保したい堎合、誰があなたをフォロヌしおいるのかを把握しおいる必芁がありたす。 <strong>プラむベヌト投皿は、あなたのフォロワヌがいる党おのむンスタンスに配信されたす</strong>。 フォロワヌのむンスタンスの管理者や゜フトりェアがあなたのプラむバシヌを尊重しおくれるかどうか怪しい堎合は、そのフォロワヌを削陀した方がよいかもしれたせん。
+    explanation_html: あなたの投皿のプラむバシヌを確保したい堎合、誰があなたをフォロヌしおいるのかを把握しおいる必芁がありたす。 <strong>プラむベヌト投皿は、あなたのフォロワヌがいる党おのサヌバヌに配信されたす</strong>。 フォロワヌのサヌバヌの管理者や゜フトりェアがあなたのプラむバシヌを尊重しおくれるかどうか怪しい堎合は、そのフォロワヌを削陀した方がよいかもしれたせん。
     followers_count: フォロワヌ数
     lock_link: 承認制アカりントにする
     purge: フォロワヌから削陀する
@@ -629,10 +633,16 @@ ja:
       one: ゚ラヌが発生したした 以䞋の゚ラヌを確認しおください
       other: ゚ラヌが発生したした 以䞋の%{count}個の゚ラヌを確認しおください
   imports:
-    preface: 他のむンスタンスで゚クスポヌトされたファむルから、フォロヌ/ブロックした情報をこのむンスタンス䞊のアカりントにむンポヌトできたす。
+    modes:
+      merge: 統合
+      merge_long: 珟圚のレコヌドを保持したたた新しいものを远加したす
+      overwrite: 䞊曞き
+      overwrite_long: 珟圚のレコヌドを新しいもので眮き換えたす
+    preface: 他のサヌバヌで゚クスポヌトされたファむルから、フォロヌ/ブロックした情報をこのサヌバヌ䞊のアカりントにむンポヌトできたす。
     success: ファむルは正垞にアップロヌドされ、珟圚凊理䞭です。しばらくしおから確認しおください
     types:
       blocking: ブロックしたアカりントリスト
+      domain_blocking: 非衚瀺にしたドメむンリスト
       following: フォロヌ䞭のアカりントリスト
       muting: ミュヌトしたアカりントリスト
     upload: アップロヌド
@@ -654,7 +664,7 @@ ja:
       one: '1'
       other: "%{count}"
     max_uses_prompt: 無制限
-    prompt: リンクを生成・共有しおこのむンスタンスぞの新芏登録を受け付けるこずができたす
+    prompt: リンクを生成・共有しおこのサヌバヌぞの新芏登録を受け付けるこずができたす
     table:
       expires_at: 有効期限
       uses: 䜿甚
@@ -801,8 +811,9 @@ ja:
     development: 開発
     edit_profile: プロフィヌルを線集
     export: デヌタの゚クスポヌト
+    featured_tags: 泚目のハッシュタグ
     flavours: フレヌバヌ
-    followers: 信頌枈みのむンスタンス
+    followers: 信頌枈みのサヌバヌ
     import: デヌタのむンポヌト
     migrate: アカりントの匕っ越し
     notifications: 通知
@@ -980,13 +991,13 @@ ja:
       final_action: 始めたしょう
       final_step: 'さあ始めたしょう たずえフォロワヌがいなくおも、あなたの公開した投皿はロヌカルタむムラむンやハッシュタグなどで誰かの目に止たるかもしれたせん。自己玹介をしたい時は #introductions ハッシュタグを䜿うずいいかもしれたせん。'
       full_handle: あなたの正匏なナヌザヌ名
-      full_handle_hint: これは別のむンスタンスからフォロヌしおもらったりメッセヌゞのやり取りをする際に、友達に䌝えるずいいでしょう。
+      full_handle_hint: これは別のサヌバヌからフォロヌしおもらったりメッセヌゞのやり取りをする際に、友達に䌝えるずいいでしょう。
       review_preferences_action: 蚭定の倉曎
       review_preferences_step: 受け取りたいメヌルや投皿の公開範囲などの蚭定を必ず行っおください。䞍快でないならアニメヌション GIF の自動再生を有効にするこずもできたす。
       subject: Mastodon ぞようこそ
-      tip_federated_timeline: 連合タむムラむンは Mastodon ネットワヌクの流れを芋られるものです。ただしあなたず同じむンスタンスの人がフォロヌしおいる人だけが含たれるので、それが党おではありたせん。
-      tip_following: 暙準では自動でむンスタンスの管理者をフォロヌしおいたす。もっず興味のある人たちを芋぀けるには、ロヌカルタむムラむンず連合タむムラむンを確認しおください。
-      tip_local_timeline: ロヌカルタむムラむンは %{instance} にいる人々の流れを芋られるものです。圌らはあなたず同じむンスタンスにいる隣人のようなものです
+      tip_federated_timeline: 連合タむムラむンは Mastodon ネットワヌクの流れを芋られるものです。ただしあなたず同じサヌバヌの人がフォロヌしおいる人だけが含たれるので、それが党おではありたせん。
+      tip_following: 暙準では自動でサヌバヌの管理者をフォロヌしおいたす。もっず興味のある人たちを芋぀けるには、ロヌカルタむムラむンず連合タむムラむンを確認しおください。
+      tip_local_timeline: ロヌカルタむムラむンは %{instance} にいる人々の流れを芋られるものです。圌らはあなたず同じサヌバヌにいる隣人のようなものです
       tip_mobile_webapp: もしモバむル端末のブラりザで Mastodon をホヌム画面に远加できる堎合、プッシュ通知を受け取るこずができたす。それはたるでネむティブアプリのように動䜜したす
       tips: 豆知識
       title: ようこそ、%{name} 
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 23fa10179..1567ac626 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -602,6 +602,10 @@ pl:
     lists: Listy
     mutes: Wyciszeni
     storage: Urządzenie przechowujące dane
+  featured_tags:
+    add_new: Dodaj nowy
+    errors:
+      limit: JuÅŒ przekroczyłeś(-aś) maksymalną liczbę wyróŌnionych hashtagów
   filters:
     contexts:
       home: Strona główna
@@ -647,10 +651,16 @@ pl:
       one: Coś jest wciÄ…ÅŒ nie tak! Przyjrzyj się poniÅŒszemu błędowi
       other: Coś jest wciÄ…ÅŒ nie tak! Przejrzyj poniÅŒsze błędy (%{count})
   imports:
+    modes:
+      merge: Połącz
+      merge_long: Zachowaj obecne wpisy i dodaj nowe
+      overwrite: Nadpisz
+      overwrite_long: Zastąp obecne wpisy nowymi
     preface: MoÅŒesz zaimportować pewne dane (np. lista kont, które śledzisz lub blokujesz) do swojego konta na tym serwerze, korzystając z danych wyeksportowanych z innego serwera.
     success: Twoje dane zostały załadowane i zostaną niebawem przetworzone
     types:
       blocking: Lista blokowanych
+      domain_blocking: Lista zablokowanych domen
       following: Lista śledzonych
       muting: Lista wyciszonych
     upload: Załaduj
@@ -826,6 +836,7 @@ pl:
     development: Tworzenie aplikacji
     edit_profile: Edytuj profil
     export: Eksportowanie danych
+    featured_tags: WyróŌnione hashtagi
     flavours: Odmiany
     followers: Autoryzowani śledzący
     import: Importowanie danych
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 674abff63..ad9ae7417 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -37,8 +37,10 @@ en:
         setting_skin: Reskins the selected Mastodon flavour
         username: Your username will be unique on %{domain}
         whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word
+      featured_tag:
+        name: 'You might want to use one of these:'
       imports:
-        data: CSV file exported from another Mastodon instance
+        data: CSV file exported from another Mastodon server
       sessions:
         otp: 'Enter the two-factor code generated by your phone app or use one of your recovery codes:'
       user:
@@ -112,6 +114,8 @@ en:
         username: Username
         username_or_email: Username or Email
         whole_word: Whole word
+      featured_tag:
+        name: Hashtag
       interactions:
         must_be_follower: Block notifications from non-followers
         must_be_following: Block notifications from people you don't follow
diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml
index 9419331e4..2cade4301 100644
--- a/config/locales/simple_form.ja.yml
+++ b/config/locales/simple_form.ja.yml
@@ -33,11 +33,14 @@ ja:
         setting_display_media_show_all: 閲芧泚意ずしおマヌクされたメディアも垞に衚瀺する
         setting_hide_network: フォロヌずフォロワヌの情報がプロフィヌルペヌゞで芋られないようにしたす
         setting_noindex: 公開プロフィヌルおよび各投皿ペヌゞに圱響したす
+        setting_show_application: トゥヌトするのに䜿甚したアプリがトゥヌトの詳现ビュヌに衚瀺されるようになりたす
         setting_theme: ログむンしおいる党おのデバむスで適甚されるデザむンです。
         username: あなたのナヌザヌ名は %{domain} の䞭で重耇しおいない必芁がありたす
         whole_word: キヌワヌドたたはフレヌズが英数字のみの堎合、単語党䜓ず䞀臎する堎合のみ適甚されるようになりたす
+      featured_tag:
+        name: 'これらを䜿うずいいかもしれたせん:'
       imports:
-        data: 他の Mastodon むンスタンスから゚クスポヌトしたCSVファむルを遞択しお䞋さい
+        data: 他の Mastodon サヌバヌから゚クスポヌトしたCSVファむルを遞択しお䞋さい
       sessions:
         otp: '携垯電話のアプリで生成された二段階認蚌コヌドを入力するか、リカバリヌコヌドを䜿甚しおください:'
       user:
@@ -101,6 +104,7 @@ ja:
         setting_hide_network: 繋がりを隠す
         setting_noindex: 怜玢゚ンゞンによるむンデックスを拒吊する
         setting_reduce_motion: アニメヌションの動きを枛らす
+        setting_show_application: トゥヌトの送信に䜿甚したアプリを開瀺する
         setting_system_font_ui: システムのデフォルトフォントを䜿う
         setting_theme: サむトテヌマ
         setting_unfollow_modal: フォロヌを解陀する前に確認ダむアログを衚瀺する
@@ -109,6 +113,8 @@ ja:
         username: ナヌザヌ名
         username_or_email: ナヌザヌ名たたはメヌルアドレス
         whole_word: 単語党䜓にマッチ
+      featured_tag:
+        name: ハッシュタグ
       interactions:
         must_be_follower: フォロワヌ以倖からの通知をブロック
         must_be_following: フォロヌしおいないナヌザヌからの通知をブロック
diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml
index 660841e06..f5b5a6ca5 100644
--- a/config/locales/simple_form.pl.yml
+++ b/config/locales/simple_form.pl.yml
@@ -33,9 +33,12 @@ pl:
         setting_display_media_show_all: Zawsze pokazuj zawartość multimedialną jako wraÅŒliwą
         setting_hide_network: Informacje o tym, kto Cię śledzi i kogo śledzisz nie będą widoczne
         setting_noindex: Wpływa na widoczność strony profilu i Twoich wpisów
+        setting_show_application: W informacjach o wpisie będzie widoczna informacja o aplikacji, z której został wysłany
         setting_skin: Zmienia wygląd uÅŒywanej odmiany Mastodona
         username: Twoja nazwa uÅŒytkownika będzie niepowtarzalna na %{domain}
         whole_word: Jeśli słowo lub fraza składa się jedynie z liter lub cyfr, filtr będzie zastosowany tylko do pełnych wystąpień
+      featured_tag:
+        name: 'Sugerujemy uÅŒycie jednego z następujących:'
       imports:
         data: Plik CSV wyeksportowany z innej instancji Mastodona
       sessions:
@@ -102,6 +105,7 @@ pl:
         setting_hide_network: Ukryj swoją sieć
         setting_noindex: Nie indeksuj mojego profilu w wyszukiwarkach internetowych
         setting_reduce_motion: Ogranicz ruch w animacjach
+        setting_show_application: Informuj o aplikacji z której wysłano wpisy
         setting_skin: Motyw
         setting_system_font_ui: UÅŒywaj domyślnej czcionki systemu
         setting_unfollow_modal: Pytaj o potwierdzenie przed cofnięciem śledzenia
@@ -110,6 +114,8 @@ pl:
         username: Nazwa uÅŒytkownika
         username_or_email: Nazwa uÅŒytkownika lub adres e-mail
         whole_word: Całe słowo
+      featured_tag:
+        name: Hashtag
       interactions:
         must_be_follower: Nie wyświetlaj powiadomień od osób, które Cię nie śledzą
         must_be_following: Nie wyświetlaj powiadomień od osób, których nie śledzisz
diff --git a/config/navigation.rb b/config/navigation.rb
index 5b0b5c343..f74c98ab2 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -6,6 +6,7 @@ SimpleNavigation::Configuration.run do |navigation|
 
     primary.item :settings, safe_join([fa_icon('cog fw'), t('settings.settings')]), settings_profile_url do |settings|
       settings.item :profile, safe_join([fa_icon('user fw'), t('settings.edit_profile')]), settings_profile_url, highlights_on: %r{/settings/profile|/settings/migration}
+      settings.item :featured_tags, safe_join([fa_icon('hashtag fw'), t('settings.featured_tags')]), settings_featured_tags_url
       settings.item :preferences, safe_join([fa_icon('sliders fw'), t('settings.preferences')]), settings_preferences_url
       settings.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_notifications_url
       settings.item :password, safe_join([fa_icon('lock fw'), t('auth.security')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete}
diff --git a/config/routes.rb b/config/routes.rb
index 976b25812..447a22794 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -74,6 +74,7 @@ Rails.application.routes.draw do
   get '/@:username', to: 'accounts#show', as: :short_account
   get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies
   get '/@:username/media', to: 'accounts#show', as: :short_account_media
+  get '/@:username/tagged/:tag', to: 'accounts#show', as: :short_account_tag
   get '/@:account_username/:id', to: 'statuses#show', as: :short_account_status
   get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status
 
@@ -119,6 +120,7 @@ Rails.application.routes.draw do
     resource :migration, only: [:show, :update]
 
     resources :sessions, only: [:destroy]
+    resources :featured_tags, only: [:index, :create, :destroy]
   end
 
   resources :media, only: [:show] do
diff --git a/db/migrate/20171005102658_create_account_moderation_notes.rb b/db/migrate/20171005102658_create_account_moderation_notes.rb
index d1802b5b3..974ed9940 100644
--- a/db/migrate/20171005102658_create_account_moderation_notes.rb
+++ b/db/migrate/20171005102658_create_account_moderation_notes.rb
@@ -7,6 +7,7 @@ class CreateAccountModerationNotes < ActiveRecord::Migration[5.1]
 
       t.timestamps
     end
+
     add_foreign_key :account_moderation_notes, :accounts, column: :target_account_id
   end
 end
diff --git a/db/migrate/20190201012802_add_overwrite_to_imports.rb b/db/migrate/20190201012802_add_overwrite_to_imports.rb
new file mode 100644
index 000000000..89b262cc7
--- /dev/null
+++ b/db/migrate/20190201012802_add_overwrite_to_imports.rb
@@ -0,0 +1,17 @@
+require Rails.root.join('lib', 'mastodon', 'migration_helpers')
+
+class AddOverwriteToImports < ActiveRecord::Migration[5.2]
+  include Mastodon::MigrationHelpers
+
+  disable_ddl_transaction!
+
+  def up
+    safety_assured do
+      add_column_with_default :imports, :overwrite, :boolean, default: false, allow_null: false
+    end
+  end
+
+  def down
+    remove_column :imports, :overwrite, :boolean
+  end
+end
diff --git a/db/migrate/20190203180359_create_featured_tags.rb b/db/migrate/20190203180359_create_featured_tags.rb
new file mode 100644
index 000000000..b08410a3a
--- /dev/null
+++ b/db/migrate/20190203180359_create_featured_tags.rb
@@ -0,0 +1,12 @@
+class CreateFeaturedTags < ActiveRecord::Migration[5.2]
+  def change
+    create_table :featured_tags do |t|
+      t.references :account, foreign_key: { on_delete: :cascade }
+      t.references :tag, foreign_key: { on_delete: :cascade }
+      t.bigint :statuses_count, default: 0, null: false
+      t.datetime :last_status_at
+
+      t.timestamps
+    end
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c6c94609f..05d4deb1a 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: 2019_01_17_114553) do
+ActiveRecord::Schema.define(version: 2019_02_03_180359) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -260,6 +260,17 @@ ActiveRecord::Schema.define(version: 2019_01_17_114553) do
     t.index ["status_id"], name: "index_favourites_on_status_id"
   end
 
+  create_table "featured_tags", force: :cascade do |t|
+    t.bigint "account_id"
+    t.bigint "tag_id"
+    t.bigint "statuses_count", default: 0, null: false
+    t.datetime "last_status_at"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["account_id"], name: "index_featured_tags_on_account_id"
+    t.index ["tag_id"], name: "index_featured_tags_on_tag_id"
+  end
+
   create_table "follow_requests", force: :cascade do |t|
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
@@ -300,6 +311,7 @@ ActiveRecord::Schema.define(version: 2019_01_17_114553) do
     t.integer "data_file_size"
     t.datetime "data_updated_at"
     t.bigint "account_id", null: false
+    t.boolean "overwrite", default: false, null: false
   end
 
   create_table "invites", force: :cascade do |t|
@@ -721,6 +733,8 @@ ActiveRecord::Schema.define(version: 2019_01_17_114553) do
   add_foreign_key "custom_filters", "accounts", on_delete: :cascade
   add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade
   add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade
+  add_foreign_key "featured_tags", "accounts", on_delete: :cascade
+  add_foreign_key "featured_tags", "tags", on_delete: :cascade
   add_foreign_key "follow_requests", "accounts", column: "target_account_id", name: "fk_9291ec025d", on_delete: :cascade
   add_foreign_key "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade
   add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade
diff --git a/spec/fabricators/featured_tag_fabricator.rb b/spec/fabricators/featured_tag_fabricator.rb
new file mode 100644
index 000000000..25cbdaac0
--- /dev/null
+++ b/spec/fabricators/featured_tag_fabricator.rb
@@ -0,0 +1,6 @@
+Fabricator(:featured_tag) do
+  account
+  tag
+  statuses_count 1_337
+  last_status_at Time.now.utc
+end
diff --git a/spec/lib/formatter_spec.rb b/spec/lib/formatter_spec.rb
index 0c1efe7c3..96d2fc7e0 100644
--- a/spec/lib/formatter_spec.rb
+++ b/spec/lib/formatter_spec.rb
@@ -74,10 +74,36 @@ RSpec.describe Formatter do
     end
 
     context 'given a URL with a query string' do
-      let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' }
+      context 'with escaped unicode character' do
+        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&q=autolink' }
 
-      it 'matches the full URL' do
-        is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&amp;q=autolink"'
+        it 'matches the full URL' do
+          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&amp;q=autolink"'
+        end
+      end
+
+      context 'with unicode character' do
+        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓&q=autolink' }
+
+        it 'matches the full URL' do
+          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=✓&amp;q=autolink"'
+        end
+      end
+
+      context 'with unicode character at the end' do
+        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=✓' }
+
+        it 'matches the full URL' do
+          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=✓"'
+        end
+      end
+
+      context 'with escaped and not escaped unicode characters' do
+        let(:text) { 'https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&utf81=✓&q=autolink' }
+
+        it 'preserves escaped unicode characters' do
+          is_expected.to include 'href="https://www.ruby-toolbox.com/search?utf8=%E2%9C%93&amp;utf81=✓&amp;q=autolink"'
+        end
       end
     end
 
@@ -89,6 +115,22 @@ RSpec.describe Formatter do
       end
     end
 
+    context 'given a URL in quotation marks' do
+      let(:text) { '"https://example.com/"' }
+
+      it 'does not match the quotation marks' do
+        is_expected.to include 'href="https://example.com/"'
+      end
+    end
+
+    context 'given a URL in angle brackets' do
+      let(:text) { '<https://example.com/>' }
+
+      it 'does not match the angle brackets' do
+        is_expected.to include 'href="https://example.com/"'
+      end
+    end
+
     context 'given a URL with Japanese path string' do
       let(:text) { 'https://ja.wikipedia.org/wiki/日本' }
 
@@ -105,6 +147,22 @@ RSpec.describe Formatter do
       end
     end
 
+    context 'given a URL with a full-width space' do
+      let(:text) { 'https://example.com/ abc123' }
+
+      it 'does not match the full-width space' do
+        is_expected.to include 'href="https://example.com/"'
+      end
+    end
+
+    context 'given a URL in Japanese quotation marks' do
+      let(:text) { '「[https://example.org/」' }
+
+      it 'does not match the quotation marks' do
+        is_expected.to include 'href="https://example.org/"'
+      end
+    end
+
     context 'given a URL with Simplified Chinese path string' do
       let(:text) { 'https://baike.baidu.com/item/䞭华人民共和囜' }
 
@@ -124,7 +182,11 @@ RSpec.describe Formatter do
     context 'given a URL containing unsafe code (XSS attack, visible part)' do
       let(:text) { %q{http://example.com/b<del>b</del>} }
 
-      it 'escapes the HTML in the URL' do
+      it 'does not include the HTML in the URL' do
+        is_expected.to include '"http://example.com/b"'
+      end
+
+      it 'escapes the HTML' do
         is_expected.to include '&lt;del&gt;b&lt;/del&gt;'
       end
     end
@@ -132,7 +194,11 @@ RSpec.describe Formatter do
     context 'given a URL containing unsafe code (XSS attack, invisible part)' do
       let(:text) { %q{http://example.com/blahblahblahblah/a<script>alert("Hello")</script>} }
 
-      it 'escapes the HTML in the URL' do
+      it 'does not include the HTML in the URL' do
+        is_expected.to include '"http://example.com/blahblahblahblah/a"'
+      end
+
+      it 'escapes the HTML' do
         is_expected.to include '&lt;script&gt;alert(&quot;Hello&quot;)&lt;/script&gt;'
       end
     end
@@ -168,6 +234,14 @@ RSpec.describe Formatter do
         is_expected.to include '/tags/hashtag" class="mention hashtag" rel="tag">#<span>hashtag</span></a>'
       end
     end
+
+    context 'given text containing a hashtag with Unicode chars' do
+      let(:text)  { '#hashtagタグ' }
+
+      it 'creates a hashtag link' do
+        is_expected.to include '/tags/hashtag%E3%82%BF%E3%82%B0" class="mention hashtag" rel="tag">#<span>hashtagタグ</span></a>'
+      end
+    end
   end
 
   describe '#format_spoiler' do
diff --git a/spec/models/concerns/account_interactions_spec.rb b/spec/models/concerns/account_interactions_spec.rb
index 9c9b87daf..36e346f14 100644
--- a/spec/models/concerns/account_interactions_spec.rb
+++ b/spec/models/concerns/account_interactions_spec.rb
@@ -244,9 +244,9 @@ describe AccountInteractions do
   end
 
   describe '#block_domain!' do
-    let(:domain_block) { Fabricate(:domain_block) }
+    let(:domain) { 'example.com' }
 
-    subject { account.block_domain!(domain_block) }
+    subject { account.block_domain!(domain) }
 
     it 'creates and returns AccountDomainBlock' do
       expect do
diff --git a/spec/models/featured_tag_spec.rb b/spec/models/featured_tag_spec.rb
new file mode 100644
index 000000000..07533e0b9
--- /dev/null
+++ b/spec/models/featured_tag_spec.rb
@@ -0,0 +1,4 @@
+require 'rails_helper'
+
+RSpec.describe FeaturedTag, type: :model do
+end