about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorThibaut Girka <thib@sitedethib.com>2018-05-21 16:40:02 +0200
committerThibaut Girka <thib@sitedethib.com>2018-05-21 16:40:02 +0200
commita4c9bda771c04526005f708dfb9ca2ccced60d52 (patch)
tree9ed60f78e2e8acc823bccb7c75ecaa3fdb1b7ab6 /app
parent98ecadbf99aa823164608f796a40ab1cd067bdc2 (diff)
parent46061dc041b0a2a4a3907976cc3432abdb1d67ec (diff)
Merge branch 'master' into glitch-soc/merge-upstream
Conflicts:
	app/javascript/styles/mastodon-light.scss
	config/locales/en.yml
	config/locales/fr.yml
	config/locales/simple_form.pl.yml
	config/themes.yml

Conflicts resolved by deleting config/themes.yml,
marking app/javascript/styles/mastodon-light.scss as added,
and taking all new translation strings, not removing anything from
them.
Diffstat (limited to 'app')
-rw-r--r--app/controllers/api/v1/statuses_controller.rb10
-rw-r--r--app/javascript/mastodon/actions/streaming.js13
-rw-r--r--app/javascript/mastodon/actions/timelines.js16
-rw-r--r--app/javascript/mastodon/components/media_gallery.js9
-rw-r--r--app/javascript/mastodon/features/community_timeline/index.js34
-rw-r--r--app/javascript/mastodon/features/public_timeline/index.js34
-rw-r--r--app/javascript/mastodon/features/ui/index.js4
-rw-r--r--app/javascript/mastodon/locales/ar.json2
-rw-r--r--app/javascript/mastodon/locales/bg.json2
-rw-r--r--app/javascript/mastodon/locales/ca.json2
-rw-r--r--app/javascript/mastodon/locales/co.json2
-rw-r--r--app/javascript/mastodon/locales/de.json2
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json16
-rw-r--r--app/javascript/mastodon/locales/el.json2
-rw-r--r--app/javascript/mastodon/locales/en.json2
-rw-r--r--app/javascript/mastodon/locales/eo.json2
-rw-r--r--app/javascript/mastodon/locales/es.json2
-rw-r--r--app/javascript/mastodon/locales/eu.json2
-rw-r--r--app/javascript/mastodon/locales/fa.json2
-rw-r--r--app/javascript/mastodon/locales/fi.json2
-rw-r--r--app/javascript/mastodon/locales/fr.json2
-rw-r--r--app/javascript/mastodon/locales/gl.json2
-rw-r--r--app/javascript/mastodon/locales/he.json2
-rw-r--r--app/javascript/mastodon/locales/hr.json2
-rw-r--r--app/javascript/mastodon/locales/hu.json2
-rw-r--r--app/javascript/mastodon/locales/hy.json2
-rw-r--r--app/javascript/mastodon/locales/id.json2
-rw-r--r--app/javascript/mastodon/locales/io.json2
-rw-r--r--app/javascript/mastodon/locales/it.json2
-rw-r--r--app/javascript/mastodon/locales/ja.json2
-rw-r--r--app/javascript/mastodon/locales/ko.json2
-rw-r--r--app/javascript/mastodon/locales/nl.json2
-rw-r--r--app/javascript/mastodon/locales/no.json2
-rw-r--r--app/javascript/mastodon/locales/oc.json14
-rw-r--r--app/javascript/mastodon/locales/pl.json6
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json2
-rw-r--r--app/javascript/mastodon/locales/pt.json2
-rw-r--r--app/javascript/mastodon/locales/ru.json2
-rw-r--r--app/javascript/mastodon/locales/sk.json2
-rw-r--r--app/javascript/mastodon/locales/sl.json2
-rw-r--r--app/javascript/mastodon/locales/sr-Latn.json2
-rw-r--r--app/javascript/mastodon/locales/sr.json2
-rw-r--r--app/javascript/mastodon/locales/sv.json2
-rw-r--r--app/javascript/mastodon/locales/te.json2
-rw-r--r--app/javascript/mastodon/locales/th.json2
-rw-r--r--app/javascript/mastodon/locales/tr.json2
-rw-r--r--app/javascript/mastodon/locales/uk.json2
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json76
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json2
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json2
-rw-r--r--app/javascript/mastodon/service_worker/web_push_notifications.js10
-rw-r--r--app/javascript/styles/mastodon/accounts.scss2
-rw-r--r--app/javascript/styles/mastodon/components.scss2
-rw-r--r--app/services/batched_remove_status_service.rb5
-rw-r--r--app/services/fan_out_on_write_service.rb8
-rw-r--r--app/services/remove_status_service.rb8
56 files changed, 256 insertions, 89 deletions
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index 01880565c..289d91045 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -10,6 +10,12 @@ class Api::V1::StatusesController < Api::BaseController
 
   respond_to :json
 
+  # This API was originally unlimited, pagination cannot be introduced without
+  # breaking backwards-compatibility. Arbitrarily high number to cover most
+  # conversations as quasi-unlimited, it would be too much work to render more
+  # than this anyway
+  CONTEXT_LIMIT = 4_096
+
   def show
     cached  = Rails.cache.read(@status.cache_key)
     @status = cached unless cached.nil?
@@ -17,8 +23,8 @@ class Api::V1::StatusesController < Api::BaseController
   end
 
   def context
-    ancestors_results   = @status.in_reply_to_id.nil? ? [] : @status.ancestors(DEFAULT_STATUSES_LIMIT, current_account)
-    descendants_results = @status.descendants(DEFAULT_STATUSES_LIMIT, current_account)
+    ancestors_results   = @status.in_reply_to_id.nil? ? [] : @status.ancestors(CONTEXT_LIMIT, current_account)
+    descendants_results = @status.descendants(CONTEXT_LIMIT, current_account)
     loaded_ancestors    = cache_collection(ancestors_results, Status)
     loaded_descendants  = cache_collection(descendants_results, Status)
 
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index 10e68bf3a..f56853bff 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -40,10 +40,9 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
   dispatch(expandHomeTimeline({}, () => dispatch(expandNotifications({}, done))));
 };
 
-export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
-export const connectCommunityStream = () => connectTimelineStream('community', 'public:local');
-export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
-export const connectPublicStream = () => connectTimelineStream('public', 'public');
-export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
-export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
-export const connectListStream = (id) => connectTimelineStream(`list:${id}`, `list&list=${id}`);
+export const connectUserStream      = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
+export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
+export const connectPublicStream    = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`);
+export const connectHashtagStream   = tag => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
+export const connectDirectStream    = () => connectTimelineStream('direct', 'direct');
+export const connectListStream      = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index 8bcfe4db9..8f54dfd8a 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -93,15 +93,15 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
   };
 };
 
-export const expandHomeTimeline         = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
-export const expandPublicTimeline       = ({ maxId } = {}, done = noOp) => expandTimeline('public', '/api/v1/timelines/public', { max_id: maxId }, done);
-export const expandCommunityTimeline    = ({ maxId } = {}, done = noOp) => expandTimeline('community', '/api/v1/timelines/public', { local: true, max_id: maxId }, done);
-export const expandDirectTimeline       = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
-export const expandAccountTimeline      = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
+export const expandHomeTimeline            = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
+export const expandPublicTimeline          = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
+export const expandCommunityTimeline       = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
+export const expandDirectTimeline          = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
+export const expandAccountTimeline         = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
 export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
-export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true });
-export const expandHashtagTimeline      = (hashtag, { maxId } = {}, done = noOp) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }, done);
-export const expandListTimeline         = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
+export const expandAccountMediaTimeline    = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true });
+export const expandHashtagTimeline         = (hashtag, { maxId } = {}, done = noOp) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }, done);
+export const expandListTimeline            = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
 
 export function expandTimelineRequest(timeline) {
   return {
diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js
index 13e1fcc52..7c4444e0e 100644
--- a/app/javascript/mastodon/components/media_gallery.js
+++ b/app/javascript/mastodon/components/media_gallery.js
@@ -20,6 +20,7 @@ class Item extends React.PureComponent {
     index: PropTypes.number.isRequired,
     size: PropTypes.number.isRequired,
     onClick: PropTypes.func.isRequired,
+    displayWidth: PropTypes.number,
   };
 
   static defaultProps = {
@@ -58,7 +59,7 @@ class Item extends React.PureComponent {
   }
 
   render () {
-    const { attachment, index, size, standalone } = this.props;
+    const { attachment, index, size, standalone, displayWidth } = this.props;
 
     let width  = 50;
     let height = 100;
@@ -121,7 +122,7 @@ class Item extends React.PureComponent {
       const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
 
       const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null;
-      const sizes  = hasSize ? `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw` : null;
+      const sizes  = hasSize ? `${displayWidth * (width / 100)}px` : null;
 
       const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
       const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
@@ -263,9 +264,9 @@ export default class MediaGallery extends React.PureComponent {
       const size = media.take(4).size;
 
       if (this.isStandaloneEligible()) {
-        children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} />;
+        children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} />;
       } else {
-        children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} />);
+        children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} />);
       }
     }
 
diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js
index 870474ed5..8ad114407 100644
--- a/app/javascript/mastodon/features/community_timeline/index.js
+++ b/app/javascript/mastodon/features/community_timeline/index.js
@@ -1,12 +1,13 @@
 import React from 'react';
 import { connect } from 'react-redux';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { NavLink } from 'react-router-dom';
 import PropTypes from 'prop-types';
 import StatusListContainer from '../ui/containers/status_list_container';
 import Column from '../../components/column';
 import ColumnHeader from '../../components/column_header';
 import { expandCommunityTimeline } from '../../actions/timelines';
 import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
 import { connectCommunityStream } from '../../actions/streaming';
 
@@ -14,20 +15,25 @@ const messages = defineMessages({
   title: { id: 'column.community', defaultMessage: 'Local timeline' },
 });
 
-const mapStateToProps = state => ({
-  hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
+const mapStateToProps = (state, { onlyMedia }) => ({
+  hasUnread: state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
 });
 
 @connect(mapStateToProps)
 @injectIntl
 export default class CommunityTimeline extends React.PureComponent {
 
+  static defaultProps = {
+    onlyMedia: false,
+  };
+
   static propTypes = {
     dispatch: PropTypes.func.isRequired,
     columnId: PropTypes.string,
     intl: PropTypes.object.isRequired,
     hasUnread: PropTypes.bool,
     multiColumn: PropTypes.bool,
+    onlyMedia: PropTypes.bool,
   };
 
   handlePin = () => {
@@ -50,10 +56,10 @@ export default class CommunityTimeline extends React.PureComponent {
   }
 
   componentDidMount () {
-    const { dispatch } = this.props;
+    const { dispatch, onlyMedia } = this.props;
 
-    dispatch(expandCommunityTimeline());
-    this.disconnect = dispatch(connectCommunityStream());
+    dispatch(expandCommunityTimeline({ onlyMedia }));
+    this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
   }
 
   componentWillUnmount () {
@@ -68,13 +74,22 @@ export default class CommunityTimeline extends React.PureComponent {
   }
 
   handleLoadMore = maxId => {
-    this.props.dispatch(expandCommunityTimeline({ maxId }));
+    const { dispatch, onlyMedia } = this.props;
+
+    dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
   }
 
   render () {
-    const { intl, hasUnread, columnId, multiColumn } = this.props;
+    const { intl, hasUnread, columnId, multiColumn, onlyMedia } = this.props;
     const pinned = !!columnId;
 
+    const headline = (
+      <div className='community-timeline__section-headline'>
+        <NavLink exact to='/timelines/public/local' replace><FormattedMessage id='timeline.posts' defaultMessage='Toots' /></NavLink>
+        <NavLink exact to='/timelines/public/local/media' replace><FormattedMessage id='timeline.media' defaultMessage='Media' /></NavLink>
+      </div>
+    );
+
     return (
       <Column ref={this.setRef}>
         <ColumnHeader
@@ -91,9 +106,10 @@ export default class CommunityTimeline extends React.PureComponent {
         </ColumnHeader>
 
         <StatusListContainer
+          prepend={headline}
           trackScroll={!pinned}
           scrollKey={`community_timeline-${columnId}`}
-          timelineId='community'
+          timelineId={`community${onlyMedia ? ':media' : ''}`}
           onLoadMore={this.handleLoadMore}
           emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
         />
diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js
index 5a88f7601..54904c5cf 100644
--- a/app/javascript/mastodon/features/public_timeline/index.js
+++ b/app/javascript/mastodon/features/public_timeline/index.js
@@ -1,12 +1,13 @@
 import React from 'react';
 import { connect } from 'react-redux';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { NavLink } from 'react-router-dom';
 import PropTypes from 'prop-types';
 import StatusListContainer from '../ui/containers/status_list_container';
 import Column from '../../components/column';
 import ColumnHeader from '../../components/column_header';
 import { expandPublicTimeline } from '../../actions/timelines';
 import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
 import { connectPublicStream } from '../../actions/streaming';
 
@@ -14,20 +15,25 @@ const messages = defineMessages({
   title: { id: 'column.public', defaultMessage: 'Federated timeline' },
 });
 
-const mapStateToProps = state => ({
-  hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0,
+const mapStateToProps = (state, { onlyMedia }) => ({
+  hasUnread: state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
 });
 
 @connect(mapStateToProps)
 @injectIntl
 export default class PublicTimeline extends React.PureComponent {
 
+  static defaultProps = {
+    onlyMedia: false,
+  };
+
   static propTypes = {
     dispatch: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
     columnId: PropTypes.string,
     multiColumn: PropTypes.bool,
     hasUnread: PropTypes.bool,
+    onlyMedia: PropTypes.bool,
   };
 
   handlePin = () => {
@@ -50,10 +56,10 @@ export default class PublicTimeline extends React.PureComponent {
   }
 
   componentDidMount () {
-    const { dispatch } = this.props;
+    const { dispatch, onlyMedia } = this.props;
 
-    dispatch(expandPublicTimeline());
-    this.disconnect = dispatch(connectPublicStream());
+    dispatch(expandPublicTimeline({ onlyMedia }));
+    this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
   }
 
   componentWillUnmount () {
@@ -68,13 +74,22 @@ export default class PublicTimeline extends React.PureComponent {
   }
 
   handleLoadMore = maxId => {
-    this.props.dispatch(expandPublicTimeline({ maxId }));
+    const { dispatch, onlyMedia } = this.props;
+
+    dispatch(expandPublicTimeline({ maxId, onlyMedia }));
   }
 
   render () {
-    const { intl, columnId, hasUnread, multiColumn } = this.props;
+    const { intl, columnId, hasUnread, multiColumn, onlyMedia } = this.props;
     const pinned = !!columnId;
 
+    const headline = (
+      <div className='public-timeline__section-headline'>
+        <NavLink exact to='/timelines/public' replace><FormattedMessage id='timeline.posts' defaultMessage='Toots' /></NavLink>
+        <NavLink exact to='/timelines/public/media' replace><FormattedMessage id='timeline.media' defaultMessage='Media' /></NavLink>
+      </div>
+    );
+
     return (
       <Column ref={this.setRef}>
         <ColumnHeader
@@ -91,7 +106,8 @@ export default class PublicTimeline extends React.PureComponent {
         </ColumnHeader>
 
         <StatusListContainer
-          timelineId='public'
+          prepend={headline}
+          timelineId={`public${onlyMedia ? ':media' : ''}`}
           onLoadMore={this.handleLoadMore}
           trackScroll={!pinned}
           scrollKey={`public_timeline-${columnId}`}
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index adca0d617..adb856b92 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -141,7 +141,9 @@ class SwitchingColumnsArea extends React.PureComponent {
           <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
           <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} />
           <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} />
-          <WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} />
+          <WrappedRoute path='/timelines/public/media' component={PublicTimeline} content={children} componentParams={{ onlyMedia: true }} />
+          <WrappedRoute path='/timelines/public/local' exact component={CommunityTimeline} content={children} />
+          <WrappedRoute path='/timelines/public/local/media' component={CommunityTimeline} content={children} componentParams={{ onlyMedia: true }} />
           <WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} />
           <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
           <WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} />
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 9827ac5f0..c8e37a366 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "المحلي",
   "tabs_bar.notifications": "الإخطارات",
   "tabs_bar.search": "البحث",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.",
   "upload_area.title": "إسحب ثم أفلت للرفع",
   "upload_button.label": "إضافة وسائط",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index caba7703d..9ac5aad9e 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Известия",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
   "upload_button.label": "Добави медия",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 7181589ec..8fd42ca34 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificacions",
   "tabs_bar.search": "Cerca",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "El vostre esborrany es perdrà si sortiu de Mastodon.",
   "upload_area.title": "Arrossega i deixa anar per carregar",
   "upload_button.label": "Afegir multimèdia",
diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json
index c6de9d016..bc75813ea 100644
--- a/app/javascript/mastodon/locales/co.json
+++ b/app/javascript/mastodon/locales/co.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Lucale",
   "tabs_bar.notifications": "Nutificazione",
   "tabs_bar.search": "Cercà",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "A bruttacopia sarà persa s'ellu hè chjosu Mastodon.",
   "upload_area.title": "Drag & drop per caricà un fugliale",
   "upload_button.label": "Aghjunghje un media",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 4d89ea704..29756aff0 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Lokal",
   "tabs_bar.notifications": "Mitteilungen",
   "tabs_bar.search": "Suchen",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Dein Entwurf geht verloren, wenn du Mastodon verlässt.",
   "upload_area.title": "Zum Hochladen hereinziehen",
   "upload_button.label": "Mediendatei hinzufügen",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 1fec1249b..48dd3d1e7 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -597,6 +597,14 @@
         "id": "column.community"
       },
       {
+        "defaultMessage": "Toots",
+        "id": "timeline.posts"
+      },
+      {
+        "defaultMessage": "Media",
+        "id": "timeline.media"
+      },
+      {
         "defaultMessage": "The local timeline is empty. Write something publicly to get the ball rolling!",
         "id": "empty_column.community"
       }
@@ -1394,6 +1402,14 @@
         "id": "column.public"
       },
       {
+        "defaultMessage": "Toots",
+        "id": "timeline.posts"
+      },
+      {
+        "defaultMessage": "Media",
+        "id": "timeline.media"
+      },
+      {
         "defaultMessage": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
         "id": "empty_column.public"
       }
diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json
index c547b2e28..1e7707b4a 100644
--- a/app/javascript/mastodon/locales/el.json
+++ b/app/javascript/mastodon/locales/el.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notifications",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
   "upload_button.label": "Add media",
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 1d7960abe..690904236 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -284,6 +284,8 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notifications",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
   "upload_button.label": "Add media",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 2715b80c9..04d9dc8d5 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Loka tempolinio",
   "tabs_bar.notifications": "Sciigoj",
   "tabs_bar.search": "Serĉi",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Via malneto perdiĝos se vi eliras de Mastodon.",
   "upload_area.title": "Altreni kaj lasi por alŝuti",
   "upload_button.label": "Aldoni aŭdovidaĵon",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 668f437b9..6092630c4 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificaciones",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.",
   "upload_area.title": "Arrastra y suelta para subir",
   "upload_button.label": "Subir multimedia",
diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json
index 8ec140753..a6b1f8e83 100644
--- a/app/javascript/mastodon/locales/eu.json
+++ b/app/javascript/mastodon/locales/eu.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notifications",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
   "upload_button.label": "Add media",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 605961a01..c45277d82 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "محلی",
   "tabs_bar.notifications": "اعلان‌ها",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "اگر از ماستدون خارج شوید پیش‌نویس شما پاک خواهد شد.",
   "upload_area.title": "برای بارگذاری به این‌جا بکشید",
   "upload_button.label": "افزودن تصویر",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index ead1eb8c4..d04e0230a 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Paikallinen",
   "tabs_bar.notifications": "Ilmoitukset",
   "tabs_bar.search": "Hae",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Luonnos häviää, jos poistut Mastodonista.",
   "upload_area.title": "Lataa raahaamalla ja pudottamalla tähän",
   "upload_button.label": "Lisää mediaa",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index c05cb40c1..4c2bd39ca 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Fil public local",
   "tabs_bar.notifications": "Notifications",
   "tabs_bar.search": "Chercher",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Votre brouillon sera perdu si vous quittez Mastodon.",
   "upload_area.title": "Glissez et déposez pour envoyer",
   "upload_button.label": "Joindre un média",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index 90447ad4c..f2f0fc260 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificacións",
   "tabs_bar.search": "Buscar",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "O borrador perderase se sae de Mastodon.",
   "upload_area.title": "Arrastre e solte para subir",
   "upload_button.label": "Engadir medios",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index 5da3e9ebb..361beebcc 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "ציר זמן מקומי",
   "tabs_bar.notifications": "התראות",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "הטיוטא תאבד אם תעזבו את מסטודון.",
   "upload_area.title": "ניתן להעלות על ידי Drag & drop",
   "upload_button.label": "הוספת מדיה",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index 6486284ec..93dc1c17d 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Lokalno",
   "tabs_bar.notifications": "Notifikacije",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Povuci i spusti kako bi uploadao",
   "upload_button.label": "Dodaj media",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 6c04eced6..158c0fe41 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Értesítések",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "A piszkozata el fog vesztődni ha elhagyja Mastodon-t.",
   "upload_area.title": "Húzza ide a feltöltéshez",
   "upload_button.label": "Média hozzáadása",
diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json
index f1f4df968..76b3a9c12 100644
--- a/app/javascript/mastodon/locales/hy.json
+++ b/app/javascript/mastodon/locales/hy.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Տեղական",
   "tabs_bar.notifications": "Ծանուցումներ",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Քո սեւագիրը կկորի, եթե լքես Մաստոդոնը։",
   "upload_area.title": "Քաշիր ու նետիր՝ վերբեռնելու համար",
   "upload_button.label": "Ավելացնել մեդիա",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index e0fa959b0..00240530d 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Lokal",
   "tabs_bar.notifications": "Notifikasi",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Naskah anda akan hilang jika anda keluar dari Mastodon.",
   "upload_area.title": "Seret & lepaskan untuk mengunggah",
   "upload_button.label": "Tambahkan media",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index 616232c3e..d20ee0f62 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Lokala",
   "tabs_bar.notifications": "Savigi",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Tranar faligar por kargar",
   "upload_button.label": "Adjuntar kontenajo",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 6bf9c73a6..485fe0fad 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Locale",
   "tabs_bar.notifications": "Notifiche",
   "tabs_bar.search": "Cerca",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "La bozza andrà persa se esci da Mastodon.",
   "upload_area.title": "Trascina per caricare",
   "upload_button.label": "Aggiungi file multimediale",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 0a6c5396e..488fb6020 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -284,6 +284,8 @@
   "tabs_bar.local_timeline": "ローカル",
   "tabs_bar.notifications": "通知",
   "tabs_bar.search": "検索",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。",
   "upload_area.title": "ドラッグ&ドロップでアップロード",
   "upload_button.label": "メディアを追加",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 66657abcd..edcf5f817 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "로컬",
   "tabs_bar.notifications": "알림",
   "tabs_bar.search": "검색",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "지금 나가면 저장되지 않은 항목을 잃게 됩니다.",
   "upload_area.title": "드래그 & 드롭으로 업로드",
   "upload_button.label": "미디어 추가",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 8ddf47f3f..1ead2011d 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Lokaal",
   "tabs_bar.notifications": "Meldingen",
   "tabs_bar.search": "Zoeken",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Je concept zal verloren gaan als je Mastodon verlaat.",
   "upload_area.title": "Hierin slepen om te uploaden",
   "upload_button.label": "Media toevoegen",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index ab755a3e8..d8de2ad4b 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Lokal",
   "tabs_bar.notifications": "Varslinger",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Din kladd vil bli forkastet om du forlater Mastodon.",
   "upload_area.title": "Dra og slipp for å laste opp",
   "upload_button.label": "Legg til media",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 9378cfa8f..e7bc148fa 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -61,9 +61,9 @@
   "column_subheading.navigation": "Navigacion",
   "column_subheading.settings": "Paramètres",
   "compose_form.direct_message_warning": "Aqueste tut serà pas que visibile pel monde mencionat.",
-  "compose_form.direct_message_warning_learn_more": "Learn more",
-  "compose_form.hashtag_warning": "Aqueste tut serà pas ligat a cap etiqueta estant qu’es pas listat. Òm pas cercar que los tuts publics per etiqueta.",
-  "compose_form.lock_disclaimer": "Vòstre compte es pas {locked}. Tot lo mond pòt vos sègre e veire los estatuts reservats als seguidors.",
+  "compose_form.direct_message_warning_learn_more": "Ne saber mai",
+  "compose_form.hashtag_warning": "Aqueste tut serà pas ligat a cap d’etiqueta estant qu’es pas listat. Òm pas cercar que los tuts publics per etiqueta.",
+  "compose_form.lock_disclaimer": "Vòstre compte es pas {locked}. Tot lo monde pòt vos sègre e veire los estatuts reservats als seguidors.",
   "compose_form.lock_disclaimer.lock": "clavat",
   "compose_form.placeholder": "A de qué pensatz ?",
   "compose_form.publish": "Tut",
@@ -187,11 +187,11 @@
   "notifications.column_settings.reblog": "Partatges :",
   "notifications.column_settings.show": "Mostrar dins la colomna",
   "notifications.column_settings.sound": "Emetre un son",
-  "notifications.group": "{count} notifications",
+  "notifications.group": "{count} notificacions",
   "onboarding.done": "Sortir",
   "onboarding.next": "Seguent",
   "onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de la gent que los de {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
-  "onboarding.page_four.home": "Lo flux d’acuèlh mòstra los estatuts del mond que seguètz.",
+  "onboarding.page_four.home": "Lo flux d’acuèlh mòstra los estatuts del monde que seguètz.",
   "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualqu’un interagís amb vos.",
   "onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per construire un malhum mai larg. Òm los apèla instàncias.",
   "onboarding.page_one.full_handle": "Vòstre escais-nom complèt",
@@ -206,7 +206,7 @@
   "onboarding.page_six.read_guidelines": "Mercés de legir la {guidelines} de {domain} !",
   "onboarding.page_six.various_app": "aplicacions per mobil",
   "onboarding.page_three.profile": "Modificatz vòstre perfil per cambiar vòstre avatar, bio e escais-nom. I a enlà totas las preferéncias.",
-  "onboarding.page_three.search": "Emplegatz la barra de recèrca per trobar de mond e engachatz las etiquetas coma {illustration} e {introductions}. Per trobar una persona d’una autra instància, picatz son identificant complèt.",
+  "onboarding.page_three.search": "Emplegatz la barra de recèrca per trobar de monde e engachatz las etiquetas coma {illustration} e {introductions}. Per trobar una persona d’una autra instància, picatz son identificant complèt.",
   "onboarding.page_two.compose": "Escrivètz un estatut dempuèi la colomna per compausar. Podètz mandar un imatge, cambiar la confidencialitat e ajustar un avertiment amb las icònas cai-jos.",
   "onboarding.skip": "Passar",
   "privacy.change": "Ajustar la confidencialitat del messatge",
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Flux public local",
   "tabs_bar.notifications": "Notificacions",
   "tabs_bar.search": "Recèrcas",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Vòstre brolhon serà perdut se quitatz Mastodon.",
   "upload_area.title": "Lisatz e depausatz per mandar",
   "upload_button.label": "Ajustar un mèdia",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index f3db8a125..486971c6d 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -64,7 +64,7 @@
   "column_subheading.navigation": "Nawigacja",
   "column_subheading.settings": "Ustawienia",
   "compose_form.direct_message_warning": "Ten wpis będzie widoczny tylko dla wszystkich wspomnianych użytkowników.",
-  "compose_form.direct_message_warning_learn_more": "Learn more",
+  "compose_form.direct_message_warning_learn_more": "Dowiedz się więcej",
   "compose_form.hashtag_warning": "Ten wpis nie będzie widoczny pod podanymi hashtagami, ponieważ jest oznaczony jako niewidoczny. Tylko publiczne wpisy mogą zostać znalezione z użyciem hashtagów.",
   "compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto Cię śledzi, może wyświetlać Twoje wpisy przeznaczone tylko dla śledzących.",
   "compose_form.lock_disclaimer.lock": "zablokowane",
@@ -191,7 +191,7 @@
   "notifications.column_settings.reblog": "Podbicia:",
   "notifications.column_settings.show": "Pokaż w kolumnie",
   "notifications.column_settings.sound": "Odtwarzaj dźwięk",
-  "notifications.group": "{count} notifications",
+  "notifications.group": "{count, number} {count, plural, one {powiadomienie} few {powiadomienia} many {powiadomień} more {powiadomień}}",
   "onboarding.done": "Gotowe",
   "onboarding.next": "Dalej",
   "onboarding.page_five.public_timelines": "Lokalna oś czasu zawiera wszystkie publiczne wpisy z {domain}. Globalna oś czasu wyświetla publiczne wpisy śledzonych przez członków {domain}. Są to publiczne osie czasu – najlepszy sposób na poznanie nowych osób.",
@@ -284,6 +284,8 @@
   "tabs_bar.local_timeline": "Lokalne",
   "tabs_bar.notifications": "Powiadomienia",
   "tabs_bar.search": "Szukaj",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Mastodona.",
   "upload_area.title": "Przeciągnij i upuść aby wysłać",
   "upload_button.label": "Dodaj zawartość multimedialną",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 82bbd17a3..c69a90e62 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificações",
   "tabs_bar.search": "Buscar",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Seu rascunho será perdido se você sair do Mastodon.",
   "upload_area.title": "Arraste e solte para enviar",
   "upload_button.label": "Adicionar mídia",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index 6463d001d..18d66be7f 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificações",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "O teu rascunho vai ser perdido se abandonares o Mastodon.",
   "upload_area.title": "Arraste e solte para enviar",
   "upload_button.label": "Adicionar media",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 6c7926ffe..577ec9921 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Локальная",
   "tabs_bar.notifications": "Уведомления",
   "tabs_bar.search": "Поиск",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Ваш черновик будет утерян, если вы покинете Mastodon.",
   "upload_area.title": "Перетащите сюда, чтобы загрузить",
   "upload_button.label": "Добавить медиаконтент",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index 6ac646bee..b85ba1318 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Lokálna",
   "tabs_bar.notifications": "Notifikácie",
   "tabs_bar.search": "Hľadaj",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Čo máte rozpísané sa stratí, ak opustíte Mastodon.",
   "upload_area.title": "Ťahaj a pusti pre nahratie",
   "upload_button.label": "Pridať médiá",
diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json
index edd5ab62b..3cc7389ae 100644
--- a/app/javascript/mastodon/locales/sl.json
+++ b/app/javascript/mastodon/locales/sl.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Lokalno",
   "tabs_bar.notifications": "Obvestila",
   "tabs_bar.search": "Poišči",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Vaš osnutek bo izgubljen, če zapustite Mastodona.",
   "upload_area.title": "Povlecite in spustite za pošiljanje",
   "upload_button.label": "Dodaj medij",
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index 2a9d3737d..e2b55c179 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Lokalno",
   "tabs_bar.notifications": "Obaveštenja",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Ako napustite Mastodont, izgubićete napisani nacrt.",
   "upload_area.title": "Prevucite ovde da otpremite",
   "upload_button.label": "Dodaj multimediju",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index b53f1ecc9..09efb985c 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Локално",
   "tabs_bar.notifications": "Обавештења",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Ако напустите Мастодонт, изгубићете написани нацрт.",
   "upload_area.title": "Превуците овде да отпремите",
   "upload_button.label": "Додај мултимедију",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index dbe2c182e..6c396e525 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Lokal",
   "tabs_bar.notifications": "Meddelanden",
   "tabs_bar.search": "Sök",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Ditt utkast kommer att förloras om du lämnar Mastodon.",
   "upload_area.title": "Dra & släpp för att ladda upp",
   "upload_button.label": "Lägg till media",
diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json
index 52bd41935..ce0d4b9cc 100644
--- a/app/javascript/mastodon/locales/te.json
+++ b/app/javascript/mastodon/locales/te.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notifications",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
   "upload_button.label": "Add media",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 32aa24080..f0ba25ec3 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notifications",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
   "upload_button.label": "Add media",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index b73157e27..89f00fd3b 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Yerel",
   "tabs_bar.notifications": "Bildirimler",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Upload için sürükle bırak yapınız",
   "upload_button.label": "Görsel ekle",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index b301c6827..e81476541 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "Локальна",
   "tabs_bar.notifications": "Сповіщення",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Перетягніть сюди, щоб завантажити",
   "upload_button.label": "Додати медіаконтент",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 455f1f828..b920bc19e 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -1,11 +1,11 @@
 {
-  "account.badges.bot": "Bot",
+  "account.badges.bot": "机器人",
   "account.block": "屏蔽 @{name}",
   "account.block_domain": "隐藏来自 {domain} 的内容",
-  "account.blocked": "Blocked",
-  "account.direct": "Direct Message @{name}",
+  "account.blocked": "已屏蔽",
+  "account.direct": "发送私信给 @{name}",
   "account.disclaimer_full": "此处显示的信息可能不是全部内容。",
-  "account.domain_blocked": "Domain hidden",
+  "account.domain_blocked": "网站已屏蔽",
   "account.edit_profile": "修改个人资料",
   "account.follow": "关注",
   "account.followers": "关注者",
@@ -17,9 +17,9 @@
   "account.moved_to": "{name} 已经迁移到:",
   "account.mute": "隐藏 @{name}",
   "account.mute_notifications": "隐藏来自 @{name} 的通知",
-  "account.muted": "Muted",
+  "account.muted": "已隐藏",
   "account.posts": "嘟文",
-  "account.posts_with_replies": "Toots with replies",
+  "account.posts_with_replies": "嘟文和回复",
   "account.report": "举报 @{name}",
   "account.requested": "正在等待对方同意。点击以取消发送关注请求",
   "account.share": "分享 @{name} 的个人资料",
@@ -30,8 +30,8 @@
   "account.unmute": "不再隐藏 @{name}",
   "account.unmute_notifications": "不再隐藏来自 @{name} 的通知",
   "account.view_full_profile": "查看完整资料",
-  "alert.unexpected.message": "An unexpected error occurred.",
-  "alert.unexpected.title": "Oops!",
+  "alert.unexpected.message": "发生了意外错误。",
+  "alert.unexpected.title": "哎呀!",
   "boost_modal.combo": "下次按住 {combo} 即可跳过此提示",
   "bundle_column_error.body": "载入这个组件时发生了错误。",
   "bundle_column_error.retry": "重试",
@@ -41,8 +41,8 @@
   "bundle_modal_error.retry": "重试",
   "column.blocks": "屏蔽用户",
   "column.community": "本站时间轴",
-  "column.direct": "Direct messages",
-  "column.domain_blocks": "Hidden domains",
+  "column.direct": "私信",
+  "column.domain_blocks": "已屏蔽的网站",
   "column.favourites": "收藏过的嘟文",
   "column.follow_requests": "关注请求",
   "column.home": "主页",
@@ -60,18 +60,18 @@
   "column_header.unpin": "取消固定",
   "column_subheading.navigation": "导航",
   "column_subheading.settings": "设置",
-  "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
-  "compose_form.direct_message_warning_learn_more": "Learn more",
+  "compose_form.direct_message_warning": "这条嘟文仅对所有被提及的用户可见。",
+  "compose_form.direct_message_warning_learn_more": "了解详情",
   "compose_form.hashtag_warning": "这条嘟文被设置为“不公开”,因此它不会出现在任何话题标签的列表下。只有公开的嘟文才能通过话题标签进行搜索。",
   "compose_form.lock_disclaimer": "你的帐户没有{locked}。任何人都可以在关注你后立即查看仅关注者可见的嘟文。",
   "compose_form.lock_disclaimer.lock": "开启保护",
   "compose_form.placeholder": "在想啥?",
   "compose_form.publish": "嘟嘟",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive.marked": "Media is marked as sensitive",
-  "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
-  "compose_form.spoiler.marked": "Text is hidden behind warning",
-  "compose_form.spoiler.unmarked": "Text is not hidden",
+  "compose_form.sensitive.marked": "媒体已被标记为敏感内容",
+  "compose_form.sensitive.unmarked": "媒体未被标记为敏感内容",
+  "compose_form.spoiler.marked": "正文已被折叠在警告信息之后",
+  "compose_form.spoiler.unmarked": "正文未被折叠",
   "compose_form.spoiler_placeholder": "折叠部分的警告消息",
   "confirmation_modal.cancel": "取消",
   "confirmations.block.confirm": "屏蔽",
@@ -103,7 +103,7 @@
   "emoji_button.symbols": "符号",
   "emoji_button.travel": "旅行和地点",
   "empty_column.community": "本站时间轴暂时没有内容,快嘟几个来抢头香啊!",
-  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+  "empty_column.direct": "你还没有使用过私信。当你发出或者收到私信时,它会在这里显示。",
   "empty_column.hashtag": "这个话题标签下暂时没有内容。",
   "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。",
   "empty_column.home.public_timeline": "公共时间轴",
@@ -137,7 +137,7 @@
   "keyboard_shortcuts.mention": "提及嘟文作者",
   "keyboard_shortcuts.reply": "回复嘟文",
   "keyboard_shortcuts.search": "选择搜索框",
-  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
+  "keyboard_shortcuts.toggle_hidden": "显示或隐藏被折叠的正文",
   "keyboard_shortcuts.toot": "发送新嘟文",
   "keyboard_shortcuts.unfocus": "取消输入",
   "keyboard_shortcuts.up": "在列表中让光标上移",
@@ -159,8 +159,8 @@
   "mute_modal.hide_notifications": "同时隐藏来自这个用户的通知",
   "navigation_bar.blocks": "被屏蔽的用户",
   "navigation_bar.community_timeline": "本站时间轴",
-  "navigation_bar.direct": "Direct messages",
-  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.direct": "私信",
+  "navigation_bar.domain_blocks": "已屏蔽的网站",
   "navigation_bar.edit_profile": "修改个人资料",
   "navigation_bar.favourites": "收藏的内容",
   "navigation_bar.follow_requests": "关注请求",
@@ -187,7 +187,7 @@
   "notifications.column_settings.reblog": "当有人转嘟了你的嘟文时:",
   "notifications.column_settings.show": "在通知栏显示",
   "notifications.column_settings.sound": "播放音效",
-  "notifications.group": "{count} notifications",
+  "notifications.group": "{count} 条通知",
   "onboarding.done": "出发!",
   "onboarding.next": "下一步",
   "onboarding.page_five.public_timelines": "“本站时间轴”显示的是由本站({domain})用户发布的所有公开嘟文。“跨站公共时间轴”显示的的是由本站用户关注对象所发布的所有公开嘟文。这些就是寻人好去处的公共时间轴啦。",
@@ -226,29 +226,29 @@
   "relative_time.minutes": "{number}分",
   "relative_time.seconds": "{number}秒",
   "reply_indicator.cancel": "取消",
-  "report.forward": "Forward to {target}",
-  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
-  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.forward": "发送举报至 {target}",
+  "report.forward_hint": "这名用户来自另一个实例。是否要向那个实例发送一条匿名的举报?",
+  "report.hint": "举报将会发送给你所在实例的监察员。你可以在下面填写举报这个用户的理由:",
   "report.placeholder": "附言",
   "report.submit": "提交",
   "report.target": "举报 {target}",
   "search.placeholder": "搜索",
   "search_popout.search_format": "高级搜索格式",
-  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
+  "search_popout.tips.full_text": "输入其他内容将会返回所有你撰写、收藏、转嘟过或提及到你的嘟文,同时也会在用户名、昵称和话题标签中进行搜索。",
   "search_popout.tips.hashtag": "话题标签",
   "search_popout.tips.status": "嘟文",
-  "search_popout.tips.text": "使用普通字符进行搜索将会返回昵称、用户名和话题标签",
+  "search_popout.tips.text": "输入其他内容将会返回昵称、用户名和话题标签",
   "search_popout.tips.user": "用户",
-  "search_results.accounts": "People",
-  "search_results.hashtags": "Hashtags",
-  "search_results.statuses": "Toots",
+  "search_results.accounts": "用户",
+  "search_results.hashtags": "话题标签",
+  "search_results.statuses": "嘟文",
   "search_results.total": "共 {count, number} 个结果",
   "standalone.public_title": "大家都在干啥?",
   "status.block": "屏蔽 @{name}",
-  "status.cancel_reblog_private": "Unboost",
+  "status.cancel_reblog_private": "取消转嘟",
   "status.cannot_reblog": "无法转嘟这条嘟文",
   "status.delete": "删除",
-  "status.direct": "Direct message @{name}",
+  "status.direct": "发送私信给 @{name}",
   "status.embed": "嵌入",
   "status.favourite": "收藏",
   "status.load_more": "加载更多",
@@ -259,9 +259,9 @@
   "status.mute_conversation": "隐藏此对话",
   "status.open": "展开嘟文",
   "status.pin": "在个人资料页面置顶",
-  "status.pinned": "Pinned toot",
+  "status.pinned": "置顶嘟文",
   "status.reblog": "转嘟",
-  "status.reblog_private": "Boost to original audience",
+  "status.reblog_private": "转嘟给原有关注者",
   "status.reblogged_by": "{name} 转嘟了",
   "status.reply": "回复",
   "status.replyAll": "回复所有人",
@@ -270,21 +270,23 @@
   "status.sensitive_warning": "敏感内容",
   "status.share": "分享",
   "status.show_less": "隐藏内容",
-  "status.show_less_all": "Show less for all",
+  "status.show_less_all": "隐藏所有内容",
   "status.show_more": "显示内容",
-  "status.show_more_all": "Show more for all",
+  "status.show_more_all": "显示所有内容",
   "status.unmute_conversation": "不再隐藏此对话",
   "status.unpin": "在个人资料页面取消置顶",
   "tabs_bar.federated_timeline": "跨站",
   "tabs_bar.home": "主页",
   "tabs_bar.local_timeline": "本站",
   "tabs_bar.notifications": "通知",
-  "tabs_bar.search": "Search",
+  "tabs_bar.search": "搜索",
+  "timeline.media": "媒体",
+  "timeline.posts": "嘟文",
   "ui.beforeunload": "如果你现在离开 Mastodon,你的草稿内容将会被丢弃。",
   "upload_area.title": "将文件拖放到此处开始上传",
   "upload_button.label": "上传媒体文件",
   "upload_form.description": "为视觉障碍人士添加文字说明",
-  "upload_form.focus": "Crop",
+  "upload_form.focus": "剪裁",
   "upload_form.undo": "取消上传",
   "upload_progress.label": "上传中…",
   "video.close": "关闭视频",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index 18be17792..cad53c69a 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "本站",
   "tabs_bar.notifications": "通知",
   "tabs_bar.search": "搜尋",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "如果你現在離開 Mastodon,你的草稿內容將會被丟棄。",
   "upload_area.title": "將檔案拖放至此上載",
   "upload_button.label": "上載媒體檔案",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 3222c035a..91b44012f 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -280,6 +280,8 @@
   "tabs_bar.local_timeline": "本地",
   "tabs_bar.notifications": "通知",
   "tabs_bar.search": "Search",
+  "timeline.media": "Media",
+  "timeline.posts": "Toots",
   "ui.beforeunload": "如果離開 Mastodon,你的草稿將會不見。",
   "upload_area.title": "拖放來上傳",
   "upload_button.label": "增加媒體",
diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js
index 16eaefcbb..f922f7dd0 100644
--- a/app/javascript/mastodon/service_worker/web_push_notifications.js
+++ b/app/javascript/mastodon/service_worker/web_push_notifications.js
@@ -1,5 +1,6 @@
 import IntlMessageFormat from 'intl-messageformat';
 import locales from './web_push_locales';
+import { unescape } from 'lodash';
 
 const MAX_NOTIFICATIONS = 5;
 const GROUP_TAG = 'tag';
@@ -59,6 +60,9 @@ const fetchFromApi = (path, method, accessToken) => {
 const formatMessage = (messageId, locale, values = {}) =>
   (new IntlMessageFormat(locales[locale][messageId], locale)).format(values);
 
+const htmlToPlainText = html =>
+  unescape(html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n').replace(/<[^>]*>/g, ''));
+
 const handlePush = (event) => {
   const { access_token, notification_id, preferred_locale, title, body, icon } = event.data.json();
 
@@ -76,7 +80,7 @@ const handlePush = (event) => {
       const options = {};
 
       options.title     = formatMessage(`notification.${notification.type}`, preferred_locale, { name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
-      options.body      = notification.status && notification.status.content;
+      options.body      = notification.status && htmlToPlainText(notification.status.content);
       options.icon      = notification.account.avatar_static;
       options.timestamp = notification.created_at && new Date(notification.created_at);
       options.tag       = notification.id;
@@ -85,10 +89,10 @@ const handlePush = (event) => {
       options.data      = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/web/statuses/${notification.status.id}` : `/web/accounts/${notification.account.id}` };
 
       if (notification.status && notification.status.sensitive) {
-        options.data.hiddenBody  = notification.status.content;
+        options.data.hiddenBody  = htmlToPlainText(notification.status.content);
         options.data.hiddenImage = notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url;
 
-        options.body    = undefined;
+        options.body    = notification.status.spoiler_text;
         options.image   = undefined;
         options.actions = [actionExpand(preferred_locale)];
       } else if (notification.status) {
diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss
index 93aa134cf..de0a7ea42 100644
--- a/app/javascript/styles/mastodon/accounts.scss
+++ b/app/javascript/styles/mastodon/accounts.scss
@@ -1,5 +1,5 @@
 .card {
-  background-color: lighten($ui-base-color, 4%);
+  background-color: $base-shadow-color;
   background-size: cover;
   background-position: center;
   border-radius: 4px 4px 0 0;
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 4022885e1..2967c2d68 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -4737,6 +4737,8 @@ a.status-card {
   }
 }
 
+.community-timeline__section-headline,
+.public-timeline__section-headline,
 .account__section-headline {
   background: darken($ui-base-color, 4%);
   border-bottom: 1px solid lighten($ui-base-color, 8%);
diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb
index cb65a2256..ace51a1fc 100644
--- a/app/services/batched_remove_status_service.rb
+++ b/app/services/batched_remove_status_service.rb
@@ -81,6 +81,11 @@ class BatchedRemoveStatusService < BaseService
       redis.publish('timeline:public', payload)
       redis.publish('timeline:public:local', payload) if status.local?
 
+      if status.media_attachments.any?
+        redis.publish('timeline:public:media', payload)
+        redis.publish('timeline:public:local:media', payload) if status.local?
+      end
+
       @tags[status.id].each do |hashtag|
         redis.publish("timeline:hashtag:#{hashtag}", payload)
         redis.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local?
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index cb82a79ed..8b3630229 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -25,6 +25,7 @@ class FanOutOnWriteService < BaseService
     return if status.reply? && status.in_reply_to_account_id != status.account_id
 
     deliver_to_public(status)
+    deliver_to_media(status) if status.media_attachments.any?
   end
 
   private
@@ -85,6 +86,13 @@ class FanOutOnWriteService < BaseService
     Redis.current.publish('timeline:public:local', @payload) if status.local?
   end
 
+  def deliver_to_media(status)
+    Rails.logger.debug "Delivering status #{status.id} to media timeline"
+
+    Redis.current.publish('timeline:public:media', @payload)
+    Redis.current.publish('timeline:public:local:media', @payload) if status.local?
+  end
+
   def deliver_to_direct_timelines(status)
     Rails.logger.debug "Delivering status #{status.id} to direct timelines"
 
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index e164c03ab..8c3e18444 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -20,6 +20,7 @@ class RemoveStatusService < BaseService
     remove_reblogs
     remove_from_hashtags
     remove_from_public
+    remove_from_media if status.media_attachments.any?
     remove_from_direct if status.direct_visibility?
 
     @status.destroy!
@@ -131,6 +132,13 @@ class RemoveStatusService < BaseService
     Redis.current.publish('timeline:public:local', @payload) if @status.local?
   end
 
+  def remove_from_media
+    return unless @status.public_visibility?
+
+    Redis.current.publish('timeline:public:media', @payload)
+    Redis.current.publish('timeline:public:local:media', @payload) if @status.local?
+  end
+
   def remove_from_direct
     @mentions.each do |mention|
       Redis.current.publish("timeline:direct:#{mention.account.id}", @payload) if mention.account.local?