about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/api/v1/push/subscriptions_controller.rb6
-rw-r--r--app/helpers/admin/account_moderation_notes_helper.rb10
-rw-r--r--app/helpers/jsonld_helper.rb15
-rw-r--r--app/helpers/settings_helper.rb1
-rw-r--r--app/javascript/mastodon/components/status.js4
-rw-r--r--app/javascript/mastodon/containers/cards_container.js59
-rw-r--r--app/javascript/mastodon/containers/media_container.js90
-rw-r--r--app/javascript/mastodon/containers/media_galleries_container.js68
-rw-r--r--app/javascript/mastodon/containers/video_container.js26
-rw-r--r--app/javascript/mastodon/features/status/components/detailed_status.js4
-rw-r--r--app/javascript/mastodon/features/ui/components/media_modal.js17
-rw-r--r--app/javascript/mastodon/features/video/index.js25
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json2
-rw-r--r--app/javascript/mastodon/service_worker/entry.js44
-rw-r--r--app/javascript/mastodon/storage/modifier.js4
-rw-r--r--app/javascript/packs/public.js33
-rw-r--r--app/javascript/styles/mastodon/admin.scss33
-rw-r--r--app/javascript/styles/mastodon/components.scss4
-rw-r--r--app/javascript/styles/mastodon/containers.scss3
-rw-r--r--app/javascript/styles/mastodon/tables.scss11
-rw-r--r--app/lib/activitypub/activity/announce.rb2
-rw-r--r--app/models/concerns/status_threading_concern.rb23
-rw-r--r--app/services/activitypub/fetch_remote_status_service.rb4
-rw-r--r--app/services/post_status_service.rb2
-rw-r--r--app/views/admin/reports/_status.html.haml26
25 files changed, 266 insertions, 250 deletions
diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb
index 5038cc03c..1a19bd0ef 100644
--- a/app/controllers/api/v1/push/subscriptions_controller.rb
+++ b/app/controllers/api/v1/push/subscriptions_controller.rb
@@ -20,6 +20,12 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
     render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
   end
 
+  def show
+    raise ActiveRecord::RecordNotFound if @web_subscription.nil?
+
+    render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
+  end
+
   def update
     raise ActiveRecord::RecordNotFound if @web_subscription.nil?
 
diff --git a/app/helpers/admin/account_moderation_notes_helper.rb b/app/helpers/admin/account_moderation_notes_helper.rb
index fdfadef08..49e764cef 100644
--- a/app/helpers/admin/account_moderation_notes_helper.rb
+++ b/app/helpers/admin/account_moderation_notes_helper.rb
@@ -10,10 +10,16 @@ module Admin::AccountModerationNotesHelper
     end
   end
 
+  def admin_account_inline_link_to(account)
+    link_to admin_account_path(account.id), class: name_tag_classes(account, true) do
+      content_tag(:span, account.acct, class: 'username')
+    end
+  end
+
   private
 
-  def name_tag_classes(account)
-    classes = ['name-tag']
+  def name_tag_classes(account, inline = false)
+    classes = [inline ? 'inline-name-tag' : 'name-tag']
     classes << 'suspended' if account.suspended?
     classes.join(' ')
   end
diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb
index e9056166c..9d2b6cf00 100644
--- a/app/helpers/jsonld_helper.rb
+++ b/app/helpers/jsonld_helper.rb
@@ -52,18 +52,22 @@ module JsonLdHelper
     graph.dump(:normalize)
   end
 
-  def fetch_resource(uri, id)
+  def fetch_resource(uri, id, on_behalf_of = nil)
     unless id
-      json = fetch_resource_without_id_validation(uri)
+      json = fetch_resource_without_id_validation(uri, on_behalf_of)
       return unless json
       uri = json['id']
     end
 
-    json = fetch_resource_without_id_validation(uri)
+    json = fetch_resource_without_id_validation(uri, on_behalf_of)
     json.present? && json['id'] == uri ? json : nil
   end
 
-  def fetch_resource_without_id_validation(uri)
+  def fetch_resource_without_id_validation(uri, on_behalf_of = nil)
+    build_request(uri, on_behalf_of).perform do |response|
+      return body_to_json(response.body_with_limit) if response.code == 200
+    end
+    # If request failed, retry without doing it on behalf of a user
     build_request(uri).perform do |response|
       response.code == 200 ? body_to_json(response.body_with_limit) : nil
     end
@@ -85,8 +89,9 @@ module JsonLdHelper
 
   private
 
-  def build_request(uri)
+  def build_request(uri, on_behalf_of = nil)
     request = Request.new(:get, uri)
+    request.on_behalf_of(on_behalf_of) if on_behalf_of
     request.add_headers('Accept' => 'application/activity+json, application/ld+json')
     request
   end
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index f6d86a18e..ba728eb32 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -33,6 +33,7 @@ module SettingsHelper
     'pt-BR': 'Português do Brasil',
     ru: 'Русский',
     sk: 'Slovensky',
+    sl: 'Slovenščina',
     sr: 'Српски',
     'sr-Latn': 'Srpski (latinica)',
     sv: 'Svenska',
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index 953d98c20..fd08ff3b7 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -84,8 +84,8 @@ export default class Status extends ImmutablePureComponent {
     return <div className='media-spoiler-video' style={{ height: '110px' }} />;
   }
 
-  handleOpenVideo = startTime => {
-    this.props.onOpenVideo(this._properStatus().getIn(['media_attachments', 0]), startTime);
+  handleOpenVideo = (media, startTime) => {
+    this.props.onOpenVideo(media, startTime);
   }
 
   handleHotkeyReply = e => {
diff --git a/app/javascript/mastodon/containers/cards_container.js b/app/javascript/mastodon/containers/cards_container.js
deleted file mode 100644
index 894bf4ef9..000000000
--- a/app/javascript/mastodon/containers/cards_container.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import React, { Fragment } from 'react';
-import ReactDOM from 'react-dom';
-import PropTypes from 'prop-types';
-import { IntlProvider, addLocaleData } from 'react-intl';
-import { getLocale } from '../locales';
-import Card from '../features/status/components/card';
-import ModalRoot from '../components/modal_root';
-import MediaModal from '../features/ui/components/media_modal';
-import { fromJS } from 'immutable';
-
-const { localeData, messages } = getLocale();
-addLocaleData(localeData);
-
-export default class CardsContainer extends React.PureComponent {
-
-  static propTypes = {
-    locale: PropTypes.string,
-    cards: PropTypes.object.isRequired,
-  };
-
-  state = {
-    media: null,
-  };
-
-  handleOpenCard = (media) => {
-    document.body.classList.add('card-standalone__body');
-    this.setState({ media });
-  }
-
-  handleCloseCard = () => {
-    document.body.classList.remove('card-standalone__body');
-    this.setState({ media: null });
-  }
-
-  render () {
-    const { locale, cards } = this.props;
-
-    return (
-      <IntlProvider locale={locale} messages={messages}>
-        <Fragment>
-          {[].map.call(cards, container => {
-            const { card, ...props } = JSON.parse(container.getAttribute('data-props'));
-
-            return ReactDOM.createPortal(
-              <Card card={fromJS(card)} onOpenMedia={this.handleOpenCard} {...props} />,
-              container,
-            );
-          })}
-          <ModalRoot onClose={this.handleCloseCard}>
-            {this.state.media && (
-              <MediaModal media={this.state.media} index={0} onClose={this.handleCloseCard} />
-            )}
-          </ModalRoot>
-        </Fragment>
-      </IntlProvider>
-    );
-  }
-
-}
diff --git a/app/javascript/mastodon/containers/media_container.js b/app/javascript/mastodon/containers/media_container.js
new file mode 100644
index 000000000..1700fba05
--- /dev/null
+++ b/app/javascript/mastodon/containers/media_container.js
@@ -0,0 +1,90 @@
+import React, { PureComponent, Fragment } from 'react';
+import ReactDOM from 'react-dom';
+import PropTypes from 'prop-types';
+import { IntlProvider, addLocaleData } from 'react-intl';
+import { getLocale } from '../locales';
+import MediaGallery from '../components/media_gallery';
+import Video from '../features/video';
+import Card from '../features/status/components/card';
+import ModalRoot from '../components/modal_root';
+import MediaModal from '../features/ui/components/media_modal';
+import { List as ImmutableList, fromJS } from 'immutable';
+
+const { localeData, messages } = getLocale();
+addLocaleData(localeData);
+
+const MEDIA_COMPONENTS = { MediaGallery, Video, Card };
+
+export default class MediaContainer extends PureComponent {
+
+  static propTypes = {
+    locale: PropTypes.string.isRequired,
+    components: PropTypes.object.isRequired,
+  };
+
+  state = {
+    media: null,
+    index: null,
+    time: null,
+  };
+
+  handleOpenMedia = (media, index) => {
+    document.body.classList.add('media-standalone__body');
+    this.setState({ media, index });
+  }
+
+  handleOpenVideo = (video, time) => {
+    const media = ImmutableList([video]);
+
+    document.body.classList.add('media-standalone__body');
+    this.setState({ media, time });
+  }
+
+  handleCloseMedia = () => {
+    document.body.classList.remove('media-standalone__body');
+    this.setState({ media: null, index: null, time: null });
+  }
+
+  render () {
+    const { locale, components } = this.props;
+
+    return (
+      <IntlProvider locale={locale} messages={messages}>
+        <Fragment>
+          {[].map.call(components, (component, i) => {
+            const componentName = component.getAttribute('data-component');
+            const Component = MEDIA_COMPONENTS[componentName];
+            const { media, card, ...props } = JSON.parse(component.getAttribute('data-props'));
+
+            Object.assign(props, {
+              ...(media ? { media: fromJS(media) } : {}),
+              ...(card  ? { card:  fromJS(card)  } : {}),
+
+              ...(componentName === 'Video' ? {
+                onOpenVideo: this.handleOpenVideo,
+              } : {
+                onOpenMedia: this.handleOpenMedia,
+              }),
+            });
+
+            return ReactDOM.createPortal(
+              <Component {...props} key={`media-${i}`} />,
+              component,
+            );
+          })}
+          <ModalRoot onClose={this.handleCloseMedia}>
+            {this.state.media && (
+              <MediaModal
+                media={this.state.media}
+                index={this.state.index || 0}
+                time={this.state.time}
+                onClose={this.handleCloseMedia}
+              />
+            )}
+          </ModalRoot>
+        </Fragment>
+      </IntlProvider>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/containers/media_galleries_container.js b/app/javascript/mastodon/containers/media_galleries_container.js
deleted file mode 100644
index d77bd688b..000000000
--- a/app/javascript/mastodon/containers/media_galleries_container.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import PropTypes from 'prop-types';
-import { IntlProvider, addLocaleData } from 'react-intl';
-import { getLocale } from '../locales';
-import MediaGallery from '../components/media_gallery';
-import ModalRoot from '../components/modal_root';
-import MediaModal from '../features/ui/components/media_modal';
-import { fromJS } from 'immutable';
-
-const { localeData, messages } = getLocale();
-addLocaleData(localeData);
-
-export default class MediaGalleriesContainer extends React.PureComponent {
-
-  static propTypes = {
-    locale: PropTypes.string.isRequired,
-    galleries: PropTypes.object.isRequired,
-  };
-
-  state = {
-    media: null,
-    index: null,
-  };
-
-  handleOpenMedia = (media, index) => {
-    document.body.classList.add('media-gallery-standalone__body');
-    this.setState({ media, index });
-  }
-
-  handleCloseMedia = () => {
-    document.body.classList.remove('media-gallery-standalone__body');
-    this.setState({ media: null, index: null });
-  }
-
-  render () {
-    const { locale, galleries } = this.props;
-
-    return (
-      <IntlProvider locale={locale} messages={messages}>
-        <React.Fragment>
-          {[].map.call(galleries, gallery => {
-            const { media, ...props } = JSON.parse(gallery.getAttribute('data-props'));
-
-            return ReactDOM.createPortal(
-              <MediaGallery
-                {...props}
-                media={fromJS(media)}
-                onOpenMedia={this.handleOpenMedia}
-              />,
-              gallery
-            );
-          })}
-          <ModalRoot onClose={this.handleCloseMedia}>
-            {this.state.media === null || this.state.index === null ? null : (
-              <MediaModal
-                media={this.state.media}
-                index={this.state.index}
-                onClose={this.handleCloseMedia}
-              />
-            )}
-          </ModalRoot>
-        </React.Fragment>
-      </IntlProvider>
-    );
-  }
-
-}
diff --git a/app/javascript/mastodon/containers/video_container.js b/app/javascript/mastodon/containers/video_container.js
deleted file mode 100644
index 2fd353096..000000000
--- a/app/javascript/mastodon/containers/video_container.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { IntlProvider, addLocaleData } from 'react-intl';
-import { getLocale } from '../locales';
-import Video from '../features/video';
-
-const { localeData, messages } = getLocale();
-addLocaleData(localeData);
-
-export default class VideoContainer extends React.PureComponent {
-
-  static propTypes = {
-    locale: PropTypes.string.isRequired,
-  };
-
-  render () {
-    const { locale, ...props } = this.props;
-
-    return (
-      <IntlProvider locale={locale} messages={messages}>
-        <Video {...props} />
-      </IntlProvider>
-    );
-  }
-
-}
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index b5f516032..417719004 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -34,8 +34,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
     e.stopPropagation();
   }
 
-  handleOpenVideo = startTime => {
-    this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime);
+  handleOpenVideo = (media, startTime) => {
+    this.props.onOpenVideo(media, startTime);
   }
 
   handleExpandedToggle = () => {
diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js
index fb76270fa..f4d6b5c4e 100644
--- a/app/javascript/mastodon/features/ui/components/media_modal.js
+++ b/app/javascript/mastodon/features/ui/components/media_modal.js
@@ -2,6 +2,7 @@ import React from 'react';
 import ReactSwipeableViews from 'react-swipeable-views';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
+import Video from '../../video';
 import ExtendedVideoPlayer from '../../../components/extended_video_player';
 import classNames from 'classnames';
 import { defineMessages, injectIntl } from 'react-intl';
@@ -112,6 +113,22 @@ export default class MediaModal extends ImmutablePureComponent {
             onClick={this.toggleNavigation}
           />
         );
+      } else if (image.get('type') === 'video') {
+        const { time } = this.props;
+
+        return (
+          <Video
+            preview={image.get('preview_url')}
+            src={image.get('url')}
+            width={image.get('width')}
+            height={image.get('height')}
+            startTime={time || 0}
+            onCloseVideo={onClose}
+            detailed
+            description={image.get('description')}
+            key={image.get('url')}
+          />
+        );
       } else if (image.get('type') === 'gifv') {
         return (
           <ExtendedVideoPlayer
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
index 98ebcb6f9..47a165e16 100644
--- a/app/javascript/mastodon/features/video/index.js
+++ b/app/javascript/mastodon/features/video/index.js
@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { fromJS } from 'immutable';
 import { throttle } from 'lodash';
 import classNames from 'classnames';
 import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
@@ -131,6 +132,8 @@ export default class Video extends React.PureComponent {
     this.seek = c;
   }
 
+  handleClickRoot = e => e.stopPropagation();
+
   handlePlay = () => {
     this.setState({ paused: false });
   }
@@ -244,8 +247,17 @@ export default class Video extends React.PureComponent {
   }
 
   handleOpenVideo = () => {
+    const { src, preview, width, height } = this.props;
+    const media = fromJS({
+      type: 'video',
+      url: src,
+      preview_url: preview,
+      width,
+      height,
+    });
+
     this.video.pause();
-    this.props.onOpenVideo(this.video.currentTime);
+    this.props.onOpenVideo(media, this.video.currentTime);
   }
 
   handleCloseVideo = () => {
@@ -270,7 +282,16 @@ export default class Video extends React.PureComponent {
     }
 
     return (
-      <div className={classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen })} style={playerStyle} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
+      <div
+        role='menuitem'
+        className={classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen })}
+        style={playerStyle}
+        ref={this.setPlayerRef}
+        onMouseEnter={this.handleMouseEnter}
+        onMouseLeave={this.handleMouseLeave}
+        onClick={this.handleClickRoot}
+        tabIndex={0}
+      >
         <video
           ref={this.setVideoRef}
           src={src}
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 74bf97361..3253e21d3 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -1870,4 +1870,4 @@
     ],
     "path": "app/javascript/mastodon/features/video/index.json"
   }
-]
+]
\ No newline at end of file
diff --git a/app/javascript/mastodon/service_worker/entry.js b/app/javascript/mastodon/service_worker/entry.js
index ba54ae996..5ad03caf2 100644
--- a/app/javascript/mastodon/service_worker/entry.js
+++ b/app/javascript/mastodon/service_worker/entry.js
@@ -28,11 +28,10 @@ self.addEventListener('fetch', function(event) {
     const asyncResponse = fetchRoot();
     const asyncCache = openWebCache();
 
-    event.respondWith(asyncResponse.then(async response => {
+    event.respondWith(asyncResponse.then(response => {
       if (response.ok) {
-        const cache = await asyncCache;
-        await cache.put('/', response);
-        return response.clone();
+        return asyncCache.then(cache => cache.put('/', response))
+                         .then(() => response.clone());
       }
 
       throw null;
@@ -41,35 +40,38 @@ self.addEventListener('fetch', function(event) {
     const asyncResponse = fetch(event.request);
     const asyncCache = openWebCache();
 
-    event.respondWith(asyncResponse.then(async response => {
+    event.respondWith(asyncResponse.then(response => {
       if (response.ok || response.type === 'opaqueredirect') {
-        await Promise.all([
+        return Promise.all([
           asyncCache.then(cache => cache.delete('/')),
           indexedDB.deleteDatabase('mastodon'),
-        ]);
+        ]).then(() => response);
       }
 
       return response;
     }));
   } else if (process.env.CDN_HOST ? url.host === process.env.CDN_HOST : url.pathname.startsWith('/system/')) {
-    event.respondWith(openSystemCache().then(async cache => {
-      const cached = await cache.match(event.request.url);
+    event.respondWith(openSystemCache().then(cache => {
+      return cache.match(event.request.url).then(cached => {
+        if (cached === undefined) {
+          return fetch(event.request).then(fetched => {
+            if (fetched.ok) {
+              const put = cache.put(event.request.url, fetched.clone());
 
-      if (cached === undefined) {
-        const fetched = await fetch(event.request);
+              put.catch(() => freeStorage());
 
-        if (fetched.ok) {
-          try {
-            await cache.put(event.request.url, fetched.clone());
-          } finally {
-            freeStorage();
-          }
-        }
+              return put.then(() => {
+                freeStorage();
+                return fetched;
+              });
+            }
 
-        return fetched;
-      }
+            return fetched;
+          });
+        }
 
-      return cached;
+        return cached;
+      });
     }));
   }
 });
diff --git a/app/javascript/mastodon/storage/modifier.js b/app/javascript/mastodon/storage/modifier.js
index c2ed6f807..db1d16058 100644
--- a/app/javascript/mastodon/storage/modifier.js
+++ b/app/javascript/mastodon/storage/modifier.js
@@ -182,7 +182,9 @@ export function putStatuses(records) {
 }
 
 export function freeStorage() {
-  return navigator.storage.estimate().then(({ quota, usage }) => {
+  // navigator.storage is not present on:
+  // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.100 Safari/537.36 Edge/16.16299
+  return 'storage' in navigator && navigator.storage.estimate().then(({ quota, usage }) => {
     if (usage + storageMargin < quota) {
       return null;
     }
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index 38aaf895f..1e6ee62af 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -6,7 +6,6 @@ function main() {
   const emojify = require('../mastodon/features/emoji/emoji').default;
   const { getLocale } = require('../mastodon/locales');
   const { localeData } = getLocale();
-  const VideoContainer = require('../mastodon/containers/video_container').default;
   const React = require('react');
   const ReactDOM = require('react-dom');
 
@@ -51,30 +50,16 @@ function main() {
       });
     });
 
-    [].forEach.call(document.querySelectorAll('[data-component="Video"]'), (content) => {
-      const props = JSON.parse(content.getAttribute('data-props'));
-      ReactDOM.render(<VideoContainer locale={locale} {...props} />, content);
-    });
-
-    const cards = document.querySelectorAll('[data-component="Card"]');
-
-    if (cards.length > 0) {
-      import(/* webpackChunkName: "containers/cards_container" */ '../mastodon/containers/cards_container').then(({ default: CardsContainer }) => {
-        const content = document.createElement('div');
-
-        ReactDOM.render(<CardsContainer locale={locale} cards={cards} />, content);
-        document.body.appendChild(content);
-      }).catch(error => console.error(error));
-    }
-
-    const mediaGalleries = document.querySelectorAll('[data-component="MediaGallery"]');
-
-    if (mediaGalleries.length > 0) {
-      const MediaGalleriesContainer = require('../mastodon/containers/media_galleries_container').default;
-      const content = document.createElement('div');
+    const reactComponents = document.querySelectorAll('[data-component]');
+    if (reactComponents.length > 0) {
+      import(/* webpackChunkName: "containers/media_container" */ '../mastodon/containers/media_container')
+        .then(({ default: MediaContainer }) => {
+          const content = document.createElement('div');
 
-      ReactDOM.render(<MediaGalleriesContainer locale={locale} galleries={mediaGalleries} />, content);
-      document.body.appendChild(content);
+          ReactDOM.render(<MediaContainer locale={locale} components={reactComponents} />, content);
+          document.body.appendChild(content);
+        })
+        .catch(error => console.error(error));
     }
   });
 }
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 1948a2a23..560b11ddf 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -484,19 +484,12 @@
 }
 
 a.name-tag,
-.name-tag {
-  display: flex;
-  align-items: center;
+.name-tag,
+a.inline-name-tag,
+.inline-name-tag {
   text-decoration: none;
   color: $secondary-text-color;
 
-  .avatar {
-    display: block;
-    margin: 0;
-    margin-right: 5px;
-    border-radius: 50%;
-  }
-
   .username {
     font-weight: 500;
   }
@@ -514,6 +507,26 @@ a.name-tag,
   }
 }
 
+a.name-tag,
+.name-tag {
+  display: flex;
+  align-items: center;
+
+  .avatar {
+    display: block;
+    margin: 0;
+    margin-right: 5px;
+    border-radius: 50%;
+  }
+
+  &.suspended {
+    .avatar {
+      filter: grayscale(100%);
+      opacity: 0.8;
+    }
+  }
+}
+
 .speech-bubble {
   margin-bottom: 20px;
   border-left: 4px solid $ui-highlight-color;
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 70ef96aa7..9f0a7a058 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -4432,6 +4432,10 @@ a.status-card {
   max-width: 100%;
   border-radius: 4px;
 
+  &:focus {
+    outline: 0;
+  }
+
   video {
     max-width: 100vw;
     max-height: 80vh;
diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss
index c40b38a5a..ac648c868 100644
--- a/app/javascript/styles/mastodon/containers.scss
+++ b/app/javascript/styles/mastodon/containers.scss
@@ -60,8 +60,7 @@
   }
 }
 
-.card-standalone__body,
-.media-gallery-standalone__body {
+.media-standalone__body {
   overflow: hidden;
 }
 
diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index fa876e603..982bfd990 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -1,3 +1,9 @@
+@keyframes Swag {
+  0% { background-position: 0% 0%; }
+  50% { background-position: 100% 0%; }
+  100% { background-position: 200% 0%; }
+}
+
 .table {
   width: 100%;
   max-width: 100%;
@@ -187,6 +193,11 @@ a.table-action-link {
 
     strong {
       font-weight: 700;
+      background: linear-gradient(to right, orange , yellow, green, cyan, blue, violet,orange , yellow, green, cyan, blue, violet);
+      background-size: 200% 100%;
+      background-clip: text;
+      color: transparent;
+      animation: Swag 2s linear 0s infinite;
     }
   }
 }
diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb
index 7e146ea8c..f810c88a2 100644
--- a/app/lib/activitypub/activity/announce.rb
+++ b/app/lib/activitypub/activity/announce.rb
@@ -30,7 +30,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
     if object_uri.start_with?('http')
       return if ActivityPub::TagManager.instance.local_uri?(object_uri)
 
-      ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true)
+      ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true, on_behalf_of: @account.followers.local.first)
     elsif @object['url'].present?
       ::FetchRemoteStatusService.new.call(@object['url'])
     end
diff --git a/app/models/concerns/status_threading_concern.rb b/app/models/concerns/status_threading_concern.rb
index 8e817be00..1ba8fc693 100644
--- a/app/models/concerns/status_threading_concern.rb
+++ b/app/models/concerns/status_threading_concern.rb
@@ -74,16 +74,7 @@ module StatusThreadingConcern
     statuses    = statuses_with_accounts(ids).to_a
     account_ids = statuses.map(&:account_id).uniq
     domains     = statuses.map(&:account_domain).compact.uniq
-
-    relations = if account.present?
-                  {
-                    blocking: Account.blocking_map(account_ids, account.id),
-                    blocked_by: Account.blocked_by_map(account_ids, account.id),
-                    muting: Account.muting_map(account_ids, account.id),
-                    following: Account.following_map(account_ids, account.id),
-                    domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id),
-                  }
-                end
+    relations   = relations_map_for_account(account, account_ids, domains)
 
     statuses.reject! { |status| filter_from_context?(status, account, relations) }
 
@@ -91,6 +82,18 @@ module StatusThreadingConcern
     statuses.sort_by! { |status| ids.index(status.id) }
   end
 
+  def relations_map_for_account(account, account_ids, domains)
+    return {} if account.nil?
+
+    {
+      blocking: Account.blocking_map(account_ids, account.id),
+      blocked_by: Account.blocked_by_map(account_ids, account.id),
+      muting: Account.muting_map(account_ids, account.id),
+      following: Account.following_map(account_ids, account.id),
+      domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id),
+    }
+  end
+
   def statuses_with_accounts(ids)
     Status.where(id: ids).includes(:account)
   end
diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb
index b6c00a9e7..2b447abb3 100644
--- a/app/services/activitypub/fetch_remote_status_service.rb
+++ b/app/services/activitypub/fetch_remote_status_service.rb
@@ -4,9 +4,9 @@ class ActivityPub::FetchRemoteStatusService < BaseService
   include JsonLdHelper
 
   # Should be called when uri has already been checked for locality
-  def call(uri, id: true, prefetched_body: nil)
+  def call(uri, id: true, prefetched_body: nil, on_behalf_of: nil)
     @json = if prefetched_body.nil?
-              fetch_resource(uri, id)
+              fetch_resource(uri, id, on_behalf_of)
             else
               body_to_json(prefetched_body)
             end
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 6eb233f9d..b1d5bd3a7 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -22,7 +22,7 @@ class PostStatusService < BaseService
     media  = validate_media!(options[:media_ids])
     status = nil
     text   = options.delete(:spoiler_text) if text.blank? && options[:spoiler_text].present?
-    text   = '.' if text.blank? && !media.empty?
+    text   = '.' if text.blank? && media.present?
 
     ApplicationRecord.transaction do
       status = account.statuses.create!(text: text,
diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml
index 9057e6048..5e174f312 100644
--- a/app/views/admin/reports/_status.html.haml
+++ b/app/views/admin/reports/_status.html.haml
@@ -3,26 +3,30 @@
     = f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id
   .batch-table__row__content
     .status__content><
-      - unless status.spoiler_text.blank?
+      - unless status.proper.spoiler_text.blank?
         %p><
-          %strong= Formatter.instance.format_spoiler(status)
+          %strong> Content warning: #{Formatter.instance.format_spoiler(status.proper)}
 
-      = Formatter.instance.format(status, custom_emojify: true)
+      = Formatter.instance.format(status.proper, custom_emojify: true)
 
-    - unless status.media_attachments.empty?
-      - if status.media_attachments.first.video?
-        - video = status.media_attachments.first
-        = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 610, height: 343, inline: true
+    - unless status.proper.media_attachments.empty?
+      - if status.proper.media_attachments.first.video?
+        - video = status.proper.media_attachments.first
+        = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.proper.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 610, height: 343, inline: true
       - else
-        = react_component :media_gallery, height: 343, sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }
+        = react_component :media_gallery, height: 343, sensitive: status.proper.sensitive? && !current_account&.user&.setting_display_sensitive_media, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }
 
     .detailed-status__meta
       = link_to TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener' do
         %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
       ·
-      = fa_visibility_icon(status)
-      = t("statuses.visibilities.#{status.visibility}")
-      - if status.sensitive?
+      - if status.reblog?
+        = fa_icon('retweet fw')
+        = t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(status.proper.account))
+      - else
+        = fa_visibility_icon(status)
+        = t("statuses.visibilities.#{status.visibility}")
+      - if status.proper.sensitive?
         ·
         = fa_icon('eye-slash fw')
         = t('stream_entries.sensitive_content')