about summary refs log tree commit diff
diff options
context:
space:
mode:
authorThibaut Girka <thib@sitedethib.com>2020-11-07 18:19:34 +0100
committerThibaut Girka <thib@sitedethib.com>2020-11-07 18:23:02 +0100
commit412218af2ecd4cd004708d781e574208742dea4c (patch)
treec0bf823109c7481ec9242a0263e091ec4aaeafeb
parent5a9fc749c3eab8d3c93dd282fa89c20a5cb0e994 (diff)
parentee8cf246cfe8e05914ad7dcf81596f8535b3e161 (diff)
Merge branch 'master' into glitch-soc/merge-upstream
-rw-r--r--Gemfile.lock6
-rw-r--r--app/controllers/admin/accounts_controller.rb7
-rw-r--r--app/controllers/admin/announcements_controller.rb2
-rw-r--r--app/controllers/api/v1/admin/accounts_controller.rb8
-rw-r--r--app/helpers/statuses_helper.rb16
-rw-r--r--app/javascript/mastodon/components/column.js6
-rw-r--r--app/javascript/mastodon/components/dropdown_menu.js4
-rw-r--r--app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js4
-rw-r--r--app/javascript/mastodon/features/compose/components/privacy_dropdown.js4
-rw-r--r--app/javascript/mastodon/features/ui/components/columns_area.js6
-rw-r--r--app/javascript/mastodon/features/ui/components/zoomable_image.js69
-rw-r--r--app/javascript/mastodon/is_mobile.js4
-rw-r--r--app/lib/activitypub/activity/create.rb2
-rw-r--r--app/models/account.rb14
-rw-r--r--app/models/account_warning.rb2
-rw-r--r--app/models/admin/account_action.rb9
-rw-r--r--app/models/admin/action_log_filter.rb2
-rw-r--r--app/models/announcement.rb1
-rw-r--r--app/policies/account_policy.rb8
-rw-r--r--app/serializers/activitypub/note_serializer.rb4
-rw-r--r--app/serializers/rest/status_serializer.rb8
-rw-r--r--app/services/suspend_account_service.rb10
-rw-r--r--app/services/unsuspend_account_service.rb10
-rw-r--r--app/views/accounts/show.html.haml4
-rw-r--r--app/views/admin/accounts/show.html.haml7
-rw-r--r--app/views/home/index.html.haml8
-rw-r--r--app/views/layouts/_theme.html.haml6
-rwxr-xr-xapp/views/layouts/application.html.haml6
-rw-r--r--app/views/layouts/embedded.html.haml6
-rw-r--r--app/views/layouts/error.html.haml2
-rw-r--r--app/views/media/player.html.haml6
-rw-r--r--app/views/statuses/_detailed_status.html.haml6
-rw-r--r--app/views/statuses/_simple_status.html.haml6
-rw-r--r--app/views/statuses/_status.html.haml8
-rw-r--r--config/application.rb2
-rw-r--r--config/initializers/devise.rb2
-rw-r--r--config/initializers/makara.rb2
-rw-r--r--config/initializers/session_store.rb6
-rw-r--r--config/locales/en.yml12
-rw-r--r--config/locales/ja.yml10
-rw-r--r--config/locales/simple_form.en.yml1
-rw-r--r--config/locales/simple_form.ja.yml1
-rw-r--r--config/routes.rb2
-rw-r--r--config/webpack/development.js2
-rw-r--r--config/webpack/production.js2
-rw-r--r--config/webpack/shared.js3
-rw-r--r--config/webpack/tests.js2
-rw-r--r--db/migrate/20200614002136_add_sensitized_to_accounts.rb5
-rw-r--r--db/schema.rb1
-rw-r--r--lib/webpacker/helper_extensions.rb20
-rw-r--r--lib/webpacker/manifest_extensions.rb17
-rw-r--r--package.json20
-rw-r--r--spec/controllers/api/v1/admin/accounts_controller_spec.rb18
-rw-r--r--spec/models/admin/account_action_spec.rb8
-rw-r--r--spec/policies/account_policy_spec.rb2
-rw-r--r--yarn.lock164
56 files changed, 403 insertions, 170 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index a990db227..71100a255 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -104,7 +104,7 @@ GEM
       debug_inspector (>= 0.0.1)
     blurhash (0.1.4)
       ffi (~> 1.10.0)
-    bootsnap (1.4.8)
+    bootsnap (1.4.9)
       msgpack (~> 1.0)
     brakeman (4.10.0)
     browser (4.2.0)
@@ -424,7 +424,7 @@ GEM
     pry-rails (0.3.9)
       pry (>= 0.10.4)
     public_suffix (4.0.6)
-    puma (5.0.2)
+    puma (5.0.4)
       nio4r (~> 2.0)
     pundit (2.1.0)
       activesupport (>= 3.0.0)
@@ -574,7 +574,7 @@ GEM
       sidekiq (>= 3)
       thwait
       tilt (>= 1.4.0)
-    sidekiq-unique-jobs (6.0.24)
+    sidekiq-unique-jobs (6.0.25)
       concurrent-ruby (~> 1.0, >= 1.0.5)
       sidekiq (>= 4.0, < 7.0)
       thor (>= 0.20, < 2.0)
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index b9b75727d..1dd7430e0 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -53,6 +53,13 @@ module Admin
       redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.destroyed_msg', username: @account.acct)
     end
 
+    def unsensitive
+      authorize @account, :unsensitive?
+      @account.unsensitize!
+      log_action :unsensitive, @account
+      redirect_to admin_account_path(@account.id)
+    end
+
     def unsilence
       authorize @account, :unsilence?
       @account.unsilence!
diff --git a/app/controllers/admin/announcements_controller.rb b/app/controllers/admin/announcements_controller.rb
index 494fd13d0..351b9a991 100644
--- a/app/controllers/admin/announcements_controller.rb
+++ b/app/controllers/admin/announcements_controller.rb
@@ -71,7 +71,7 @@ class Admin::AnnouncementsController < Admin::BaseController
   private
 
   def set_announcements
-    @announcements = AnnouncementFilter.new(filter_params).results.page(params[:page])
+    @announcements = AnnouncementFilter.new(filter_params).results.reverse_chronological.page(params[:page])
   end
 
   def set_announcement
diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb
index 3af572f25..63cc521ed 100644
--- a/app/controllers/api/v1/admin/accounts_controller.rb
+++ b/app/controllers/api/v1/admin/accounts_controller.rb
@@ -22,6 +22,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController
     active
     pending
     disabled
+    sensitized
     silenced
     suspended
     username
@@ -68,6 +69,13 @@ class Api::V1::Admin::AccountsController < Api::BaseController
     render json: @account, serializer: REST::Admin::AccountSerializer
   end
 
+  def unsensitive
+    authorize @account, :unsensitive?
+    @account.unsensitize!
+    log_action :unsensitive, @account
+    render json: @account, serializer: REST::Admin::AccountSerializer
+  end
+
   def unsilence
     authorize @account, :unsilence?
     @account.unsilence!
diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb
index a51597cf3..daed9048f 100644
--- a/app/helpers/statuses_helper.rb
+++ b/app/helpers/statuses_helper.rb
@@ -4,8 +4,12 @@ module StatusesHelper
   EMBEDDED_CONTROLLER = 'statuses'
   EMBEDDED_ACTION = 'embed'
 
-  def link_to_more(url)
-    link_to t('statuses.show_more'), url, class: 'load-more load-gap'
+  def link_to_newer(url)
+    link_to t('statuses.show_newer'), url, class: 'load-more load-gap'
+  end
+
+  def link_to_older(url)
+    link_to t('statuses.show_older'), url, class: 'load-more load-gap'
   end
 
   def nothing_here(extra_classes = '')
@@ -117,6 +121,14 @@ module StatusesHelper
     end
   end
 
+  def sensitized?(status, account)
+    if !account.nil? && account.id == status.account_id
+      status.sensitive
+    else
+      status.account.sensitized? || status.sensitive
+    end
+  end
+
   private
 
   def simplified_text(text)
diff --git a/app/javascript/mastodon/components/column.js b/app/javascript/mastodon/components/column.js
index 55e3bfd5e..239824a4f 100644
--- a/app/javascript/mastodon/components/column.js
+++ b/app/javascript/mastodon/components/column.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import detectPassiveEvents from 'detect-passive-events';
+import { supportsPassiveEvents } from 'detect-passive-events';
 import { scrollTop } from '../scroll';
 
 export default class Column extends React.PureComponent {
@@ -35,9 +35,9 @@ export default class Column extends React.PureComponent {
 
   componentDidMount () {
     if (this.props.bindToDocument) {
-      document.addEventListener('wheel', this.handleWheel,  detectPassiveEvents.hasSupport ? { passive: true } : false);
+      document.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
     } else {
-      this.node.addEventListener('wheel', this.handleWheel,  detectPassiveEvents.hasSupport ? { passive: true } : false);
+      this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
     }
   }
 
diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js
index 09e3c9df8..c6b4b1187 100644
--- a/app/javascript/mastodon/components/dropdown_menu.js
+++ b/app/javascript/mastodon/components/dropdown_menu.js
@@ -5,9 +5,9 @@ import IconButton from './icon_button';
 import Overlay from 'react-overlays/lib/Overlay';
 import Motion from '../features/ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
-import detectPassiveEvents from 'detect-passive-events';
+import { supportsPassiveEvents } from 'detect-passive-events';
 
-const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
+const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
 let id = 0;
 
 class DropdownMenu extends React.PureComponent {
diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
index bac136e5e..dc4f48060 100644
--- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
@@ -5,7 +5,7 @@ import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'
 import Overlay from 'react-overlays/lib/Overlay';
 import classNames from 'classnames';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import detectPassiveEvents from 'detect-passive-events';
+import { supportsPassiveEvents } from 'detect-passive-events';
 import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
 import { assetHost } from 'mastodon/utils/config';
 
@@ -29,7 +29,7 @@ const messages = defineMessages({
 let EmojiPicker, Emoji; // load asynchronously
 
 const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`;
-const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
+const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
 
 class ModifierPickerMenu extends React.PureComponent {
 
diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
index 5223025fb..309f46290 100644
--- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
@@ -5,7 +5,7 @@ import IconButton from '../../../components/icon_button';
 import Overlay from 'react-overlays/lib/Overlay';
 import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
-import detectPassiveEvents from 'detect-passive-events';
+import { supportsPassiveEvents } from 'detect-passive-events';
 import classNames from 'classnames';
 import Icon from 'mastodon/components/icon';
 
@@ -21,7 +21,7 @@ const messages = defineMessages({
   change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
 });
 
-const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
+const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
 
 class PrivacyDropdownMenu extends React.PureComponent {
 
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index ecc0b8f0b..36a84fcbf 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -31,7 +31,7 @@ import Icon from 'mastodon/components/icon';
 import ComposePanel from './compose_panel';
 import NavigationPanel from './navigation_panel';
 
-import detectPassiveEvents from 'detect-passive-events';
+import { supportsPassiveEvents } from 'detect-passive-events';
 import { scrollRight } from '../../../scroll';
 
 const componentMap = {
@@ -80,7 +80,7 @@ class ColumnsArea extends ImmutablePureComponent {
 
   componentDidMount() {
     if (!this.props.singleColumn) {
-      this.node.addEventListener('wheel', this.handleWheel,  detectPassiveEvents.hasSupport ? { passive: true } : false);
+      this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
     }
 
     this.lastIndex   = getIndex(this.context.router.history.location.pathname);
@@ -97,7 +97,7 @@ class ColumnsArea extends ImmutablePureComponent {
 
   componentDidUpdate(prevProps) {
     if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
-      this.node.addEventListener('wheel', this.handleWheel,  detectPassiveEvents.hasSupport ? { passive: true } : false);
+      this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
     }
     this.lastIndex = getIndex(this.context.router.history.location.pathname);
     this.setState({ shouldAnimate: true });
diff --git a/app/javascript/mastodon/features/ui/components/zoomable_image.js b/app/javascript/mastodon/features/ui/components/zoomable_image.js
index 402196727..1cf263cb9 100644
--- a/app/javascript/mastodon/features/ui/components/zoomable_image.js
+++ b/app/javascript/mastodon/features/ui/components/zoomable_image.js
@@ -113,7 +113,8 @@ class ZoomableImage extends React.PureComponent {
   state = {
     scale: MIN_SCALE,
     zoomMatrix: {
-      type: null, // 'full-width' 'full-height'
+      type: null, // 'width' 'height'
+      fullScreen: null, // bool
       rate: null, // full screen scale rate
       clientWidth: null,
       clientHeight: null,
@@ -122,12 +123,15 @@ class ZoomableImage extends React.PureComponent {
       clientHeightFixed: null,
       scrollTop: null,
       scrollLeft: null,
+      translateX: null,
+      translateY: null,
     },
     zoomState: 'expand', // 'expand' 'compress'
     navigationHidden: false,
     dragPosition: { top: 0, left: 0, x: 0, y: 0 },
     dragged: false,
     lockScroll: { x: 0, y: 0 },
+    lockTranslate: { x: 0, y: 0 },
   }
 
   removers = [];
@@ -168,18 +172,24 @@ class ZoomableImage extends React.PureComponent {
   }
 
   componentDidUpdate () {
+    this.setState({ zoomState: this.state.scale >= this.state.zoomMatrix.rate ? 'compress' : 'expand' });
+
+    if (this.state.scale === MIN_SCALE) {
+      this.container.style.removeProperty('cursor');
+    }
+  }
+
+  UNSAFE_componentWillReceiveProps () {
+    // reset when slide to next image
     if (this.props.zoomButtonHidden) {
-      this.setState({ scale: MIN_SCALE }, () => {
+      this.setState({
+        scale: MIN_SCALE,
+        lockTranslate: { x: 0, y: 0 },
+      }, () => {
         this.container.scrollLeft = 0;
         this.container.scrollTop = 0;
       });
     }
-
-    this.setState({ zoomState: this.state.scale >= this.state.zoomMatrix.rate ? 'compress' : 'expand' });
-
-    if (this.state.scale === 1) {
-      this.container.style.removeProperty('cursor');
-    }
   }
 
   removeEventListeners () {
@@ -192,7 +202,7 @@ class ZoomableImage extends React.PureComponent {
 
     const event = normalizeWheel(e);
 
-    if (this.state.zoomMatrix.type === 'full-width') {
+    if (this.state.zoomMatrix.type === 'width') {
       // full width, scroll vertical
       this.container.scrollTop = Math.max(this.container.scrollTop + event.pixelY, this.state.lockScroll.y);
     } else {
@@ -268,7 +278,7 @@ class ZoomableImage extends React.PureComponent {
   }
 
   zoom(nextScale, midpoint) {
-    const { scale } = this.state;
+    const { scale, zoomMatrix } = this.state;
     const { scrollLeft, scrollTop } = this.container;
 
     // math memo:
@@ -283,6 +293,15 @@ class ZoomableImage extends React.PureComponent {
     this.setState({ scale: nextScale }, () => {
       this.container.scrollLeft = nextScrollLeft;
       this.container.scrollTop = nextScrollTop;
+      // reset the translateX/Y constantly
+      if (nextScale < zoomMatrix.rate) {
+        this.setState({
+          lockTranslate: {
+            x: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateX * ((nextScale - MIN_SCALE) / (zoomMatrix.rate - MIN_SCALE)),
+            y: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateY * ((nextScale - MIN_SCALE) / (zoomMatrix.rate - MIN_SCALE)),
+          },
+        });
+      }
     });
   }
 
@@ -307,14 +326,18 @@ class ZoomableImage extends React.PureComponent {
     const { offsetWidth, offsetHeight } = this.image;
     const clientHeightFixed = clientHeight - NAV_BAR_HEIGHT;
 
-    const type = width/height < clientWidth / clientHeightFixed ? 'full-width' : 'full-height';
-    const rate = type === 'full-width' ? clientWidth / offsetWidth : clientHeightFixed / offsetHeight;
-    const scrollTop = type === 'full-width' ?  (clientHeight - offsetHeight) / 2 - NAV_BAR_HEIGHT : (clientHeightFixed - offsetHeight) / 2;
+    const type = width / height < clientWidth / clientHeightFixed ? 'width' : 'height';
+    const fullScreen = type === 'width' ?  width > clientWidth : height > clientHeightFixed;
+    const rate = type === 'width' ? Math.min(clientWidth, width) / offsetWidth : Math.min(clientHeightFixed, height) / offsetHeight;
+    const scrollTop = type === 'width' ?  (clientHeight - offsetHeight) / 2 - NAV_BAR_HEIGHT : (clientHeightFixed - offsetHeight) / 2;
     const scrollLeft = (clientWidth - offsetWidth) / 2;
+    const translateX = type === 'width' ? (width - offsetWidth) / (2 * rate) : 0;
+    const translateY = type === 'height' ? (height - offsetHeight) / (2 * rate) : 0;
 
     this.setState({
       zoomMatrix: {
         type: type,
+        fullScreen: fullScreen,
         rate: rate,
         clientWidth: clientWidth,
         clientHeight: clientHeight,
@@ -323,6 +346,8 @@ class ZoomableImage extends React.PureComponent {
         clientHeightFixed: clientHeightFixed,
         scrollTop: scrollTop,
         scrollLeft: scrollLeft,
+        translateX: translateX,
+        translateY: translateY,
       },
     });
   }
@@ -340,6 +365,10 @@ class ZoomableImage extends React.PureComponent {
           x: 0,
           y: 0,
         },
+        lockTranslate: {
+          x: 0,
+          y: 0,
+        },
       }, () => {
         this.container.scrollLeft = 0;
         this.container.scrollTop = 0;
@@ -351,6 +380,10 @@ class ZoomableImage extends React.PureComponent {
           x: zoomMatrix.scrollLeft,
           y: zoomMatrix.scrollTop,
         },
+        lockTranslate: {
+          x: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateX,
+          y: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateY,
+        },
       }, () => {
         this.container.scrollLeft = zoomMatrix.scrollLeft;
         this.container.scrollTop = zoomMatrix.scrollTop;
@@ -371,15 +404,15 @@ class ZoomableImage extends React.PureComponent {
 
   render () {
     const { alt, src, width, height, intl } = this.props;
-    const { scale } = this.state;
-    const overflow = scale === 1 ? 'hidden' : 'scroll';
-    const zoomButtonSshouldHide = !this.state.navigationHidden && !this.props.zoomButtonHidden ? '' : 'media-modal__zoom-button--hidden';
+    const { scale, lockTranslate } = this.state;
+    const overflow = scale === MIN_SCALE ? 'hidden' : 'scroll';
+    const zoomButtonShouldHide = this.state.navigationHidden || this.props.zoomButtonHidden || this.state.zoomMatrix.rate <= MIN_SCALE ? 'media-modal__zoom-button--hidden' : '';
     const zoomButtonTitle = this.state.zoomState === 'compress' ? intl.formatMessage(messages.compress) : intl.formatMessage(messages.expand);
 
     return (
       <React.Fragment>
         <IconButton
-          className={`media-modal__zoom-button ${zoomButtonSshouldHide}`}
+          className={`media-modal__zoom-button ${zoomButtonShouldHide}`}
           title={zoomButtonTitle}
           icon={this.state.zoomState}
           onClick={this.handleZoomClick}
@@ -402,7 +435,7 @@ class ZoomableImage extends React.PureComponent {
             width={width}
             height={height}
             style={{
-              transform: `scale(${scale})`,
+              transform: `scale(${scale}) translate(-${lockTranslate.x}px, -${lockTranslate.y}px)`,
               transformOrigin: '0 0',
             }}
             draggable={false}
diff --git a/app/javascript/mastodon/is_mobile.js b/app/javascript/mastodon/is_mobile.js
index f96df1ebb..5a8c3db08 100644
--- a/app/javascript/mastodon/is_mobile.js
+++ b/app/javascript/mastodon/is_mobile.js
@@ -1,4 +1,4 @@
-import detectPassiveEvents from 'detect-passive-events';
+import { supportsPassiveEvents } from 'detect-passive-events';
 
 const LAYOUT_BREAKPOINT = 630;
 
@@ -9,7 +9,7 @@ export function isMobile(width) {
 const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
 
 let userTouching = false;
-let listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
+let listenerOptions = supportsPassiveEvents ? { passive: true } : false;
 
 function touchListener() {
   userTouching = true;
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index 3a9f83978..d56d47a2d 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -111,7 +111,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
         created_at: @object['published'],
         override_timestamps: @options[:override_timestamps],
         reply: @object['inReplyTo'].present?,
-        sensitive: @object['sensitive'] || false,
+        sensitive: @account.sensitized? || @object['sensitive'] || false,
         visibility: visibility_from_audience,
         thread: replied_to_status,
         conversation: conversation_from_uri(@object['conversation']),
diff --git a/app/models/account.rb b/app/models/account.rb
index 38f235baa..14df68058 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -50,6 +50,7 @@
 #  avatar_storage_schema_version :integer
 #  header_storage_schema_version :integer
 #  devices_url                   :string
+#  sensitized_at                 :datetime
 #
 
 class Account < ApplicationRecord
@@ -96,6 +97,7 @@ class Account < ApplicationRecord
   scope :partitioned, -> { order(Arel.sql('row_number() over (partition by domain)')) }
   scope :silenced, -> { where.not(silenced_at: nil) }
   scope :suspended, -> { where.not(suspended_at: nil) }
+  scope :sensitized, -> { where.not(sensitized_at: nil) }
   scope :without_suspended, -> { where(suspended_at: nil) }
   scope :without_silenced, -> { where(silenced_at: nil) }
   scope :recent, -> { reorder(id: :desc) }
@@ -238,6 +240,18 @@ class Account < ApplicationRecord
     end
   end
 
+  def sensitized?
+    sensitized_at.present?
+  end
+
+  def sensitize!(date = Time.now.utc)
+    update!(sensitized_at: date)
+  end
+
+  def unsensitize!
+    update!(sensitized_at: nil)
+  end
+
   def memorialize!
     update!(memorial: true)
   end
diff --git a/app/models/account_warning.rb b/app/models/account_warning.rb
index 157e6c04d..5efc924d5 100644
--- a/app/models/account_warning.rb
+++ b/app/models/account_warning.rb
@@ -13,7 +13,7 @@
 #
 
 class AccountWarning < ApplicationRecord
-  enum action: %i(none disable silence suspend), _suffix: :action
+  enum action: %i(none disable sensitive silence suspend), _suffix: :action
 
   belongs_to :account, inverse_of: :account_warnings
   belongs_to :target_account, class_name: 'Account', inverse_of: :targeted_account_warnings
diff --git a/app/models/admin/account_action.rb b/app/models/admin/account_action.rb
index c4ac09520..11ce737f3 100644
--- a/app/models/admin/account_action.rb
+++ b/app/models/admin/account_action.rb
@@ -8,6 +8,7 @@ class Admin::AccountAction
   TYPES = %w(
     none
     disable
+    sensitive
     silence
     suspend
   ).freeze
@@ -64,6 +65,8 @@ class Admin::AccountAction
     case type
     when 'disable'
       handle_disable!
+    when 'sensitive'
+      handle_sensitive!
     when 'silence'
       handle_silence!
     when 'suspend'
@@ -109,6 +112,12 @@ class Admin::AccountAction
     target_account.user&.disable!
   end
 
+  def handle_sensitive!
+    authorize(target_account, :sensitive?)
+    log_action(:sensitive, target_account)
+    target_account.sensitize!
+  end
+
   def handle_silence!
     authorize(target_account, :silence?)
     log_action(:silence, target_account)
diff --git a/app/models/admin/action_log_filter.rb b/app/models/admin/action_log_filter.rb
index 0ba7e1609..3a1b67e06 100644
--- a/app/models/admin/action_log_filter.rb
+++ b/app/models/admin/action_log_filter.rb
@@ -35,9 +35,11 @@ class Admin::ActionLogFilter
     reopen_report: { target_type: 'Report', action: 'reopen' }.freeze,
     reset_password_user: { target_type: 'User', action: 'reset_password' }.freeze,
     resolve_report: { target_type: 'Report', action: 'resolve' }.freeze,
+    sensitive_account: { target_type: 'Account', action: 'sensitive' }.freeze,
     silence_account: { target_type: 'Account', action: 'silence' }.freeze,
     suspend_account: { target_type: 'Account', action: 'suspend' }.freeze,
     unassigned_report: { target_type: 'Report', action: 'unassigned' }.freeze,
+    unsensitive_account: { target_type: 'Account', action: 'unsensitive' }.freeze,
     unsilence_account: { target_type: 'Account', action: 'unsilence' }.freeze,
     unsuspend_account: { target_type: 'Account', action: 'unsuspend' }.freeze,
     update_announcement: { target_type: 'Announcement', action: 'update' }.freeze,
diff --git a/app/models/announcement.rb b/app/models/announcement.rb
index c493604c2..f8183aabc 100644
--- a/app/models/announcement.rb
+++ b/app/models/announcement.rb
@@ -22,6 +22,7 @@ class Announcement < ApplicationRecord
   scope :published, -> { where(published: true) }
   scope :without_muted, ->(account) { joins("LEFT OUTER JOIN announcement_mutes ON announcement_mutes.announcement_id = announcements.id AND announcement_mutes.account_id = #{account.id}").where('announcement_mutes.id IS NULL') }
   scope :chronological, -> { order(Arel.sql('COALESCE(announcements.starts_at, announcements.scheduled_at, announcements.published_at, announcements.created_at) ASC')) }
+  scope :reverse_chronological, -> { order(Arel.sql('COALESCE(announcements.starts_at, announcements.scheduled_at, announcements.published_at, announcements.created_at) DESC')) }
 
   has_many :announcement_mutes, dependent: :destroy
   has_many :announcement_reactions, dependent: :destroy
diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb
index 1b105e92a..679119075 100644
--- a/app/policies/account_policy.rb
+++ b/app/policies/account_policy.rb
@@ -25,6 +25,14 @@ class AccountPolicy < ApplicationPolicy
     staff?
   end
 
+  def sensitive?
+    staff? && !record.user&.staff?
+  end
+
+  def unsensitive?
+    staff?
+  end
+
   def silence?
     staff? && !record.user&.staff?
   end
diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb
index a0965790e..4ac699ddf 100644
--- a/app/serializers/activitypub/note_serializer.rb
+++ b/app/serializers/activitypub/note_serializer.rb
@@ -110,6 +110,10 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
     ActivityPub::TagManager.instance.cc(object)
   end
 
+  def sensitive
+    object.account.sensitized? || object.sensitive
+  end
+
   def virtual_tags
     object.active_mentions.to_a.sort_by(&:id) + object.tags + object.emojis
   end
diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb
index 58e7bd4e4..b5dcf6208 100644
--- a/app/serializers/rest/status_serializer.rb
+++ b/app/serializers/rest/status_serializer.rb
@@ -60,6 +60,14 @@ class REST::StatusSerializer < ActiveModel::Serializer
     end
   end
 
+  def sensitive
+    if current_user? && current_user.account_id == object.account_id
+      object.sensitive
+    else
+      object.account.sensitized? || object.sensitive
+    end
+  end
+
   def uri
     ActivityPub::TagManager.instance.uri_for(object)
   end
diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb
index 5a079c3ac..f08c41e17 100644
--- a/app/services/suspend_account_service.rb
+++ b/app/services/suspend_account_service.rb
@@ -18,7 +18,7 @@ class SuspendAccountService < BaseService
 
   def unmerge_from_home_timelines!
     @account.followers_for_local_distribution.find_each do |follower|
-      FeedManager.instance.unmerge_from_timeline(@account, follower)
+      FeedManager.instance.unmerge_from_home(@account, follower)
     end
   end
 
@@ -39,11 +39,15 @@ class SuspendAccountService < BaseService
         styles.each do |style|
           case Paperclip::Attachment.default_options[:storage]
           when :s3
-            attachment.s3_object(style).acl.put(:private)
+            attachment.s3_object(style).acl.put(acl: 'private')
           when :fog
             # Not supported
           when :filesystem
-            FileUtils.chmod(0o600 & ~File.umask, attachment.path(style))
+            begin
+              FileUtils.chmod(0o600 & ~File.umask, attachment.path(style)) unless attachment.path(style).nil?
+            rescue Errno::ENOENT
+              Rails.logger.warn "Tried to change permission on non-existent file #{attachment.path(style)}"
+            end
           end
         end
       end
diff --git a/app/services/unsuspend_account_service.rb b/app/services/unsuspend_account_service.rb
index 3e731ddd9..91dbc9c18 100644
--- a/app/services/unsuspend_account_service.rb
+++ b/app/services/unsuspend_account_service.rb
@@ -18,7 +18,7 @@ class UnsuspendAccountService < BaseService
 
   def merge_into_home_timelines!
     @account.followers_for_local_distribution.find_each do |follower|
-      FeedManager.instance.merge_into_timeline(@account, follower)
+      FeedManager.instance.merge_into_home(@account, follower)
     end
   end
 
@@ -39,11 +39,15 @@ class UnsuspendAccountService < BaseService
         styles.each do |style|
           case Paperclip::Attachment.default_options[:storage]
           when :s3
-            attachment.s3_object(style).acl.put(Paperclip::Attachment.default_options[:s3_permissions])
+            attachment.s3_object(style).acl.put(acl: Paperclip::Attachment.default_options[:s3_permissions])
           when :fog
             # Not supported
           when :filesystem
-            FileUtils.chmod(0o666 & ~File.umask, attachment.path(style))
+            begin
+              FileUtils.chmod(0o666 & ~File.umask, attachment.path(style)) unless attachment.path(style).nil?
+            rescue Errno::ENOENT
+              Rails.logger.warn "Tried to change permission on non-existent file #{attachment.path(style)}"
+            end
           end
         end
       end
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index c9688ea88..1a81b96f6 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -39,12 +39,12 @@
             = render partial: 'statuses/status', collection: @pinned_statuses, as: :status, locals: { pinned: true }
 
           - if @newer_url
-            .entry= link_to_more @newer_url
+            .entry= link_to_newer @newer_url
 
           = render partial: 'statuses/status', collection: @statuses, as: :status
 
           - if @older_url
-            .entry= link_to_more @older_url
+            .entry= link_to_older @older_url
 
   .column-1
     - if @account.memorial?
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index f0a216f6b..d5978eddd 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -69,6 +69,8 @@
           = t('admin.accounts.confirming')
         - elsif @account.local? && !@account.user_approved?
           = t('admin.accounts.pending')
+        - elsif @account.sensitized?
+          = t('admin.accounts.sensitive')
         - else
           = t('admin.accounts.no_limits_imposed')
       .dashboard__counters__label= t 'admin.accounts.login_status'
@@ -192,6 +194,11 @@
           - else
             = link_to t('admin.accounts.disable'), new_admin_account_action_path(@account.id, type: 'disable'), class: 'button' if can?(:disable, @account.user)
 
+        - if @account.sensitized?
+          = link_to t('admin.accounts.undo_sensitized'), unsensitive_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsensitive, @account)
+        - elsif !@account.local? || @account.user_approved?
+          = link_to t('admin.accounts.sensitive'), new_admin_account_action_path(@account.id, type: 'sensitive'), class: 'button' if can?(:sensitive, @account)
+
         - if @account.silenced?
           = link_to t('admin.accounts.undo_silenced'), unsilence_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsilence, @account)
         - elsif !@account.local? || @account.user_approved?
diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml
index 9530e612a..ce47418d4 100644
--- a/app/views/home/index.html.haml
+++ b/app/views/home/index.html.haml
@@ -1,8 +1,8 @@
 - content_for :header_tags do
-  = preload_link_tag asset_pack_path('features/getting_started.js'), crossorigin: 'anonymous'
-  = preload_link_tag asset_pack_path('features/compose.js'), crossorigin: 'anonymous'
-  = preload_link_tag asset_pack_path('features/home_timeline.js'), crossorigin: 'anonymous'
-  = preload_link_tag asset_pack_path('features/notifications.js'), crossorigin: 'anonymous'
+  = preload_pack_asset 'features/getting_started.js', crossorigin: 'anonymous'
+  = preload_pack_asset 'features/compose.js', crossorigin: 'anonymous'
+  = preload_pack_asset 'features/home_timeline.js', crossorigin: 'anonymous'
+  = preload_pack_asset 'features/notifications.js', crossorigin: 'anonymous'
 
   %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
   = render_initial_state
diff --git a/app/views/layouts/_theme.html.haml b/app/views/layouts/_theme.html.haml
index 066d9de42..92de64b0d 100644
--- a/app/views/layouts/_theme.html.haml
+++ b/app/views/layouts/_theme.html.haml
@@ -2,12 +2,12 @@
   - if theme[:pack] != 'common' && theme[:common]
     = render partial: 'layouts/theme', object: theme[:common]
   - if theme[:pack]
-    = javascript_pack_tag theme[:flavour] ? "flavours/#{theme[:flavour]}/#{theme[:pack]}" : "core/#{theme[:pack]}", integrity: true, crossorigin: 'anonymous'
+    = javascript_pack_tag theme[:flavour] ? "flavours/#{theme[:flavour]}/#{theme[:pack]}" : "core/#{theme[:pack]}", crossorigin: 'anonymous'
     - if theme[:skin]
       - if !theme[:flavour] || theme[:skin] == 'default'
-        = stylesheet_pack_tag theme[:flavour] ? "flavours/#{theme[:flavour]}/#{theme[:pack]}" : "core/#{theme[:pack]}", integrity: true, media: 'all'
+        = stylesheet_pack_tag theme[:flavour] ? "flavours/#{theme[:flavour]}/#{theme[:pack]}" : "core/#{theme[:pack]}", media: 'all', crossorigin: 'anonymous'
       - else
-        = stylesheet_pack_tag "skins/#{theme[:flavour]}/#{theme[:skin]}/#{theme[:pack]}"
+        = stylesheet_pack_tag "skins/#{theme[:flavour]}/#{theme[:skin]}/#{theme[:pack]}", crossorigin: 'anonymous'
     - if theme[:preload]
       - theme[:preload].each do |link|
         %link{ href: asset_pack_path("#{link}.js"), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 1481f6973..32681773f 100755
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -21,12 +21,12 @@
 
     %title= content_for?(:page_title) ? safe_join([yield(:page_title).chomp.html_safe, title], ' - ') : title
 
-    = javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous'
+    = javascript_pack_tag "locales", crossorigin: 'anonymous'
     - if @theme
       - if @theme[:supported_locales].include? I18n.locale.to_s
-        = javascript_pack_tag "locales/#{@theme[:flavour]}/#{I18n.locale}", integrity: true, crossorigin: 'anonymous'
+        = javascript_pack_tag "locales/#{@theme[:flavour]}/#{I18n.locale}", crossorigin: 'anonymous'
       - elsif @theme[:supported_locales].include? 'en'
-        = javascript_pack_tag "locales/#{@theme[:flavour]}/en", integrity: true, crossorigin: 'anonymous'
+        = javascript_pack_tag "locales/#{@theme[:flavour]}/en", crossorigin: 'anonymous'
     = csrf_meta_tags
     %meta{ name: 'style-nonce', content: request.content_security_policy_nonce }
 
diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml
index 69b206f69..431bd260c 100644
--- a/app/views/layouts/embedded.html.haml
+++ b/app/views/layouts/embedded.html.haml
@@ -12,12 +12,12 @@
       %link{ rel: 'dns-prefetch', href: storage_host }/
 
     = render_initial_state
-    = javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous'
+    = javascript_pack_tag "locales", crossorigin: 'anonymous'
     - if @theme
       - if @theme[:supported_locales].include? I18n.locale.to_s
-        = javascript_pack_tag "locales/#{@theme[:flavour]}/#{I18n.locale}", integrity: true, crossorigin: 'anonymous'
+        = javascript_pack_tag "locales/#{@theme[:flavour]}/#{I18n.locale}", crossorigin: 'anonymous'
       - elsif @theme[:supported_locales].include? 'en'
-        = javascript_pack_tag "locales/#{@theme[:flavour]}/en", integrity: true, crossorigin: 'anonymous'
+        = javascript_pack_tag "locales/#{@theme[:flavour]}/en", crossorigin: 'anonymous'
     = render partial: 'layouts/theme', object: @core
     = render partial: 'layouts/theme', object: @theme
 
diff --git a/app/views/layouts/error.html.haml b/app/views/layouts/error.html.haml
index f8315afb5..55da5de3f 100644
--- a/app/views/layouts/error.html.haml
+++ b/app/views/layouts/error.html.haml
@@ -5,7 +5,7 @@
     %meta{ charset: 'utf-8' }/
     %title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ')
     %meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/
-    = javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous'
+    = javascript_pack_tag "locales", crossorigin: 'anonymous'
     = render partial: 'layouts/theme', object: (@core || { pack: 'common' })
     = render partial: 'layouts/theme', object: (@theme || { pack: 'error', flavour: 'glitch', common: { pack: 'common', flavour: 'glitch', skin: 'default' } })
   %body.error
diff --git a/app/views/media/player.html.haml b/app/views/media/player.html.haml
index bd5610a0b..7369628a4 100644
--- a/app/views/media/player.html.haml
+++ b/app/views/media/player.html.haml
@@ -1,11 +1,11 @@
 - content_for :header_tags do
   = render_initial_state
-  = javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous'
+  = javascript_pack_tag "locales", crossorigin: 'anonymous'
   - if @theme
     - if @theme[:supported_locales].include? I18n.locale.to_s
-      = javascript_pack_tag "locales/#{@theme[:flavour]}/#{I18n.locale}", integrity: true, crossorigin: 'anonymous'
+      = javascript_pack_tag "locales/#{@theme[:flavour]}/#{I18n.locale}", crossorigin: 'anonymous'
     - elsif @theme[:supported_locales].include? 'en'
-      = javascript_pack_tag "locales/#{@theme[:flavour]}/en", integrity: true, crossorigin: 'anonymous'
+      = javascript_pack_tag "locales/#{@theme[:flavour]}/en", crossorigin: 'anonymous'
   = render partial: 'layouts/theme', object: @core
   = render partial: 'layouts/theme', object: @theme
 
diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml
index b3e9c44fc..a4dd8534f 100644
--- a/app/views/statuses/_detailed_status.html.haml
+++ b/app/views/statuses/_detailed_status.html.haml
@@ -29,17 +29,17 @@
   - if !status.media_attachments.empty?
     - if status.media_attachments.first.video?
       - video = status.media_attachments.first
-      = react_component :video, src: full_asset_url(video.file.url(:original)), preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), blurhash: video.blurhash, sensitive: status.sensitive?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do
+      = react_component :video, src: full_asset_url(video.file.url(:original)), preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), blurhash: video.blurhash, sensitive: sensitized?(status, current_account), width: 670, height: 380, detailed: true, inline: true, alt: video.description do
         = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
     - elsif status.media_attachments.first.audio?
       - audio = status.media_attachments.first
       = react_component :audio, src: full_asset_url(audio.file.url(:original)), poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url), backgroundColor: audio.file.meta.dig('colors', 'background'), foregroundColor: audio.file.meta.dig('colors', 'foreground'), accentColor: audio.file.meta.dig('colors', 'accent'), width: 670, height: 380, alt: audio.description, duration: audio.file.meta.dig('original', 'duration') do
         = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
     - else
-      = react_component :media_gallery, height: 380, sensitive: status.sensitive?, standalone: true, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
+      = react_component :media_gallery, height: 380, sensitive: sensitized?(status, current_account), standalone: true, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
         = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
   - elsif status.preview_card
-    = react_component :card, sensitive: status.sensitive?, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
+    = react_component :card, sensitive: sensitized?(status, current_account), 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
 
   .detailed-status__meta
     %data.dt-published{ value: status.created_at.to_time.iso8601 }
diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml
index d095a1613..199061c46 100644
--- a/app/views/statuses/_simple_status.html.haml
+++ b/app/views/statuses/_simple_status.html.haml
@@ -35,17 +35,17 @@
   - if !status.media_attachments.empty?
     - if status.media_attachments.first.video?
       - video = status.media_attachments.first
-      = react_component :video, src: full_asset_url(video.file.url(:original)), preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), blurhash: video.blurhash, sensitive: status.sensitive?, width: 610, height: 343, inline: true, alt: video.description do
+      = react_component :video, src: full_asset_url(video.file.url(:original)), preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), blurhash: video.blurhash, sensitive: sensitized?(status, current_account), width: 610, height: 343, inline: true, alt: video.description do
         = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
     - elsif status.media_attachments.first.audio?
       - audio = status.media_attachments.first
       = react_component :audio, src: full_asset_url(audio.file.url(:original)), poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url), backgroundColor: audio.file.meta.dig('colors', 'background'), foregroundColor: audio.file.meta.dig('colors', 'foreground'), accentColor: audio.file.meta.dig('colors', 'accent'), width: 610, height: 343, alt: audio.description, duration: audio.file.meta.dig('original', 'duration') do
         = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
     - else
-      = react_component :media_gallery, height: 343, sensitive: status.sensitive?, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
+      = react_component :media_gallery, height: 343, sensitive: sensitized?(status, current_account), autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
         = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
   - elsif status.preview_card
-    = react_component :card, sensitive: status.sensitive?, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
+    = react_component :card, sensitive: sensitized?(status, current_account), 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
 
   - if !status.in_reply_to_id.nil? && status.in_reply_to_account_id == status.account.id
     = link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__content__read-more-button', target: stream_link_target, rel: 'noopener noreferrer' do
diff --git a/app/views/statuses/_status.html.haml b/app/views/statuses/_status.html.haml
index 0e3652503..650f9b679 100644
--- a/app/views/statuses/_status.html.haml
+++ b/app/views/statuses/_status.html.haml
@@ -17,7 +17,7 @@
 - if status.reply? && include_threads
   - if @next_ancestor
     .entry{ class: entry_classes }
-      = link_to_more ActivityPub::TagManager.instance.url_for(@next_ancestor)
+      = link_to_older ActivityPub::TagManager.instance.url_for(@next_ancestor)
 
   = render partial: 'statuses/status', collection: @ancestors, as: :status, locals: { is_predecessor: true, direct_reply_id: status.in_reply_to_id }, autoplay: autoplay
 
@@ -44,16 +44,16 @@
 - if include_threads
   - if @since_descendant_thread_id
     .entry{ class: entry_classes }
-      = link_to_more short_account_status_url(status.account.username, status, max_descendant_thread_id: @since_descendant_thread_id + 1)
+      = link_to_newer short_account_status_url(status.account.username, status, max_descendant_thread_id: @since_descendant_thread_id + 1)
   - @descendant_threads.each do |thread|
     = render partial: 'statuses/status', collection: thread[:statuses], as: :status, locals: { is_successor: true, parent_id: status.id }, autoplay: autoplay
 
     - if thread[:next_status]
       .entry{ class: entry_classes }
-        = link_to_more ActivityPub::TagManager.instance.url_for(thread[:next_status])
+        = link_to_newer ActivityPub::TagManager.instance.url_for(thread[:next_status])
   - if @next_descendant_thread
     .entry{ class: entry_classes }
-      = link_to_more short_account_status_url(status.account.username, status, since_descendant_thread_id: @max_descendant_thread_id - 1)
+      = link_to_newer short_account_status_url(status.account.username, status, since_descendant_thread_id: @max_descendant_thread_id - 1)
 
 - if include_threads && !embedded_view? && !user_signed_in?
   .entry{ class: entry_classes }
diff --git a/config/application.rb b/config/application.rb
index ad6cf82d7..bf467d6c3 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -22,6 +22,8 @@ require_relative '../lib/mastodon/version'
 require_relative '../lib/devise/two_factor_ldap_authenticatable'
 require_relative '../lib/devise/two_factor_pam_authenticatable'
 require_relative '../lib/chewy/strategy/custom_sidekiq'
+require_relative '../lib/webpacker/manifest_extensions'
+require_relative '../lib/webpacker/helper_extensions'
 
 Dotenv::Railtie.load
 
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 59e69ad37..ef612e177 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -10,6 +10,7 @@ Warden::Manager.after_set_user except: :fetch do |user, warden|
     expires: 1.year.from_now,
     httponly: true,
     secure: (Rails.env.production? || ENV['LOCAL_HTTPS'] == 'true'),
+    same_site: :lax,
   }
 end
 
@@ -20,6 +21,7 @@ Warden::Manager.after_fetch do |user, warden|
       expires: 1.year.from_now,
       httponly: true,
       secure: (Rails.env.production? || ENV['LOCAL_HTTPS'] == 'true'),
+      same_site: :lax,
     }
   else
     warden.logout
diff --git a/config/initializers/makara.rb b/config/initializers/makara.rb
new file mode 100644
index 000000000..dc88fa63c
--- /dev/null
+++ b/config/initializers/makara.rb
@@ -0,0 +1,2 @@
+Makara::Cookie::DEFAULT_OPTIONS[:same_site] = :lax
+Makara::Cookie::DEFAULT_OPTIONS[:secure]    = Rails.env.production? || ENV['LOCAL_HTTPS'] == 'true'
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index 3dc0edd6f..e5d1be4c6 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -1,3 +1,7 @@
 # Be sure to restart your server when you modify this file.
 
-Rails.application.config.session_store :cookie_store, key: '_mastodon_session', secure: (Rails.env.production? || ENV['LOCAL_HTTPS'] == 'true')
+Rails.application.config.session_store :cookie_store, {
+  key: '_mastodon_session',
+  secure: (Rails.env.production? || ENV['LOCAL_HTTPS'] == 'true'),
+  same_site: :lax,
+}
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 084006a2a..047ba36ac 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -188,6 +188,8 @@ en:
       search: Search
       search_same_email_domain: Other users with the same e-mail domain
       search_same_ip: Other users with the same IP
+      sensitive: Sensitive
+      sensitized: marked as sensitive
       shared_inbox_url: Shared inbox URL
       show:
         created_reports: Made reports
@@ -202,6 +204,7 @@ en:
       time_in_queue: Waiting in queue %{time}
       title: Accounts
       unconfirmed_email: Unconfirmed email
+      undo_sensitized: Undo sensitive
       undo_silenced: Undo silence
       undo_suspension: Undo suspension
       unsilenced_msg: Successfully unlimited %{username}'s account
@@ -243,9 +246,11 @@ en:
         reopen_report: Reopen Report
         reset_password_user: Reset Password
         resolve_report: Resolve Report
+        sensitive_account: Mark the media in your account as sensitive
         silence_account: Silence Account
         suspend_account: Suspend Account
         unassigned_report: Unassign Report
+        unsensitive_account: Unmark the media in your account as sensitive
         unsilence_account: Unsilence Account
         unsuspend_account: Unsuspend Account
         update_announcement: Update Announcement
@@ -281,9 +286,11 @@ en:
         reopen_report: "%{name} reopened report %{target}"
         reset_password_user: "%{name} reset password of user %{target}"
         resolve_report: "%{name} resolved report %{target}"
+        sensitive_account: "%{name} marked %{target}'s media as sensitive"
         silence_account: "%{name} silenced %{target}'s account"
         suspend_account: "%{name} suspended %{target}'s account"
         unassigned_report: "%{name} unassigned report %{target}"
+        unsensitive_account: "%{name} unmarked %{target}'s media as sensitive"
         unsilence_account: "%{name} unsilenced %{target}'s account"
         unsuspend_account: "%{name} unsuspended %{target}'s account"
         update_announcement: "%{name} updated announcement %{target}"
@@ -1203,6 +1210,8 @@ en:
         other: "%{count} votes"
       vote: Vote
     show_more: Show more
+    show_newer: Show newer
+    show_older: Show older
     show_thread: Show thread
     sign_in_to_participate: Sign in to participate in the conversation
     title: '%{name}: "%{quote}"'
@@ -1339,6 +1348,7 @@ en:
     warning:
       explanation:
         disable: You can no longer login to your account or use it in any other way, but your profile and other data remains intact.
+        sensitive: Your uploaded media files and linked media will be treated as sensitive.
         silence: You can still use your account but only people who are already following you will see your toots on this server, and you may be excluded from various public listings. However, others may still manually follow you.
         suspend: You can no longer use your account, and your profile and other data are no longer accessible. You can still login to request a backup of your data until the data is fully removed, but we will retain some data to prevent you from evading the suspension.
       get_in_touch: You can reply to this e-mail to get in touch with the staff of %{instance}.
@@ -1347,11 +1357,13 @@ en:
       subject:
         disable: Your account %{acct} has been frozen
         none: Warning for %{acct}
+        sensitive: Your account %{acct} posting media has been marked as sensitive
         silence: Your account %{acct} has been limited
         suspend: Your account %{acct} has been suspended
       title:
         disable: Account frozen
         none: Warning
+        sensitive: Your media has been marked as sensitive
         silence: Account limited
         suspend: Account suspended
     welcome:
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index fb6255546..fd9ec9427 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -172,6 +172,8 @@ ja:
       search: 検索
       search_same_email_domain: 同じドメインのメールアドレスを使用しているユーザー
       search_same_ip: 同じ IP のユーザーを検索
+      sensitive: 閲覧注意
+      sensitized: 閲覧注意済み
       shared_inbox_url: Shared inbox URL
       show:
         created_reports: このアカウントで作られた通報
@@ -184,6 +186,7 @@ ja:
       time_in_queue: "%{time} 待ち"
       title: アカウント
       unconfirmed_email: 確認待ちのメールアドレス
+      undo_sensitized: 閲覧注意から戻す
       undo_silenced: サイレンスから戻す
       undo_suspension: 停止から戻す
       unsubscribe: 購読の解除
@@ -220,9 +223,11 @@ ja:
         reopen_report: 通報を再度開く
         reset_password_user: パスワードをリセット
         resolve_report: 通報を解決済みにする
+        sensitive_account: アカウントのメディアを閲覧注意にマーク
         silence_account: アカウントをサイレンス
         suspend_account: アカウントを停止
         unassigned_report: 通報の担当を解除
+        unsensitive_account: アカウントのメディアの閲覧注意マークを解除
         unsilence_account: アカウントのサイレンスを解除
         unsuspend_account: アカウントの停止を解除
         update_announcement: お知らせを更新
@@ -256,9 +261,11 @@ ja:
         reopen_report: "%{name} さんが通報 %{target} を再び開きました"
         reset_password_user: "%{name} さんが %{target} さんのパスワードをリセットしました"
         resolve_report: "%{name} さんが通報 %{target} を解決済みにしました"
+        sensitive_account: "%{name} さんが %{target} さんのメディアを閲覧注意にマークしました"
         silence_account: "%{name} さんが %{target} さんをサイレンスにしました"
         suspend_account: "%{name} さんが %{target} さんを停止しました"
         unassigned_report: "%{name} さんが通報 %{target} の担当を外しました"
+        unsensitive_account: "%{name} さんが %{target} さんのメディアの閲覧注意を解除しました"
         unsilence_account: "%{name} さんが %{target} さんのサイレンスを解除しました"
         unsuspend_account: "%{name} さんが %{target} さんの停止を解除しました"
         update_announcement: "%{name} さんがお知らせ %{target} を更新しました"
@@ -1271,6 +1278,7 @@ ja:
     warning:
       explanation:
         disable: アカウントが凍結されている間、データはそのまま残りますが、凍結が解除されるまでは何の操作もできません。
+        sensitive: あなたのアップロードしたメディアファイルとリンク先のメディアは、閲覧注意として扱われます。
         silence: あなたのアカウントは制限されていますが、あなたをフォローしているユーザーのみ、このサーバー上の投稿を見ることができます。そしてあなたは様々な公開リストから除外されるかもしれません。ただし、他のユーザーは手動であなたをフォローすることができます。
         suspend: あなたのアカウントは停止されています。あなたの投稿とアップロードされたメディアファイルは、このサーバーとあなたのフォロワーが参加していたサーバーから完全に削除されました。
       get_in_touch: このメールに返信することで %{instance} のスタッフと連絡を取ることができます。
@@ -1279,11 +1287,13 @@ ja:
       subject:
         disable: あなたのアカウント %{acct} は凍結されました
         none: "%{acct} に対する警告"
+        sensitive: あなたのアカウント %{acct} の投稿メディアは閲覧注意とマークされました
         silence: あなたのアカウント %{acct} はサイレンスにされました
         suspend: あなたのアカウント %{acct} は停止されました
       title:
         disable: アカウントが凍結されました
         none: 警告
+        sensitive: あなたのメディアが閲覧注意とマークされました
         silence: アカウントがサイレンスにされました
         suspend: アカウントが停止されました
     welcome:
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index b69487953..46a4759a8 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -100,6 +100,7 @@ en:
         types:
           disable: Freeze
           none: Send a warning
+          sensitive: Sensitive
           silence: Limit
           suspend: Suspend
         warning_preset_id: Use a warning preset
diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml
index bbc0b5fd7..00f469b87 100644
--- a/config/locales/simple_form.ja.yml
+++ b/config/locales/simple_form.ja.yml
@@ -91,6 +91,7 @@ ja:
         types:
           disable: ログインを無効化
           none: 何もしない
+          sensitive: 閲覧注意
           silence: サイレンス
           suspend: 停止しアカウントのデータを恒久的に削除する
         warning_preset_id: プリセット警告文を使用
diff --git a/config/routes.rb b/config/routes.rb
index 327dcc58c..e78a2c4d0 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -238,6 +238,7 @@ Rails.application.routes.draw do
     resources :accounts, only: [:index, :show, :destroy] do
       member do
         post :enable
+        post :unsensitive
         post :unsilence
         post :unsuspend
         post :redownload
@@ -480,6 +481,7 @@ Rails.application.routes.draw do
         resources :accounts, only: [:index, :show, :destroy] do
           member do
             post :enable
+            post :unsensitive
             post :unsilence
             post :unsuspend
             post :approve
diff --git a/config/webpack/development.js b/config/webpack/development.js
index 774ecbc07..c3cf1b655 100644
--- a/config/webpack/development.js
+++ b/config/webpack/development.js
@@ -1,6 +1,6 @@
 // Note: You must restart bin/webpack-dev-server for changes to take effect
 
-const merge = require('webpack-merge');
+const { merge } = require('webpack-merge');
 const sharedConfig = require('./shared');
 const { settings, output } = require('./configuration');
 
diff --git a/config/webpack/production.js b/config/webpack/production.js
index f2f216422..f1d0dabae 100644
--- a/config/webpack/production.js
+++ b/config/webpack/production.js
@@ -2,7 +2,7 @@
 
 const path = require('path');
 const { URL } = require('url');
-const merge = require('webpack-merge');
+const { merge } = require('webpack-merge');
 const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
 const OfflinePlugin = require('offline-plugin');
 const TerserPlugin = require('terser-webpack-plugin');
diff --git a/config/webpack/shared.js b/config/webpack/shared.js
index 11c321c58..ce08ac206 100644
--- a/config/webpack/shared.js
+++ b/config/webpack/shared.js
@@ -104,7 +104,8 @@ module.exports = {
       chunkFilename: 'css/[name]-[contenthash:8].chunk.css',
     }),
     new AssetsManifestPlugin({
-      integrity: false,
+      integrity: true,
+      integrityHashes: ['sha256'],
       entrypoints: true,
       writeToDisk: true,
       publicPath: true,
diff --git a/config/webpack/tests.js b/config/webpack/tests.js
index 8b56eb92f..f9d39f1b8 100644
--- a/config/webpack/tests.js
+++ b/config/webpack/tests.js
@@ -1,6 +1,6 @@
 // Note: You must restart bin/webpack-dev-server for changes to take effect
 
-const merge = require('webpack-merge');
+const { merge } = require('webpack-merge');
 const sharedConfig = require('./shared.js');
 
 module.exports = merge(sharedConfig, {
diff --git a/db/migrate/20200614002136_add_sensitized_to_accounts.rb b/db/migrate/20200614002136_add_sensitized_to_accounts.rb
new file mode 100644
index 000000000..bc2dfcb63
--- /dev/null
+++ b/db/migrate/20200614002136_add_sensitized_to_accounts.rb
@@ -0,0 +1,5 @@
+class AddSensitizedToAccounts < ActiveRecord::Migration[5.2]
+  def change
+    add_column :accounts, :sensitized_at, :datetime
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 6d6f97f0a..6d721d63e 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -189,6 +189,7 @@ ActiveRecord::Schema.define(version: 2020_10_08_220312) do
     t.integer "avatar_storage_schema_version"
     t.integer "header_storage_schema_version"
     t.string "devices_url"
+    t.datetime "sensitized_at"
     t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
     t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true
     t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id"
diff --git a/lib/webpacker/helper_extensions.rb b/lib/webpacker/helper_extensions.rb
new file mode 100644
index 000000000..8f46d7631
--- /dev/null
+++ b/lib/webpacker/helper_extensions.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Webpacker::HelperExtensions
+  def javascript_pack_tag(name, **options)
+    src, integrity = current_webpacker_instance.manifest.lookup!(name, type: :javascript, with_integrity: true)
+    javascript_include_tag(src, options.merge(integrity: integrity))
+  end
+
+  def stylesheet_pack_tag(name, **options)
+    src, integrity = current_webpacker_instance.manifest.lookup!(name, type: :stylesheet, with_integrity: true)
+    stylesheet_link_tag(src, options.merge(integrity: integrity))
+  end
+
+  def preload_pack_asset(name, **options)
+    src, integrity = current_webpacker_instance.manifest.lookup!(name, with_integrity: true)
+    preload_link_tag(src, options.merge(integrity: integrity))
+  end
+end
+
+Webpacker::Helper.prepend(Webpacker::HelperExtensions)
diff --git a/lib/webpacker/manifest_extensions.rb b/lib/webpacker/manifest_extensions.rb
new file mode 100644
index 000000000..789eb81cc
--- /dev/null
+++ b/lib/webpacker/manifest_extensions.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Webpacker::ManifestExtensions
+  def lookup(name, pack_type = {})
+    asset = super
+
+    if pack_type[:with_integrity] && asset.respond_to?(:dig)
+      [asset.dig('src'), asset.dig('integrity')]
+    elsif asset.respond_to?(:dig)
+      asset.dig('src')
+    else
+      asset
+    end
+  end
+end
+
+Webpacker::Manifest.prepend(Webpacker::ManifestExtensions)
diff --git a/package.json b/package.json
index 15f6b39f9..b125f8cb6 100644
--- a/package.json
+++ b/package.json
@@ -85,11 +85,11 @@
     "babel-runtime": "^6.26.0",
     "blurhash": "^1.1.3",
     "classnames": "^2.2.5",
-    "compression-webpack-plugin": "^6.0.3",
+    "compression-webpack-plugin": "^6.0.4",
     "cross-env": "^7.0.2",
     "css-loader": "^5.0.0",
     "cssnano": "^4.1.10",
-    "detect-passive-events": "^1.0.5",
+    "detect-passive-events": "^2.0.1",
     "dotenv": "^8.2.0",
     "emoji-mart": "Gargron/emoji-mart#build",
     "es6-symbol": "^3.1.3",
@@ -97,7 +97,7 @@
     "exif-js": "^2.3.0",
     "express": "^4.17.1",
     "favico.js": "^0.3.10",
-    "file-loader": "^6.1.1",
+    "file-loader": "^6.2.0",
     "font-awesome": "^4.7.0",
     "glob": "^7.1.6",
     "history": "^4.10.1",
@@ -113,7 +113,7 @@
     "lodash": "^4.17.19",
     "mark-loader": "^0.1.6",
     "marky": "^1.2.1",
-    "mini-css-extract-plugin": "^1.2.0",
+    "mini-css-extract-plugin": "^1.2.1",
     "mkdirp": "^1.0.4",
     "npmlog": "^4.1.2",
     "object-assign": "^4.1.1",
@@ -137,7 +137,7 @@
     "react-motion": "^0.5.2",
     "react-notification": "^6.8.5",
     "react-overlays": "^0.9.2",
-    "react-redux": "^7.2.1",
+    "react-redux": "^7.2.2",
     "react-redux-loading-bar": "^4.0.8",
     "react-router-dom": "^4.1.1",
     "react-router-scroll-4": "^1.0.0-beta.1",
@@ -155,7 +155,7 @@
     "requestidlecallback": "^0.3.0",
     "reselect": "^4.0.0",
     "rimraf": "^3.0.2",
-    "sass": "^1.27.0",
+    "sass": "^1.28.0",
     "sass-loader": "^10.0.4",
     "stacktrace-js": "^2.0.2",
     "stringz": "^2.1.0",
@@ -169,17 +169,17 @@
     "webpack-assets-manifest": "^3.1.1",
     "webpack-bundle-analyzer": "^3.9.0",
     "webpack-cli": "^3.3.12",
-    "webpack-merge": "^4.2.1",
-    "wicg-inert": "^3.0.3"
+    "webpack-merge": "^5.0.9",
+    "wicg-inert": "^3.1.0"
   },
   "devDependencies": {
     "@testing-library/jest-dom": "^5.11.5",
     "@testing-library/react": "^11.1.0",
     "babel-eslint": "^10.1.0",
     "babel-jest": "^26.6.1",
-    "eslint": "^7.12.0",
+    "eslint": "^7.12.1",
     "eslint-plugin-import": "~2.22.1",
-    "eslint-plugin-jsx-a11y": "~6.3.1",
+    "eslint-plugin-jsx-a11y": "~6.4.1",
     "eslint-plugin-promise": "~4.2.1",
     "eslint-plugin-react": "~7.21.5",
     "jest": "^26.6.1",
diff --git a/spec/controllers/api/v1/admin/accounts_controller_spec.rb b/spec/controllers/api/v1/admin/accounts_controller_spec.rb
index f3f9946ba..89cadb222 100644
--- a/spec/controllers/api/v1/admin/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/admin/accounts_controller_spec.rb
@@ -127,6 +127,24 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
     end
   end
 
+  describe 'POST #unsensitive' do
+    before do
+      account.touch(:sensitized_at)
+      post :unsensitive, params: { id: account.id }
+    end
+
+    it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+    it_behaves_like 'forbidden for wrong role', 'user'
+
+    it 'returns http success' do
+      expect(response).to have_http_status(200)
+    end
+
+    it 'unsensitives account' do
+      expect(account.reload.sensitized?).to be false
+    end
+  end
+
   describe 'POST #unsilence' do
     before do
       account.touch(:silenced_at)
diff --git a/spec/models/admin/account_action_spec.rb b/spec/models/admin/account_action_spec.rb
index 87fc28500..2366b9ca4 100644
--- a/spec/models/admin/account_action_spec.rb
+++ b/spec/models/admin/account_action_spec.rb
@@ -115,16 +115,16 @@ RSpec.describe Admin::AccountAction, type: :model do
     context 'account.local?' do
       let(:account) { Fabricate(:account, domain: nil) }
 
-      it 'returns ["none", "disable", "silence", "suspend"]' do
-        expect(subject).to eq %w(none disable silence suspend)
+      it 'returns ["none", "disable", "sensitive", "silence", "suspend"]' do
+        expect(subject).to eq %w(none disable sensitive silence suspend)
       end
     end
 
     context '!account.local?' do
       let(:account) { Fabricate(:account, domain: 'hoge.com') }
 
-      it 'returns ["silence", "suspend"]' do
-        expect(subject).to eq %w(silence suspend)
+      it 'returns ["sensitive", "silence", "suspend"]' do
+        expect(subject).to eq %w(sensitive silence suspend)
       end
     end
   end
diff --git a/spec/policies/account_policy_spec.rb b/spec/policies/account_policy_spec.rb
index 6648b0888..d27e9d5b0 100644
--- a/spec/policies/account_policy_spec.rb
+++ b/spec/policies/account_policy_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe AccountPolicy do
   let(:admin)   { Fabricate(:user, admin: true).account }
   let(:john)    { Fabricate(:user).account }
 
-  permissions :index?, :show?, :unsuspend?, :unsilence?, :remove_avatar?, :remove_header? do
+  permissions :index?, :show?, :unsuspend?, :unsensitive?, :unsilence?, :remove_avatar?, :remove_header? do
     context 'staff' do
       it 'permits' do
         expect(subject).to permit(admin)
diff --git a/yarn.lock b/yarn.lock
index aa6810819..40a8735ea 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1080,10 +1080,10 @@
   resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.4.tgz#622a72bebd1e3f48d921563b4b60a762295a81fc"
   integrity sha512-6PYY5DVdAY1ifaQW6XYTnOMihmBVT27elqSjEoodchsGjzYlEsTQMcEhSud99kVawatyTZRTiVkJ/c6lwbQ7nA==
 
-"@eslint/eslintrc@^0.2.0":
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.2.0.tgz#bc7e3c4304d4c8720968ccaee793087dfb5fe6b4"
-  integrity sha512-+cIGPCBdLCzqxdtwppswP+zTsH9BOIGzAeKfBIbtb4gW/giMlfMwP0HUSFfhzh20f9u8uZ8hOp62+4GPquTbwQ==
+"@eslint/eslintrc@^0.2.1":
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.2.1.tgz#f72069c330461a06684d119384435e12a5d76e3c"
+  integrity sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA==
   dependencies:
     ajv "^6.12.4"
     debug "^4.1.1"
@@ -2100,10 +2100,10 @@ aws4@^1.8.0:
   resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428"
   integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==
 
-axe-core@^3.5.4:
-  version "3.5.5"
-  resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227"
-  integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q==
+axe-core@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.0.2.tgz#c7cf7378378a51fcd272d3c09668002a4990b1cb"
+  integrity sha512-arU1h31OGFu+LPrOLGZ7nB45v940NMDMEJeNmbutu57P+UFDVnkZg3e+J1I2HJRZ9hT7gO8J91dn/PMrAiKakA==
 
 axios@^0.21.0:
   version "0.21.0"
@@ -2112,7 +2112,7 @@ axios@^0.21.0:
   dependencies:
     follow-redirects "^1.10.0"
 
-axobject-query@^2.1.2:
+axobject-query@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
   integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==
@@ -2894,6 +2894,15 @@ cliui@^7.0.2:
     strip-ansi "^6.0.0"
     wrap-ansi "^7.0.0"
 
+clone-deep@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
+  integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
+  dependencies:
+    is-plain-object "^2.0.4"
+    kind-of "^6.0.2"
+    shallow-clone "^3.0.0"
+
 co@^4.6.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -3000,10 +3009,10 @@ compressible@~2.0.16:
   dependencies:
     mime-db ">= 1.43.0 < 2"
 
-compression-webpack-plugin@^6.0.3:
-  version "6.0.3"
-  resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-6.0.3.tgz#d0d3e913810e3bf67462e1cecd794b3109af89de"
-  integrity sha512-xzSWiZWwBs+HHGhlYxw0oFaYL/0VYErEqDHCAJhJ3Mza5fmF5JJ4iaB6Ap2JT68C0UhhmoI4Mh37LVz/THv2Fw==
+compression-webpack-plugin@^6.0.4:
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-6.0.4.tgz#524699c0ad4e94cab0eb199c734e291f6ab685b9"
+  integrity sha512-PViPdrF5UmqZxsr9WNoE+R6lTre6/5tC9TmWotBfhOQtWlc7oj/SXCsrecbZJ9LDpwLjHH6llPCKmw+JGPGN+A==
   dependencies:
     cacache "^15.0.5"
     find-cache-dir "^3.3.1"
@@ -3644,10 +3653,10 @@ detect-node@^2.0.4:
   resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
   integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
 
-detect-passive-events@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/detect-passive-events/-/detect-passive-events-1.0.5.tgz#ce324db665123bef9e368b8059ff95d95217cc05"
-  integrity sha512-foW7Q35wwOCxVzW0xLf5XeB5Fhe7oyRgvkBYdiP9IWgLMzjqUqTvsJv9ymuEWGjY6AoDXD3OC294+Z9iuOw0QA==
+detect-passive-events@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/detect-passive-events/-/detect-passive-events-2.0.1.tgz#fdbd6f6dd5e6ac10c6189a4cb26ab264d41c0835"
+  integrity sha512-7WbRn4mznO63FW0KSYa7S3HgCG94uZ6HGZO1TyVRtdZuMNGUeY/ScWrIx45XnUz1LWoLZVi13ULVHqKE07ZfKg==
 
 diff-sequences@^25.2.6:
   version "25.2.6"
@@ -4107,21 +4116,21 @@ eslint-plugin-import@~2.22.1:
     resolve "^1.17.0"
     tsconfig-paths "^3.9.0"
 
-eslint-plugin-jsx-a11y@~6.3.1:
-  version "6.3.1"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.3.1.tgz#99ef7e97f567cc6a5b8dd5ab95a94a67058a2660"
-  integrity sha512-i1S+P+c3HOlBJzMFORRbC58tHa65Kbo8b52/TwCwSKLohwvpfT5rm2GjGWzOHTEuq4xxf2aRlHHTtmExDQOP+g==
+eslint-plugin-jsx-a11y@~6.4.1:
+  version "6.4.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz#a2d84caa49756942f42f1ffab9002436391718fd"
+  integrity sha512-0rGPJBbwHoGNPU73/QCLP/vveMlM1b1Z9PponxO87jfr6tuH5ligXbDT6nHSSzBC8ovX2Z+BQu7Bk5D/Xgq9zg==
   dependencies:
-    "@babel/runtime" "^7.10.2"
+    "@babel/runtime" "^7.11.2"
     aria-query "^4.2.2"
     array-includes "^3.1.1"
     ast-types-flow "^0.0.7"
-    axe-core "^3.5.4"
-    axobject-query "^2.1.2"
+    axe-core "^4.0.2"
+    axobject-query "^2.2.0"
     damerau-levenshtein "^1.0.6"
     emoji-regex "^9.0.0"
     has "^1.0.3"
-    jsx-ast-utils "^2.4.1"
+    jsx-ast-utils "^3.1.0"
     language-tags "^1.0.5"
 
 eslint-plugin-promise@~4.2.1:
@@ -4218,13 +4227,13 @@ eslint@^2.7.0:
     text-table "~0.2.0"
     user-home "^2.0.0"
 
-eslint@^7.12.0:
-  version "7.12.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.12.0.tgz#7b6a85f87a9adc239e979bb721cde5ce0dc27da6"
-  integrity sha512-n5pEU27DRxCSlOhJ2rO57GDLcNsxO0LPpAbpFdh7xmcDmjmlGUfoyrsB3I7yYdQXO5N3gkSTiDrPSPNFiiirXA==
+eslint@^7.12.1:
+  version "7.12.1"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.12.1.tgz#bd9a81fa67a6cfd51656cdb88812ce49ccec5801"
+  integrity sha512-HlMTEdr/LicJfN08LB3nM1rRYliDXOmfoO4vj39xN6BLpFzF00hbwBoqHk8UcJ2M/3nlARZWy/mslvGEuZFvsg==
   dependencies:
     "@babel/code-frame" "^7.0.0"
-    "@eslint/eslintrc" "^0.2.0"
+    "@eslint/eslintrc" "^0.2.1"
     ajv "^6.10.0"
     chalk "^4.0.0"
     cross-spawn "^7.0.2"
@@ -4586,10 +4595,10 @@ file-entry-cache@^5.0.1:
   dependencies:
     flat-cache "^2.0.1"
 
-file-loader@^6.1.1:
-  version "6.1.1"
-  resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.1.1.tgz#a6f29dfb3f5933a1c350b2dbaa20ac5be0539baa"
-  integrity sha512-Klt8C4BjWSXYQAfhpYYkG4qHNTna4toMHEbWrI5IuVoxbU6uiDKeKAP99R8mmbJi3lvewn/jQBOgU4+NS3tDQw==
+file-loader@^6.2.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d"
+  integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==
   dependencies:
     loader-utils "^2.0.0"
     schema-utils "^3.0.0"
@@ -5196,10 +5205,10 @@ hoist-non-react-statics@^2.5.0:
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
   integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
 
-hoist-non-react-statics@^3.3.0:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b"
-  integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==
+hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
+  integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
   dependencies:
     react-is "^16.7.0"
 
@@ -6576,18 +6585,10 @@ jsprim@^1.2.2:
     json-schema "0.2.3"
     verror "1.10.0"
 
-jsx-ast-utils@^2.4.1:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz#1114a4c1209481db06c690c2b4f488cc665f657e"
-  integrity sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w==
-  dependencies:
-    array-includes "^3.1.1"
-    object.assign "^4.1.0"
-
-"jsx-ast-utils@^2.4.1 || ^3.0.0":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.0.0.tgz#0f49d5093bafa4b45d3fe02147d8b40ffc6c7438"
-  integrity sha512-sPuicm6EPKYI/UnWpOatvg4pI50qaBo4dSOMGUPutmJ26ttedFKXr0It0XXPk4HKnQ/1X0st4eSS2w2jhFk9Ow==
+"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.1.0.tgz#642f1d7b88aa6d7eb9d8f2210e166478444fa891"
+  integrity sha512-d4/UOjg+mxAWxCiF0c5UTSwyqbchkbqCvK87aBovhnh8GtysTjWmgC63tY0cJx/HzGgm9qnA147jVBdpOiQ2RA==
   dependencies:
     array-includes "^3.1.1"
     object.assign "^4.1.1"
@@ -7006,10 +7007,10 @@ min-indent@^1.0.0:
   resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
   integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
 
-mini-css-extract-plugin@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.2.0.tgz#f1bdfa7bb6f6a238bc327f813f204283ea33ee36"
-  integrity sha512-iBZokjaIjHvI4N0AURx5aPBawcmxB/d2NYikxZ4J57Lg5sDShUPyWvuSWl1dueI5oCs7nz8V7qtOCaLjB7AYPw==
+mini-css-extract-plugin@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.2.1.tgz#30ea7dee632b3002b0c77aeed447790408cb247e"
+  integrity sha512-G3yw7/TQaPfkuiR73MDcyiqhyP8SnbmLhUbpC76H+wtQxA6wfKhMCQOCb6wnPK0dQbjORAeOILQqEesg4/wF7A==
   dependencies:
     loader-utils "^2.0.0"
     schema-utils "^3.0.0"
@@ -8679,7 +8680,7 @@ react-intl@^2.9.0:
     intl-relativeformat "^2.1.0"
     invariant "^2.1.1"
 
-react-is@^16.12.0, react-is@^16.3.2, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6, react-is@^16.9.0:
+react-is@^16.12.0, react-is@^16.13.1, react-is@^16.3.2, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6:
   version "16.13.1"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -8739,16 +8740,16 @@ react-redux-loading-bar@^4.0.8:
     prop-types "^15.6.2"
     react-lifecycles-compat "^3.0.2"
 
-react-redux@^7.2.1:
-  version "7.2.1"
-  resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.1.tgz#8dedf784901014db2feca1ab633864dee68ad985"
-  integrity sha512-T+VfD/bvgGTUA74iW9d2i5THrDQWbweXP0AVNI8tNd1Rk5ch1rnMiJkDD67ejw7YBKM4+REvcvqRuWJb7BLuEg==
+react-redux@^7.2.2:
+  version "7.2.2"
+  resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.2.tgz#03862e803a30b6b9ef8582dadcc810947f74b736"
+  integrity sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==
   dependencies:
-    "@babel/runtime" "^7.5.5"
-    hoist-non-react-statics "^3.3.0"
+    "@babel/runtime" "^7.12.1"
+    hoist-non-react-statics "^3.3.2"
     loose-envify "^1.4.0"
     prop-types "^15.7.2"
-    react-is "^16.9.0"
+    react-is "^16.13.1"
 
 react-router-dom@^4.1.1:
   version "4.3.1"
@@ -9424,10 +9425,10 @@ sass-loader@^10.0.4:
     schema-utils "^3.0.0"
     semver "^7.3.2"
 
-sass@^1.27.0:
-  version "1.27.0"
-  resolved "https://registry.yarnpkg.com/sass/-/sass-1.27.0.tgz#0657ff674206b95ec20dc638a93e179c78f6ada2"
-  integrity sha512-0gcrER56OkzotK/GGwgg4fPrKuiFlPNitO7eUJ18Bs+/NBlofJfMxmxqpqJxjae9vu0Wq8TZzrSyxZal00WDig==
+sass@^1.28.0:
+  version "1.28.0"
+  resolved "https://registry.yarnpkg.com/sass/-/sass-1.28.0.tgz#546f1308ff74cc4ec2ad735fd35dc18bc3f51f72"
+  integrity sha512-9FWX/0wuE1KxwfiP02chZhHaPzu6adpx9+wGch7WMOuHy5npOo0UapRI3FNSHva2CczaYJu2yNUBN8cCSqHz/A==
   dependencies:
     chokidar ">=2.0.0 <4.0.0"
 
@@ -9615,6 +9616,13 @@ sha.js@^2.4.0, sha.js@^2.4.8:
     inherits "^2.0.1"
     safe-buffer "^5.0.1"
 
+shallow-clone@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
+  integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
+  dependencies:
+    kind-of "^6.0.2"
+
 shallow-equal@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da"
@@ -11048,12 +11056,13 @@ webpack-log@^2.0.0:
     ansi-colors "^3.0.0"
     uuid "^3.3.2"
 
-webpack-merge@^4.2.1:
-  version "4.2.2"
-  resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d"
-  integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==
+webpack-merge@^5.0.9:
+  version "5.0.9"
+  resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.0.9.tgz#d5e0e0ae564ae704836d747893bdd2741544bf31"
+  integrity sha512-P4teh6O26xIDPugOGX61wPxaeP918QOMjmzhu54zTVcLtOS28ffPWtnv+ilt3wscwBUCL2WNMnh97XkrKqt9Fw==
   dependencies:
-    lodash "^4.17.15"
+    clone-deep "^4.0.1"
+    wildcard "^2.0.0"
 
 webpack-sources@^1.0.0, webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
   version "1.4.3"
@@ -11153,10 +11162,10 @@ which@^2.0.1, which@^2.0.2:
   dependencies:
     isexe "^2.0.0"
 
-wicg-inert@^3.0.3:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/wicg-inert/-/wicg-inert-3.0.3.tgz#7d05eaed64176887ee4c66fc0c4d6fe4b38ccce5"
-  integrity sha512-XwXf8K0NN4cpagjBlZ2/j/5Sjf6dW3HNbfywEy1y6Z8PJKvSHVGiuc5Id/9RZ6EmGq+GQCGTo7B2SK0Misbr6g==
+wicg-inert@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/wicg-inert/-/wicg-inert-3.1.0.tgz#6525f12db188b83f0051bed2ddcf6c1aa5b17590"
+  integrity sha512-P0ZiWaN9SxOkJbYtF/PIwmIRO8UTqTJtyl33QTQlHfAb6h15T0Dp5m7WTJ8N6UWIoj+KU5M0a8EtfRZLlHiP0Q==
 
 wide-align@^1.1.0:
   version "1.1.3"
@@ -11165,6 +11174,11 @@ wide-align@^1.1.0:
   dependencies:
     string-width "^1.0.2 || 2"
 
+wildcard@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
+  integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
+
 word-wrap@^1.2.3, word-wrap@~1.2.3:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"