about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/concerns/localized.rb6
-rw-r--r--app/helpers/stream_entries_helper.rb29
-rw-r--r--app/javascript/flavours/glitch/actions/compose.js5
-rw-r--r--app/javascript/flavours/glitch/actions/reports.js9
-rw-r--r--app/javascript/flavours/glitch/actions/timelines.js19
-rw-r--r--app/javascript/flavours/glitch/components/status_content.js11
-rw-r--r--app/javascript/flavours/glitch/components/status_list.js19
-rw-r--r--app/javascript/flavours/glitch/components/status_prepend.js10
-rw-r--r--app/javascript/flavours/glitch/containers/status_container.js5
-rw-r--r--app/javascript/flavours/glitch/features/account/components/action_bar.js4
-rw-r--r--app/javascript/flavours/glitch/features/account_gallery/components/media_item.js16
-rw-r--r--app/javascript/flavours/glitch/features/account_gallery/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/components/header.js13
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/index.js36
-rw-r--r--app/javascript/flavours/glitch/features/composer/index.js7
-rw-r--r--app/javascript/flavours/glitch/features/followers/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/following/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/getting_started/index.js51
-rw-r--r--app/javascript/flavours/glitch/features/report/components/status_check_box.js46
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/column_link.js8
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/report_modal.js47
-rw-r--r--app/javascript/flavours/glitch/features/ui/index.js1
-rw-r--r--app/javascript/flavours/glitch/reducers/compose.js3
-rw-r--r--app/javascript/flavours/glitch/reducers/reports.js4
-rw-r--r--app/javascript/flavours/glitch/styles/components/accounts.scss96
-rw-r--r--app/javascript/flavours/glitch/styles/components/index.scss11
-rw-r--r--app/javascript/flavours/glitch/styles/components/media.scss1
-rw-r--r--app/javascript/flavours/glitch/styles/components/modal.scss89
-rw-r--r--app/javascript/flavours/glitch/styles/components/status.scss29
-rw-r--r--app/javascript/flavours/glitch/util/react_router_helpers.js9
-rw-r--r--app/javascript/mastodon/actions/interactions.js18
-rw-r--r--app/javascript/mastodon/actions/notifications.js2
-rw-r--r--app/javascript/mastodon/components/media_gallery.js65
-rw-r--r--app/javascript/mastodon/components/status_content.js2
-rw-r--r--app/javascript/mastodon/features/account_timeline/index.js10
-rw-r--r--app/javascript/mastodon/features/compose/containers/sensitive_button_container.js5
-rw-r--r--app/javascript/mastodon/features/compose/containers/spoiler_button_container.js5
-rw-r--r--app/javascript/mastodon/features/compose/containers/warning_container.js2
-rw-r--r--app/javascript/mastodon/features/compose/index.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/focal_point_modal.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/media_modal.js35
-rw-r--r--app/javascript/mastodon/locales/ar.json10
-rw-r--r--app/javascript/mastodon/locales/bg.json8
-rw-r--r--app/javascript/mastodon/locales/ca.json8
-rw-r--r--app/javascript/mastodon/locales/de.json8
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json24
-rw-r--r--app/javascript/mastodon/locales/en.json8
-rw-r--r--app/javascript/mastodon/locales/eo.json8
-rw-r--r--app/javascript/mastodon/locales/es.json64
-rw-r--r--app/javascript/mastodon/locales/fa.json8
-rw-r--r--app/javascript/mastodon/locales/fi.json32
-rw-r--r--app/javascript/mastodon/locales/fr.json8
-rw-r--r--app/javascript/mastodon/locales/gl.json8
-rw-r--r--app/javascript/mastodon/locales/he.json8
-rw-r--r--app/javascript/mastodon/locales/hr.json8
-rw-r--r--app/javascript/mastodon/locales/hu.json8
-rw-r--r--app/javascript/mastodon/locales/hy.json8
-rw-r--r--app/javascript/mastodon/locales/id.json150
-rw-r--r--app/javascript/mastodon/locales/io.json8
-rw-r--r--app/javascript/mastodon/locales/it.json8
-rw-r--r--app/javascript/mastodon/locales/ja.json10
-rw-r--r--app/javascript/mastodon/locales/ko.json8
-rw-r--r--app/javascript/mastodon/locales/nl.json10
-rw-r--r--app/javascript/mastodon/locales/no.json8
-rw-r--r--app/javascript/mastodon/locales/oc.json8
-rw-r--r--app/javascript/mastodon/locales/pl.json12
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json8
-rw-r--r--app/javascript/mastodon/locales/pt.json8
-rw-r--r--app/javascript/mastodon/locales/ru.json8
-rw-r--r--app/javascript/mastodon/locales/sk.json12
-rw-r--r--app/javascript/mastodon/locales/sr-Latn.json8
-rw-r--r--app/javascript/mastodon/locales/sr.json8
-rw-r--r--app/javascript/mastodon/locales/sv.json30
-rw-r--r--app/javascript/mastodon/locales/th.json8
-rw-r--r--app/javascript/mastodon/locales/tr.json8
-rw-r--r--app/javascript/mastodon/locales/uk.json8
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json8
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json8
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json8
-rw-r--r--app/javascript/mastodon/reducers/compose.js14
-rw-r--r--app/javascript/mastodon/reducers/statuses.js22
-rw-r--r--app/javascript/mastodon/service_worker/entry.js40
-rw-r--r--app/javascript/styles/mastodon/accounts.scss1
-rw-r--r--app/javascript/styles/mastodon/components.scss34
-rw-r--r--app/javascript/styles/mastodon/rtl.scss16
-rw-r--r--app/lib/activitypub/activity/create.rb2
-rw-r--r--app/lib/ostatus/activity/creation.rb2
-rw-r--r--app/models/concerns/remotable.rb2
-rw-r--r--app/models/tag.rb2
-rw-r--r--app/services/backup_service.rb2
-rw-r--r--app/views/about/show.html.haml2
-rw-r--r--app/views/accounts/_og.html.haml2
-rw-r--r--app/views/accounts/show.html.haml2
-rw-r--r--app/views/stream_entries/_og_description.html.haml2
-rw-r--r--app/views/stream_entries/_og_image.html.haml2
-rw-r--r--app/views/stream_entries/show.html.haml4
-rw-r--r--app/workers/backup_worker.rb11
97 files changed, 1015 insertions, 466 deletions
diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb
index e697284a8..abd85ea27 100644
--- a/app/controllers/concerns/localized.rb
+++ b/app/controllers/concerns/localized.rb
@@ -17,7 +17,11 @@ module Localized
   end
 
   def default_locale
-    request_locale || I18n.default_locale
+    if ENV['DEFAULT_LOCALE'].present?
+      I18n.default_locale
+    else
+      request_locale || I18n.default_locale
+    end
   end
 
   def request_locale
diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb
index 54b92bdf4..3992432db 100644
--- a/app/helpers/stream_entries_helper.rb
+++ b/app/helpers/stream_entries_helper.rb
@@ -29,6 +29,35 @@ module StreamEntriesHelper
     [prepend_str, account.note].join(' · ')
   end
 
+  def media_summary(status)
+    attachments = { image: 0, video: 0 }
+
+    status.media_attachments.each do |media|
+      if media.video?
+        attachments[:video] += 1
+      else
+        attachments[:image] += 1
+      end
+    end
+
+    text = attachments.to_a.reject { |_, value| value.zero? }.map { |key, value| t("statuses.attached.#{key}", count: value) }.join(' · ')
+
+    return if text.blank?
+
+    t('statuses.attached.description', attached: text)
+  end
+
+  def status_text_summary(status)
+    return if status.spoiler_text.blank?
+    t('statuses.content_warning', warning: status.spoiler_text)
+  end
+
+  def status_description(status)
+    components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')]
+    components << status.text if status.spoiler_text.blank?
+    components.reject(&:blank?).join("\n\n")
+  end
+
   def stream_link_target
     embedded_view? ? '_blank' : nil
   end
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js
index c46387104..1e9386859 100644
--- a/app/javascript/flavours/glitch/actions/compose.js
+++ b/app/javascript/flavours/glitch/actions/compose.js
@@ -102,8 +102,9 @@ export function mentionCompose(account, router) {
 export function submitCompose() {
   return function (dispatch, getState) {
     let status = getState().getIn(['compose', 'text'], '');
+    let media  = getState().getIn(['compose', 'media_attachments']);
 
-    if (!status || !status.length) {
+    if ((!status || !status.length) && media.size === 0) {
       return;
     }
 
@@ -114,7 +115,7 @@ export function submitCompose() {
     api(getState).post('/api/v1/statuses', {
       status,
       in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
-      media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')),
+      media_ids: media.map(item => item.get('id')),
       sensitive: getState().getIn(['compose', 'sensitive']),
       spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
       visibility: getState().getIn(['compose', 'privacy']),
diff --git a/app/javascript/flavours/glitch/actions/reports.js b/app/javascript/flavours/glitch/actions/reports.js
index ad4fd18a9..80c3b3280 100644
--- a/app/javascript/flavours/glitch/actions/reports.js
+++ b/app/javascript/flavours/glitch/actions/reports.js
@@ -10,6 +10,7 @@ export const REPORT_SUBMIT_FAIL    = 'REPORT_SUBMIT_FAIL';
 
 export const REPORT_STATUS_TOGGLE  = 'REPORT_STATUS_TOGGLE';
 export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
+export const REPORT_FORWARD_CHANGE = 'REPORT_FORWARD_CHANGE';
 
 export function initReport(account, status) {
   return dispatch => {
@@ -45,6 +46,7 @@ export function submitReport() {
       account_id: getState().getIn(['reports', 'new', 'account_id']),
       status_ids: getState().getIn(['reports', 'new', 'status_ids']),
       comment: getState().getIn(['reports', 'new', 'comment']),
+      forward: getState().getIn(['reports', 'new', 'forward']),
     }).then(response => {
       dispatch(closeModal());
       dispatch(submitReportSuccess(response.data));
@@ -78,3 +80,10 @@ export function changeReportComment(comment) {
     comment,
   };
 };
+
+export function changeReportForward(forward) {
+  return {
+    type: REPORT_FORWARD_CHANGE,
+    forward,
+  };
+};
diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js
index 3a9d64084..d99c6d98b 100644
--- a/app/javascript/flavours/glitch/actions/timelines.js
+++ b/app/javascript/flavours/glitch/actions/timelines.js
@@ -117,14 +117,15 @@ export function refreshTimeline(timelineId, path, params = {}) {
   };
 };
 
-export const refreshHomeTimeline         = () => refreshTimeline('home', '/api/v1/timelines/home');
-export const refreshPublicTimeline       = () => refreshTimeline('public', '/api/v1/timelines/public');
-export const refreshCommunityTimeline    = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
-export const refreshDirectTimeline       = () => refreshTimeline('direct', '/api/v1/timelines/direct');
-export const refreshAccountTimeline      = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
-export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
-export const refreshHashtagTimeline      = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
-export const refreshListTimeline         = id => refreshTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
+export const refreshHomeTimeline            = () => refreshTimeline('home', '/api/v1/timelines/home');
+export const refreshPublicTimeline          = () => refreshTimeline('public', '/api/v1/timelines/public');
+export const refreshCommunityTimeline       = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
+export const refreshDirectTimeline          = () => refreshTimeline('direct', '/api/v1/timelines/direct');
+export const refreshAccountTimeline         = (accountId, withReplies) => refreshTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies });
+export const refreshAccountFeaturedTimeline = accountId => refreshTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
+export const refreshAccountMediaTimeline    = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
+export const refreshHashtagTimeline         = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
+export const refreshListTimeline            = id => refreshTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
 
 export function refreshTimelineFail(timeline, error, skipLoading) {
   return {
@@ -163,7 +164,7 @@ export const expandHomeTimeline         = () => expandTimeline('home', '/api/v1/
 export const expandPublicTimeline       = () => expandTimeline('public', '/api/v1/timelines/public');
 export const expandCommunityTimeline    = () => expandTimeline('community', '/api/v1/timelines/public', { local: true });
 export const expandDirectTimeline       = () => expandTimeline('direct', '/api/v1/timelines/direct');
-export const expandAccountTimeline      = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
+export const expandAccountTimeline      = (accountId, withReplies) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies })
 export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
 export const expandHashtagTimeline      = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
 export const expandListTimeline         = id => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js
index 0c40e62cc..a15bf7c52 100644
--- a/app/javascript/flavours/glitch/components/status_content.js
+++ b/app/javascript/flavours/glitch/components/status_content.js
@@ -23,7 +23,12 @@ export default class StatusContent extends React.PureComponent {
   };
 
   _updateStatusLinks () {
-    const node  = this.node;
+    const node = this.node;
+
+    if (!node) {
+      return;
+    }
+
     const links = node.querySelectorAll('a');
 
     for (var i = 0; i < links.length; ++i) {
@@ -126,6 +131,10 @@ export default class StatusContent extends React.PureComponent {
       disabled,
     } = this.props;
 
+    if (status.get('content').length === 0) {
+      return null;
+    }
+
     const hidden = this.props.setExpansion ? !this.props.expanded : this.state.hidden;
 
     const content = { __html: status.get('contentHtml') };
diff --git a/app/javascript/flavours/glitch/components/status_list.js b/app/javascript/flavours/glitch/components/status_list.js
index f253f0fdc..ea40463da 100644
--- a/app/javascript/flavours/glitch/components/status_list.js
+++ b/app/javascript/flavours/glitch/components/status_list.js
@@ -11,6 +11,7 @@ export default class StatusList extends ImmutablePureComponent {
   static propTypes = {
     scrollKey: PropTypes.string.isRequired,
     statusIds: ImmutablePropTypes.list.isRequired,
+    featuredStatusIds: ImmutablePropTypes.list,
     onScrollToBottom: PropTypes.func,
     onScrollToTop: PropTypes.func,
     onScroll: PropTypes.func,
@@ -50,7 +51,7 @@ export default class StatusList extends ImmutablePureComponent {
   }
 
   render () {
-    const { statusIds, ...other }  = this.props;
+    const { statusIds, featuredStatusIds, ...other }  = this.props;
     const { isLoading, isPartial } = other;
 
     if (isPartial) {
@@ -68,8 +69,8 @@ export default class StatusList extends ImmutablePureComponent {
       );
     }
 
-    const scrollableContent = (isLoading || statusIds.size > 0) ? (
-      statusIds.map((statusId) => (
+    let scrollableContent = (isLoading || statusIds.size > 0) ? (
+      statusIds.map(statusId => (
         <StatusContainer
           key={statusId}
           id={statusId}
@@ -79,6 +80,18 @@ export default class StatusList extends ImmutablePureComponent {
       ))
     ) : null;
 
+    if (scrollableContent && featuredStatusIds) {
+      scrollableContent = featuredStatusIds.map(statusId => (
+        <StatusContainer
+          key={`f-${statusId}`}
+          id={statusId}
+          featured
+          onMoveUp={this.handleMoveUp}
+          onMoveDown={this.handleMoveDown}
+        />
+      )).concat(scrollableContent);
+    }
+
     return (
       <ScrollableList {...other} ref={this.setRef}>
         {scrollableContent}
diff --git a/app/javascript/flavours/glitch/components/status_prepend.js b/app/javascript/flavours/glitch/components/status_prepend.js
index bd2559e46..f4ef83135 100644
--- a/app/javascript/flavours/glitch/components/status_prepend.js
+++ b/app/javascript/flavours/glitch/components/status_prepend.js
@@ -34,6 +34,10 @@ export default class StatusPrepend extends React.PureComponent {
       </a>
     );
     switch (type) {
+    case 'featured':
+      return (
+        <FormattedMessage id='status.pinned' defaultMessage='Pinned toot' />
+      );
     case 'reblogged_by':
       return (
         <FormattedMessage
@@ -67,11 +71,11 @@ export default class StatusPrepend extends React.PureComponent {
     const { type } = this.props;
 
     return !type ? null : (
-      <aside className={type === 'reblogged_by' ? 'status__prepend' : 'notification__message'}>
-        <div className={type === 'reblogged_by' ? 'status__prepend-icon-wrapper' : 'notification__favourite-icon-wrapper'}>
+      <aside className={type === 'reblogged_by' || type === 'featured' ? 'status__prepend' : 'notification__message'}>
+        <div className={type === 'reblogged_by' || type === 'featured' ? 'status__prepend-icon-wrapper' : 'notification__favourite-icon-wrapper'}>
           <i
             className={`fa fa-fw fa-${
-              type === 'favourite' ? 'star star-icon' : 'retweet'
+              type === 'favourite' ? 'star star-icon' : (type === 'featured' ? 'thumb-tack' : 'retweet')
             } status__prepend-icon`}
           />
         </div>
diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js
index c0b9b5800..f3db05ae6 100644
--- a/app/javascript/flavours/glitch/containers/status_container.js
+++ b/app/javascript/flavours/glitch/containers/status_container.js
@@ -38,7 +38,10 @@ const makeMapStateToProps = () => {
     let account = undefined;
     let prepend = undefined;
 
-    if (reblogStatus !== null && typeof reblogStatus === 'object') {
+    if (props.featured) {
+      account = status.get('account');
+      prepend = 'featured';
+    } else if (reblogStatus !== null && typeof reblogStatus === 'object') {
       account = status.get('account');
       status = reblogStatus;
       prepend = 'reblogged_by';
diff --git a/app/javascript/flavours/glitch/features/account/components/action_bar.js b/app/javascript/flavours/glitch/features/account/components/action_bar.js
index df8cb3733..fb90722f3 100644
--- a/app/javascript/flavours/glitch/features/account/components/action_bar.js
+++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js
@@ -53,11 +53,11 @@ export default class ActionBar extends React.PureComponent {
     let extraInfo = '';
 
     menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
+
     if ('share' in navigator) {
       menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
     }
-    menu.push(null);
-    menu.push({ text: intl.formatMessage(messages.media), to: `/accounts/${account.get('id')}/media` });
+
     menu.push(null);
 
     if (account.get('id') === me) {
diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js
index e52d3b0bb..c2cf48d7b 100644
--- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js
+++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js
@@ -12,24 +12,26 @@ export default class MediaItem extends ImmutablePureComponent {
   render () {
     const { media } = this.props;
     const status = media.get('status');
+    const focusX = media.getIn(['meta', 'focus', 'x']);
+    const focusY = media.getIn(['meta', 'focus', 'y']);
+    const x = ((focusX /  2) + .5) * 100;
+    const y = ((focusY / -2) + .5) * 100;
+    const style = {};
 
-    let content, style;
+    let content;
 
     if (media.get('type') === 'gifv') {
       content = <span className='media-gallery__gifv__label'>GIF</span>;
     }
 
     if (!status.get('sensitive')) {
-      style = { backgroundImage: `url(${media.get('preview_url')})` };
+      style.backgroundImage    = `url(${media.get('preview_url')})`;
+      style.backgroundPosition = `${x}% ${y}%`;
     }
 
     return (
       <div className='account-gallery__item'>
-        <Permalink
-          to={`/statuses/${status.get('id')}`}
-          href={status.get('url')}
-          style={style}
-        >
+        <Permalink to={`/statuses/${status.get('id')}`} href={status.get('url')} style={style}>
           {content}
         </Permalink>
       </div>
diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js
index df66b3b21..63ff98deb 100644
--- a/app/javascript/flavours/glitch/features/account_gallery/index.js
+++ b/app/javascript/flavours/glitch/features/account_gallery/index.js
@@ -11,7 +11,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 import { getAccountGallery } from 'flavours/glitch/selectors';
 import MediaItem from './components/media_item';
 import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container';
-import { FormattedMessage } from 'react-intl';
 import { ScrollContainer } from 'react-router-scroll-4';
 import LoadMore from 'flavours/glitch/components/load_more';
 
@@ -89,10 +88,6 @@ export default class AccountGallery extends ImmutablePureComponent {
           <div className='scrollable' onScroll={this.handleScroll}>
             <HeaderContainer accountId={this.props.params.accountId} />
 
-            <div className='account-section-headline'>
-              <FormattedMessage id='account.media' defaultMessage='Media' />
-            </div>
-
             <div className='account-gallery__container'>
               {medias.map(media =>
                 (<MediaItem
diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js
index 092034664..43fa68ce2 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js
@@ -5,6 +5,8 @@ import InnerHeader from 'flavours/glitch/features/account/components/header';
 import ActionBar from 'flavours/glitch/features/account/components/action_bar';
 import MissingIndicator from 'flavours/glitch/components/missing_indicator';
 import ImmutablePureComponent from 'react-immutable-pure-component';
+import { FormattedMessage } from 'react-intl';
+import { NavLink } from 'react-router-dom';
 
 export default class Header extends ImmutablePureComponent {
 
@@ -18,6 +20,7 @@ export default class Header extends ImmutablePureComponent {
     onMute: PropTypes.func.isRequired,
     onBlockDomain: PropTypes.func.isRequired,
     onUnblockDomain: PropTypes.func.isRequired,
+    hideTabs: PropTypes.bool,
   };
 
   static contextTypes = {
@@ -65,7 +68,7 @@ export default class Header extends ImmutablePureComponent {
   }
 
   render () {
-    const { account } = this.props;
+    const { account, hideTabs } = this.props;
 
     if (account === null) {
       return <MissingIndicator />;
@@ -89,6 +92,14 @@ export default class Header extends ImmutablePureComponent {
           onBlockDomain={this.handleBlockDomain}
           onUnblockDomain={this.handleUnblockDomain}
         />
+
+        {!hideTabs && (
+          <div className='account__section-headline'>
+            <NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
+            <NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>
+            <NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
+          </div>
+        )}
       </div>
     );
   }
diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js
index 75dba5049..fbb16dff9 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/index.js
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import { fetchAccount } from 'flavours/glitch/actions/accounts';
-import { refreshAccountTimeline, expandAccountTimeline } from 'flavours/glitch/actions/timelines';
+import { refreshAccountTimeline, refreshAccountFeaturedTimeline, expandAccountTimeline } from 'flavours/glitch/actions/timelines';
 import StatusList from '../../components/status_list';
 import LoadingIndicator from '../../components/loading_indicator';
 import Column from '../ui/components/column';
@@ -12,11 +12,16 @@ import ColumnBackButton from '../../components/column_back_button';
 import { List as ImmutableList } from 'immutable';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
-const mapStateToProps = (state, props) => ({
-  statusIds: state.getIn(['timelines', `account:${props.params.accountId}`, 'items'], ImmutableList()),
-  isLoading: state.getIn(['timelines', `account:${props.params.accountId}`, 'isLoading']),
-  hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}`, 'next']),
-});
+const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => {
+  const path = withReplies ? `${accountId}:with_replies` : accountId;
+
+  return {
+    statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()),
+    featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()),
+    isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
+    hasMore: !!state.getIn(['timelines', `account:${path}`, 'next']),
+  };
+};
 
 @connect(mapStateToProps)
 export default class AccountTimeline extends ImmutablePureComponent {
@@ -25,30 +30,36 @@ export default class AccountTimeline extends ImmutablePureComponent {
     params: PropTypes.object.isRequired,
     dispatch: PropTypes.func.isRequired,
     statusIds: ImmutablePropTypes.list,
+    featuredStatusIds: ImmutablePropTypes.list,
     isLoading: PropTypes.bool,
     hasMore: PropTypes.bool,
+    withReplies: PropTypes.bool,
   };
 
   componentWillMount () {
-    this.props.dispatch(fetchAccount(this.props.params.accountId));
-    this.props.dispatch(refreshAccountTimeline(this.props.params.accountId));
+    const { params: { accountId }, withReplies } = this.props;
+
+    this.props.dispatch(fetchAccount(accountId));
+    this.props.dispatch(refreshAccountFeaturedTimeline(accountId));
+    this.props.dispatch(refreshAccountTimeline(accountId, withReplies));
   }
 
   componentWillReceiveProps (nextProps) {
-    if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
+    if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
       this.props.dispatch(fetchAccount(nextProps.params.accountId));
-      this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId));
+      this.props.dispatch(refreshAccountFeaturedTimeline(nextProps.params.accountId));
+      this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId, nextProps.params.withReplies));
     }
   }
 
   handleScrollToBottom = () => {
     if (!this.props.isLoading && this.props.hasMore) {
-      this.props.dispatch(expandAccountTimeline(this.props.params.accountId));
+      this.props.dispatch(expandAccountTimeline(this.props.params.accountId, this.props.withReplies));
     }
   }
 
   render () {
-    const { statusIds, isLoading, hasMore } = this.props;
+    const { statusIds, featuredStatusIds, isLoading, hasMore } = this.props;
 
     if (!statusIds && isLoading) {
       return (
@@ -66,6 +77,7 @@ export default class AccountTimeline extends ImmutablePureComponent {
           prepend={<HeaderContainer accountId={this.props.params.accountId} />}
           scrollKey='account_timeline'
           statusIds={statusIds}
+          featuredStatusIds={featuredStatusIds}
           isLoading={isLoading}
           hasMore={hasMore}
           onScrollToBottom={this.handleScrollToBottom}
diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js
index e50f3ec3f..792ed79a3 100644
--- a/app/javascript/flavours/glitch/features/composer/index.js
+++ b/app/javascript/flavours/glitch/features/composer/index.js
@@ -73,6 +73,7 @@ function mapStateToProps (state) {
     suggestionToken: state.getIn(['compose', 'suggestion_token']),
     suggestions: state.getIn(['compose', 'suggestions']),
     text: state.getIn(['compose', 'text']),
+    anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
   };
 };
 
@@ -272,6 +273,7 @@ class Composer extends React.Component {
       acceptContentTypes,
       advancedOptions,
       amUnlocked,
+      anyMedia,
       intl,
       isSubmitting,
       isUploading,
@@ -305,6 +307,8 @@ class Composer extends React.Component {
       text,
     } = this.props;
 
+    let disabledButton = isSubmitting || isUploading || (!!text.length && !text.trim().length && !anyMedia);
+
     return (
       <div className='composer'>
         <ComposerSpoiler
@@ -374,7 +378,7 @@ class Composer extends React.Component {
         />
         <ComposerPublisher
           countText={`${spoilerText}${countableText(text)}${advancedOptions && advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`}
-          disabled={isSubmitting || isUploading || !!text.length && !text.trim().length}
+          disabled={disabledButton}
           intl={intl}
           onSecondarySubmit={handleSecondarySubmit}
           onSubmit={handleSubmit}
@@ -436,6 +440,7 @@ Composer.propTypes = {
   onUndoUpload: PropTypes.func,
   onUnmount: PropTypes.func,
   onUpload: PropTypes.func,
+  anyMedia: PropTypes.bool,
 };
 
 //  Connecting and export.
diff --git a/app/javascript/flavours/glitch/features/followers/index.js b/app/javascript/flavours/glitch/features/followers/index.js
index f0ef29ff6..c42e0386c 100644
--- a/app/javascript/flavours/glitch/features/followers/index.js
+++ b/app/javascript/flavours/glitch/features/followers/index.js
@@ -80,7 +80,7 @@ export default class Followers extends ImmutablePureComponent {
         <ScrollContainer scrollKey='followers'>
           <div className='scrollable' onScroll={this.handleScroll}>
             <div className='followers'>
-              <HeaderContainer accountId={this.props.params.accountId} />
+              <HeaderContainer accountId={this.props.params.accountId} hideTabs />
               {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
               {loadMore}
             </div>
diff --git a/app/javascript/flavours/glitch/features/following/index.js b/app/javascript/flavours/glitch/features/following/index.js
index f30f7b0d9..c05742d4f 100644
--- a/app/javascript/flavours/glitch/features/following/index.js
+++ b/app/javascript/flavours/glitch/features/following/index.js
@@ -80,7 +80,7 @@ export default class Following extends ImmutablePureComponent {
         <ScrollContainer scrollKey='following'>
           <div className='scrollable' onScroll={this.handleScroll}>
             <div className='following'>
-              <HeaderContainer accountId={this.props.params.accountId} />
+              <HeaderContainer accountId={this.props.params.accountId} hideTabs />
               {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
               {loadMore}
             </div>
diff --git a/app/javascript/flavours/glitch/features/getting_started/index.js b/app/javascript/flavours/glitch/features/getting_started/index.js
index 0077f193b..bf9fe118a 100644
--- a/app/javascript/flavours/glitch/features/getting_started/index.js
+++ b/app/javascript/flavours/glitch/features/getting_started/index.js
@@ -9,6 +9,8 @@ import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { me } from 'flavours/glitch/util/initial_state';
+import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
+import { List as ImmutableList } from 'immutable';
 import { createSelector } from 'reselect';
 import { fetchLists } from 'flavours/glitch/actions/lists';
 
@@ -45,13 +47,31 @@ const makeMapStateToProps = () => {
     lists: getOrderedLists(state),
     myAccount: state.getIn(['accounts', me]),
     columns: state.getIn(['settings', 'columns']),
+    unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
+    unreadNotifications: state.getIn(['notifications', 'unread']),
   });
 
   return mapStateToProps;
 };
 
+const mapDispatchToProps = dispatch => ({
+  fetchFollowRequests: () => dispatch(fetchFollowRequests()),
+  fetchLists: () => dispatch(fetchLists()),
+  openSettings: () => dispatch(openModal('SETTINGS', {})),
+});
+
+const badgeDisplay = (number, limit) => {
+  if (number === 0) {
+    return undefined;
+  } else if (limit && number >= limit) {
+    return `${limit}+`;
+  } else {
+    return number;
+  }
+};
+
+@connect(makeMapStateToProps, mapDispatchToProps)
 @injectIntl
-@connect(makeMapStateToProps)
 export default class GettingStarted extends ImmutablePureComponent {
 
   static propTypes = {
@@ -59,25 +79,28 @@ export default class GettingStarted extends ImmutablePureComponent {
     myAccount: ImmutablePropTypes.map.isRequired,
     columns: ImmutablePropTypes.list,
     multiColumn: PropTypes.bool,
-    dispatch: PropTypes.func.isRequired,
+    fetchFollowRequests: PropTypes.func.isRequired,
+    unreadFollowRequests: PropTypes.number,
+    unreadNotifications: PropTypes.number,
     lists: ImmutablePropTypes.list,
+    fetchLists: PropTypes.func.isRequired,
+    openSettings: PropTypes.func.isRequired,
   };
 
-  openSettings = () => {
-    this.props.dispatch(openModal('SETTINGS', {}));
+  componentWillMount () {
+    this.props.fetchLists();
   }
 
-  openOnboardingModal = (e) => {
-    e.preventDefault();
-    this.props.dispatch(openModal('ONBOARDING'));
-  }
+  componentDidMount () {
+    const { myAccount, fetchFollowRequests } = this.props;
 
-  componentWillMount () {
-    this.props.dispatch(fetchLists());
+    if (myAccount.get('locked')) {
+      fetchFollowRequests();
+    }
   }
 
   render () {
-    const { intl, myAccount, columns, multiColumn, lists } = this.props;
+    const { intl, myAccount, columns, multiColumn, unreadFollowRequests, unreadNotifications, lists, openSettings } = this.props;
 
     const navItems = [];
     let listItems = [];
@@ -88,7 +111,7 @@ export default class GettingStarted extends ImmutablePureComponent {
       }
 
       if (!columns.find(item => item.get('id') === 'NOTIFICATIONS')) {
-        navItems.push(<ColumnLink key='1' icon='bell' text={intl.formatMessage(messages.notifications)} to='/notifications' />);
+        navItems.push(<ColumnLink key='1' icon='bell' text={intl.formatMessage(messages.notifications)} badge={badgeDisplay(unreadNotifications)} to='/notifications' />);
       }
 
       if (!columns.find(item => item.get('id') === 'COMMUNITY')) {
@@ -105,7 +128,7 @@ export default class GettingStarted extends ImmutablePureComponent {
     }
 
     if (myAccount.get('locked')) {
-      navItems.push(<ColumnLink key='5' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
+      navItems.push(<ColumnLink key='5' icon='users' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
     }
 
     navItems.push(<ColumnLink key='6' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />);
@@ -129,7 +152,7 @@ export default class GettingStarted extends ImmutablePureComponent {
             {listItems}
             <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
             <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
-            <ColumnLink icon='cogs' text={intl.formatMessage(messages.settings)} onClick={this.openSettings} />
+            <ColumnLink icon='cogs' text={intl.formatMessage(messages.settings)} onClick={openSettings} />
             <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
           </div>
 
diff --git a/app/javascript/flavours/glitch/features/report/components/status_check_box.js b/app/javascript/flavours/glitch/features/report/components/status_check_box.js
index cc9232201..d72a0fd07 100644
--- a/app/javascript/flavours/glitch/features/report/components/status_check_box.js
+++ b/app/javascript/flavours/glitch/features/report/components/status_check_box.js
@@ -2,6 +2,10 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import Toggle from 'react-toggle';
+import noop from 'lodash/noop';
+import StatusContent from 'flavours/glitch/components/status_content';
+import { MediaGallery, Video } from 'flavours/glitch/util/async-components';
+import Bundle from 'flavours/glitch/features/ui/components/bundle';
 
 export default class StatusCheckBox extends React.PureComponent {
 
@@ -14,18 +18,50 @@ export default class StatusCheckBox extends React.PureComponent {
 
   render () {
     const { status, checked, onToggle, disabled } = this.props;
-    const content = { __html: status.get('contentHtml') };
+    let media = null;
 
     if (status.get('reblog')) {
       return null;
     }
 
+    if (status.get('media_attachments').size > 0) {
+      if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
+
+      } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
+        const video = status.getIn(['media_attachments', 0]);
+
+        media = (
+          <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
+            {Component => (
+              <Component
+                preview={video.get('preview_url')}
+                src={video.get('url')}
+                width={239}
+                height={110}
+                inline
+                sensitive={status.get('sensitive')}
+                onOpenVideo={noop}
+              />
+            )}
+          </Bundle>
+        );
+      } else {
+        media = (
+          <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
+            {Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={noop} />}
+          </Bundle>
+        );
+      }
+    }
+
     return (
       <div className='status-check-box'>
-        <div
-          className='status__content'
-          dangerouslySetInnerHTML={content}
-        />
+        <div className='status-check-box__status'>
+          <StatusContent
+            status={status}
+            media={media}
+          />
+        </div>
 
         <div className='status-check-box-toggle'>
           <Toggle checked={checked} onChange={onToggle} disabled={disabled} />
diff --git a/app/javascript/flavours/glitch/features/ui/components/column_link.js b/app/javascript/flavours/glitch/features/ui/components/column_link.js
index b845d1895..b058aa963 100644
--- a/app/javascript/flavours/glitch/features/ui/components/column_link.js
+++ b/app/javascript/flavours/glitch/features/ui/components/column_link.js
@@ -2,12 +2,15 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { Link } from 'react-router-dom';
 
-const ColumnLink = ({ icon, text, to, onClick, href, method }) => {
+const ColumnLink = ({ icon, text, to, onClick, href, method, badge }) => {
+  const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null;
+
   if (href) {
     return (
       <a href={href} className='column-link' data-method={method}>
         <i className={`fa fa-fw fa-${icon} column-link__icon`} />
         {text}
+        {badgeElement}
       </a>
     );
   } else if (to) {
@@ -15,6 +18,7 @@ const ColumnLink = ({ icon, text, to, onClick, href, method }) => {
       <Link to={to} className='column-link'>
         <i className={`fa fa-fw fa-${icon} column-link__icon`} />
         {text}
+        {badgeElement}
       </Link>
     );
   } else {
@@ -22,6 +26,7 @@ const ColumnLink = ({ icon, text, to, onClick, href, method }) => {
       <a onClick={onClick} className='column-link' role='button' tabIndex='0' data-method={method}>
         <i className={`fa fa-fw fa-${icon} column-link__icon`} />
         {text}
+        {badgeElement}
       </a>
     );
   }
@@ -34,6 +39,7 @@ ColumnLink.propTypes = {
   onClick: PropTypes.func,
   href: PropTypes.string,
   method: PropTypes.string,
+  badge: PropTypes.node,
 };
 
 export default ColumnLink;
diff --git a/app/javascript/flavours/glitch/features/ui/components/report_modal.js b/app/javascript/flavours/glitch/features/ui/components/report_modal.js
index b4dc1e3d6..b5fc33d03 100644
--- a/app/javascript/flavours/glitch/features/ui/components/report_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/report_modal.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import { connect } from 'react-redux';
-import { changeReportComment, submitReport } from 'flavours/glitch/actions/reports';
+import { changeReportComment, changeReportForward, submitReport } from 'flavours/glitch/actions/reports';
 import { refreshAccountTimeline } from 'flavours/glitch/actions/timelines';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
@@ -10,8 +10,11 @@ import StatusCheckBox from 'flavours/glitch/features/report/containers/status_ch
 import { OrderedSet } from 'immutable';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import Button from 'flavours/glitch/components/button';
+import Toggle from 'react-toggle';
+import IconButton from '../../../components/icon_button';
 
 const messages = defineMessages({
+  close: { id: 'lightbox.close', defaultMessage: 'Close' },
   placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
   submit: { id: 'report.submit', defaultMessage: 'Submit' },
 });
@@ -26,6 +29,7 @@ const makeMapStateToProps = () => {
       isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
       account: getAccount(state, accountId),
       comment: state.getIn(['reports', 'new', 'comment']),
+      forward: state.getIn(['reports', 'new', 'forward']),
       statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])),
     };
   };
@@ -42,14 +46,19 @@ export default class ReportModal extends ImmutablePureComponent {
     account: ImmutablePropTypes.map,
     statusIds: ImmutablePropTypes.orderedSet.isRequired,
     comment: PropTypes.string.isRequired,
+    forward: PropTypes.bool,
     dispatch: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
   };
 
-  handleCommentChange = (e) => {
+  handleCommentChange = e => {
     this.props.dispatch(changeReportComment(e.target.value));
   }
 
+  handleForwardChange = e => {
+    this.props.dispatch(changeReportForward(e.target.checked));
+  }
+
   handleSubmit = () => {
     this.props.dispatch(submitReport());
   }
@@ -65,26 +74,25 @@ export default class ReportModal extends ImmutablePureComponent {
   }
 
   render () {
-    const { account, comment, intl, statusIds, isSubmitting } = this.props;
+    const { account, comment, intl, statusIds, isSubmitting, forward, onClose } = this.props;
 
     if (!account) {
       return null;
     }
 
+    const domain = account.get('acct').split('@')[1];
+
     return (
       <div className='modal-root__modal report-modal'>
         <div className='report-modal__target'>
+          <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} />
           <FormattedMessage id='report.target' defaultMessage='Report {target}' values={{ target: <strong>{account.get('acct')}</strong> }} />
         </div>
 
         <div className='report-modal__container'>
-          <div className='report-modal__statuses'>
-            <div>
-              {statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)}
-            </div>
-          </div>
-
           <div className='report-modal__comment'>
+            <p><FormattedMessage id='report.hint' defaultMessage='The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:' /></p>
+
             <textarea
               className='setting-text light'
               placeholder={intl.formatMessage(messages.placeholder)}
@@ -92,11 +100,26 @@ export default class ReportModal extends ImmutablePureComponent {
               onChange={this.handleCommentChange}
               disabled={isSubmitting}
             />
+
+            {domain && (
+              <div>
+                <p><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' /></p>
+
+                <div className='setting-toggle'>
+                  <Toggle id='report-forward' checked={forward} disabled={isSubmitting} onChange={this.handleForwardChange} />
+                  <label htmlFor='report-forward' className='setting-toggle__label'><FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} /></label>
+                </div>
+              </div>
+            )}
+
+            <Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} />
           </div>
-        </div>
 
-        <div className='report-modal__action-bar'>
-          <Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} />
+          <div className='report-modal__statuses'>
+            <div>
+              {statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)}
+            </div>
+          </div>
         </div>
       </div>
     );
diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js
index fae705deb..0b031a7f0 100644
--- a/app/javascript/flavours/glitch/features/ui/index.js
+++ b/app/javascript/flavours/glitch/features/ui/index.js
@@ -431,6 +431,7 @@ export default class UI extends React.Component {
               <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
 
               <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} />
+              <WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
               <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} />
               <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} />
               <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} />
diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js
index 722670cf1..8973c7713 100644
--- a/app/javascript/flavours/glitch/reducers/compose.js
+++ b/app/javascript/flavours/glitch/reducers/compose.js
@@ -158,7 +158,6 @@ function appendMedia(state, media) {
     map.update('media_attachments', list => list.push(media));
     map.set('is_uploading', false);
     map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
-    map.update('text', oldText => `${oldText.trim()} ${media.get('text_url')}`);
     map.set('focusDate', new Date());
     map.set('idempotencyKey', uuid());
 
@@ -169,12 +168,10 @@ function appendMedia(state, media) {
 };
 
 function removeMedia(state, mediaId) {
-  const media    = state.get('media_attachments').find(item => item.get('id') === mediaId);
   const prevSize = state.get('media_attachments').size;
 
   return state.withMutations(map => {
     map.update('media_attachments', list => list.filterNot(item => item.get('id') === mediaId));
-    map.update('text', text => text.replace(media.get('text_url'), '').trim());
     map.set('idempotencyKey', uuid());
 
     if (prevSize === 1) {
diff --git a/app/javascript/flavours/glitch/reducers/reports.js b/app/javascript/flavours/glitch/reducers/reports.js
index c18fbcdc6..fdcfb14a0 100644
--- a/app/javascript/flavours/glitch/reducers/reports.js
+++ b/app/javascript/flavours/glitch/reducers/reports.js
@@ -6,6 +6,7 @@ import {
   REPORT_CANCEL,
   REPORT_STATUS_TOGGLE,
   REPORT_COMMENT_CHANGE,
+  REPORT_FORWARD_CHANGE,
 } from 'flavours/glitch/actions/reports';
 import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
 
@@ -15,6 +16,7 @@ const initialState = ImmutableMap({
     account_id: null,
     status_ids: ImmutableSet(),
     comment: '',
+    forward: false,
   }),
 });
 
@@ -42,6 +44,8 @@ export default function reports(state = initialState, action) {
     });
   case REPORT_COMMENT_CHANGE:
     return state.setIn(['new', 'comment'], action.comment);
+  case REPORT_FORWARD_CHANGE:
+    return state.setIn(['new', 'forward'], action.forward);
   case REPORT_SUBMIT_REQUEST:
     return state.setIn(['new', 'isSubmitting'], true);
   case REPORT_SUBMIT_FAIL:
diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss
index 2bc894d25..a86120e08 100644
--- a/app/javascript/flavours/glitch/styles/components/accounts.scss
+++ b/app/javascript/flavours/glitch/styles/components/accounts.scss
@@ -380,64 +380,96 @@
 }
 
 .account-gallery__container {
-  margin: -2px;
-  padding: 4px;
   display: flex;
+  justify-content: center;
   flex-wrap: wrap;
+  padding: 2px;
 }
 
 .account-gallery__item {
-  flex: 1 1 auto;
-  width: calc(100% / 3 - 4px);
-  height: 95px;
-  margin: 2px;
+  flex-grow: 1;
+  width: 50%;
+  overflow: hidden;
+  position: relative;
+
+  &::before {
+    content: "";
+    display: block;
+    padding-top: 100%;
+  }
 
   a {
     display: block;
-    width: 100%;
-    height: 100%;
+    width: calc(100% - 4px);
+    height: calc(100% - 4px);
+    margin: 2px;
+    top: 0;
+    left: 0;
     background-color: $base-overlay-background;
     background-size: cover;
     background-position: center;
-    position: relative;
+    position: absolute;
     color: inherit;
     text-decoration: none;
+    border-radius: 4px;
 
     &:hover,
     &:active,
     &:focus {
       outline: 0;
+
+      &::before {
+        content: "";
+        display: block;
+        width: 100%;
+        height: 100%;
+        background: rgba($base-overlay-background, 0.3);
+        border-radius: 4px;
+      }
     }
   }
 }
 
-.account-section-headline {
-  color: $ui-base-lighter-color;
-  background: lighten($ui-base-color, 2%);
-  border-bottom: 1px solid lighten($ui-base-color, 4%);
-  padding: 15px 10px;
-  font-size: 14px;
-  font-weight: 500;
-  position: relative;
+.account__section-headline {
+  background: darken($ui-base-color, 4%);
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
   cursor: default;
+  display: flex;
 
-  &::before,
-  &::after {
+  a {
     display: block;
-    content: "";
-    position: absolute;
-    bottom: 0;
-    left: 18px;
-    width: 0;
-    height: 0;
-    border-style: solid;
-    border-width: 0 10px 10px;
-    border-color: transparent transparent lighten($ui-base-color, 4%);
-  }
+    flex: 1 1 auto;
+    color: $ui-primary-color;
+    padding: 15px 0;
+    font-size: 14px;
+    font-weight: 500;
+    text-align: center;
+    text-decoration: none;
+    position: relative;
 
-  &::after {
-    bottom: -1px;
-    border-color: transparent transparent $ui-base-color;
+    &.active {
+      color: $ui-secondary-color;
+
+      &::before,
+      &::after {
+        display: block;
+        content: "";
+        position: absolute;
+        bottom: 0;
+        left: 50%;
+        width: 0;
+        height: 0;
+        transform: translateX(-50%);
+        border-style: solid;
+        border-width: 0 10px 10px;
+        border-color: transparent transparent lighten($ui-base-color, 8%);
+      }
+
+      &::after {
+        bottom: -1px;
+        border-color: transparent transparent $ui-base-color;
+      }
+    }
   }
 }
 
diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss
index 10cad345e..3d16c856d 100644
--- a/app/javascript/flavours/glitch/styles/components/index.scss
+++ b/app/javascript/flavours/glitch/styles/components/index.scss
@@ -753,6 +753,17 @@
   }
 }
 
+.column-link__badge {
+  display: inline-block;
+  border-radius: 4px;
+  font-size: 12px;
+  line-height: 19px;
+  font-weight: 500;
+  background: $ui-base-color;
+  padding: 4px 8px;
+  margin: -6px 10px;
+}
+
 .keyboard-shortcuts {
   padding: 8px 0 0;
   overflow: hidden;
diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss
index 4dd748227..d7407cdaf 100644
--- a/app/javascript/flavours/glitch/styles/components/media.scss
+++ b/app/javascript/flavours/glitch/styles/components/media.scss
@@ -102,6 +102,7 @@
   &.standalone {
     .media-gallery__item-gifv-thumbnail {
       transform: none;
+      top: 0;
     }
   }
 }
diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss
index 1ac399fa1..d424b1eda 100644
--- a/app/javascript/flavours/glitch/styles/components/modal.scss
+++ b/app/javascript/flavours/glitch/styles/components/modal.scss
@@ -476,8 +476,7 @@
 .boost-modal__action-bar,
 .favourite-modal__action-bar,
 .confirmation-modal__action-bar,
-.mute-modal__action-bar,
-.report-modal__action-bar {
+.mute-modal__action-bar {
   display: flex;
   justify-content: space-between;
   background: $ui-secondary-color;
@@ -523,21 +522,103 @@
   vertical-align: middle;
 }
 
+.report-modal {
+  width: 90vw;
+  max-width: 700px;
+}
+
+.report-modal__container {
+  display: flex;
+  border-top: 1px solid $ui-secondary-color;
+
+  @media screen and (max-width: 480px) {
+    flex-wrap: wrap;
+    overflow-y: auto;
+  }
+}
+
 .report-modal__statuses,
 .report-modal__comment {
-  padding: 10px;
+  box-sizing: border-box;
+  width: 50%;
+
+  @media screen and (max-width: 480px) {
+    width: 100%;
+  }
 }
 
 .report-modal__statuses {
+  flex: 1 1 auto;
   min-height: 20vh;
   max-height: 40vh;
   overflow-y: auto;
   overflow-x: hidden;
+
+  @media screen and (max-width: 480px) {
+    max-height: 10vh;
+  }
 }
 
 .report-modal__comment {
+  padding: 20px;
+  border-right: 1px solid $ui-secondary-color;
+  max-width: 320px;
+
+  p {
+    font-size: 14px;
+    line-height: 20px;
+    margin-bottom: 20px;
+  }
+
   .setting-text {
-    margin-top: 10px;
+    display: block;
+    box-sizing: border-box;
+    width: 100%;
+    margin: 0;
+    color: $ui-base-color;
+    background: $white;
+    padding: 10px;
+    font-family: inherit;
+    font-size: 14px;
+    resize: vertical;
+    border: 0;
+    outline: 0;
+    border-radius: 4px;
+    border: 1px solid $ui-secondary-color;
+    margin-bottom: 20px;
+
+    &:focus {
+      border: 1px solid darken($ui-secondary-color, 8%);
+    }
+  }
+
+  .setting-toggle {
+    margin-top: 20px;
+    margin-bottom: 24px;
+
+    &__label {
+      color: $ui-base-color;
+      font-size: 14px;
+    }
+  }
+
+  @media screen and (max-width: 480px) {
+    padding: 10px;
+    max-width: 100%;
+    order: 2;
+
+    .setting-toggle {
+      margin-bottom: 4px;
+    }
+  }
+}
+
+.report-modal__target {
+  padding: 20px;
+
+  .media-modal__close {
+    top: 19px;
+    right: 15px;
   }
 }
 
diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss
index 1c5e294f9..15727155b 100644
--- a/app/javascript/flavours/glitch/styles/components/status.scss
+++ b/app/javascript/flavours/glitch/styles/components/status.scss
@@ -8,7 +8,6 @@
   padding: 0 12px;
   font-size: 15px;
   line-height: 20px;
-  color: $primary-text-color;
   word-wrap: break-word;
   font-weight: 400;
   overflow: visible;
@@ -322,18 +321,26 @@
   border-bottom: 1px solid $ui-secondary-color;
   display: flex;
 
-  .status__content {
-    flex: 1 1 auto;
-    padding: 10px;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    white-space: nowrap;
+  .status-check-box__status {
+    margin: 10px 0 10px 10px;
+    flex: 1;
+
+    .media-gallery {
+      max-width: 250px;
+    }
 
     .status__content {
-      color: #3a3a3a;
-      a {
-        color: #005aa9;
-      }
+      padding: 0;
+      white-space: normal;
+    }
+
+    .video-player {
+      margin-top: 8px;
+      max-width: 250px;
+    }
+
+    .media-gallery__item-thumbnail {
+      cursor: default;
     }
   }
 }
diff --git a/app/javascript/flavours/glitch/util/react_router_helpers.js b/app/javascript/flavours/glitch/util/react_router_helpers.js
index 1dba5e9bb..e36c512f3 100644
--- a/app/javascript/flavours/glitch/util/react_router_helpers.js
+++ b/app/javascript/flavours/glitch/util/react_router_helpers.js
@@ -35,14 +35,19 @@ export class WrappedRoute extends React.Component {
     component: PropTypes.func.isRequired,
     content: PropTypes.node,
     multiColumn: PropTypes.bool,
+    componentParams: PropTypes.object,
   }
 
+  static defaultProps = {
+    componentParams: {},
+  };
+
   renderComponent = ({ match }) => {
-    const { component, content, multiColumn } = this.props;
+    const { component, content, multiColumn, componentParams } = this.props;
 
     return (
       <BundleContainer fetchComponent={component} loading={this.renderLoading} error={this.renderError}>
-        {Component => <Component params={match.params} multiColumn={multiColumn}>{content}</Component>}
+        {Component => <Component params={match.params} multiColumn={multiColumn} {...componentParams}>{content}</Component>}
       </BundleContainer>
     );
   }
diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js
index 7b5f4bd9c..10e66910a 100644
--- a/app/javascript/mastodon/actions/interactions.js
+++ b/app/javascript/mastodon/actions/interactions.js
@@ -62,6 +62,7 @@ export function reblogRequest(status) {
   return {
     type: REBLOG_REQUEST,
     status: status,
+    skipLoading: true,
   };
 };
 
@@ -70,6 +71,7 @@ export function reblogSuccess(status, response) {
     type: REBLOG_SUCCESS,
     status: status,
     response: response,
+    skipLoading: true,
   };
 };
 
@@ -78,6 +80,7 @@ export function reblogFail(status, error) {
     type: REBLOG_FAIL,
     status: status,
     error: error,
+    skipLoading: true,
   };
 };
 
@@ -85,6 +88,7 @@ export function unreblogRequest(status) {
   return {
     type: UNREBLOG_REQUEST,
     status: status,
+    skipLoading: true,
   };
 };
 
@@ -93,6 +97,7 @@ export function unreblogSuccess(status, response) {
     type: UNREBLOG_SUCCESS,
     status: status,
     response: response,
+    skipLoading: true,
   };
 };
 
@@ -101,6 +106,7 @@ export function unreblogFail(status, error) {
     type: UNREBLOG_FAIL,
     status: status,
     error: error,
+    skipLoading: true,
   };
 };
 
@@ -132,6 +138,7 @@ export function favouriteRequest(status) {
   return {
     type: FAVOURITE_REQUEST,
     status: status,
+    skipLoading: true,
   };
 };
 
@@ -140,6 +147,7 @@ export function favouriteSuccess(status, response) {
     type: FAVOURITE_SUCCESS,
     status: status,
     response: response,
+    skipLoading: true,
   };
 };
 
@@ -148,6 +156,7 @@ export function favouriteFail(status, error) {
     type: FAVOURITE_FAIL,
     status: status,
     error: error,
+    skipLoading: true,
   };
 };
 
@@ -155,6 +164,7 @@ export function unfavouriteRequest(status) {
   return {
     type: UNFAVOURITE_REQUEST,
     status: status,
+    skipLoading: true,
   };
 };
 
@@ -163,6 +173,7 @@ export function unfavouriteSuccess(status, response) {
     type: UNFAVOURITE_SUCCESS,
     status: status,
     response: response,
+    skipLoading: true,
   };
 };
 
@@ -171,6 +182,7 @@ export function unfavouriteFail(status, error) {
     type: UNFAVOURITE_FAIL,
     status: status,
     error: error,
+    skipLoading: true,
   };
 };
 
@@ -258,6 +270,7 @@ export function pinRequest(status) {
   return {
     type: PIN_REQUEST,
     status,
+    skipLoading: true,
   };
 };
 
@@ -266,6 +279,7 @@ export function pinSuccess(status, response) {
     type: PIN_SUCCESS,
     status,
     response,
+    skipLoading: true,
   };
 };
 
@@ -274,6 +288,7 @@ export function pinFail(status, error) {
     type: PIN_FAIL,
     status,
     error,
+    skipLoading: true,
   };
 };
 
@@ -293,6 +308,7 @@ export function unpinRequest(status) {
   return {
     type: UNPIN_REQUEST,
     status,
+    skipLoading: true,
   };
 };
 
@@ -301,6 +317,7 @@ export function unpinSuccess(status, response) {
     type: UNPIN_SUCCESS,
     status,
     response,
+    skipLoading: true,
   };
 };
 
@@ -309,5 +326,6 @@ export function unpinFail(status, error) {
     type: UNPIN_FAIL,
     status,
     error,
+    skipLoading: true,
   };
 };
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index 502690045..cf9242d0f 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -24,7 +24,7 @@ defineMessages({
 const fetchRelatedRelationships = (dispatch, notifications) => {
   const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id);
 
-  if (accountIds > 0) {
+  if (accountIds.length > 0) {
     dispatch(fetchRelationships(accountIds));
   }
 };
diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js
index 9310e7c96..1cef029d8 100644
--- a/app/javascript/mastodon/components/media_gallery.js
+++ b/app/javascript/mastodon/components/media_gallery.js
@@ -12,26 +12,6 @@ const messages = defineMessages({
   toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
 });
 
-const shiftToPoint = (containerToImageRatio, containerSize, imageSize, focusSize, toMinus) => {
-  const containerCenter = Math.floor(containerSize / 2);
-  const focusFactor     = (focusSize + 1) / 2;
-  const scaledImage     = Math.floor(imageSize / containerToImageRatio);
-
-  let focus = Math.floor(focusFactor * scaledImage);
-
-  if (toMinus) focus = scaledImage - focus;
-
-  let focusOffset = focus - containerCenter;
-
-  const remainder = scaledImage - focus;
-  const containerRemainder = containerSize - containerCenter;
-
-  if (remainder < containerRemainder) focusOffset -= containerRemainder - remainder;
-  if (focusOffset < 0) focusOffset = 0;
-
-  return (focusOffset * -100 / containerSize) + '%';
-};
-
 class Item extends React.PureComponent {
 
   static contextTypes = {
@@ -44,8 +24,6 @@ class Item extends React.PureComponent {
     index: PropTypes.number.isRequired,
     size: PropTypes.number.isRequired,
     onClick: PropTypes.func.isRequired,
-    containerWidth: PropTypes.number,
-    containerHeight: PropTypes.number,
   };
 
   static defaultProps = {
@@ -84,7 +62,7 @@ class Item extends React.PureComponent {
   }
 
   render () {
-    const { attachment, index, size, standalone, containerWidth, containerHeight } = this.props;
+    const { attachment, index, size, standalone } = this.props;
 
     let width  = 50;
     let height = 100;
@@ -143,45 +121,16 @@ class Item extends React.PureComponent {
 
       const originalUrl    = attachment.get('url');
       const originalWidth  = attachment.getIn(['meta', 'original', 'width']);
-      const originalHeight = attachment.getIn(['meta', 'original', 'height']);
 
       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 focusX     = attachment.getIn(['meta', 'focus', 'x']);
-      const focusY     = attachment.getIn(['meta', 'focus', 'y']);
-      const imageStyle = {};
-
-      if (originalWidth && originalHeight && containerWidth && containerHeight && focusX && focusY) {
-        const widthRatio  = originalWidth / (containerWidth * (width / 100));
-        const heightRatio = originalHeight / (containerHeight * (height / 100));
-
-        let hShift = 0;
-        let vShift = 0;
-
-        if (widthRatio > heightRatio) {
-          hShift = shiftToPoint(heightRatio, (containerWidth * (width / 100)), originalWidth, focusX);
-        } else if(widthRatio < heightRatio) {
-          vShift = shiftToPoint(widthRatio, (containerHeight * (height / 100)), originalHeight, focusY, true);
-        }
-
-        if (originalWidth > originalHeight) {
-          imageStyle.height   = '100%';
-          imageStyle.width    = 'auto';
-          imageStyle.minWidth = '100%';
-        } else {
-          imageStyle.height    = 'auto';
-          imageStyle.width     = '100%';
-          imageStyle.minHeight = '100%';
-        }
-
-        imageStyle.top  = vShift;
-        imageStyle.left = hShift;
-      } else {
-        imageStyle.height = '100%';
-      }
+      const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
+      const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
+      const x      = ((focusX /  2) + .5) * 100;
+      const y      = ((focusY / -2) + .5) * 100;
 
       thumbnail = (
         <a
@@ -196,7 +145,7 @@ class Item extends React.PureComponent {
             sizes={sizes}
             alt={attachment.get('description')}
             title={attachment.get('description')}
-            style={imageStyle}
+            style={{ objectPosition: `${x}% ${y}%` }}
           />
         </a>
       );
@@ -320,7 +269,7 @@ export default class MediaGallery extends React.PureComponent {
       if (this.isStandaloneEligible()) {
         children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} />;
       } else {
-        children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} containerWidth={width} containerHeight={style.height} />);
+        children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} />);
       }
     }
 
diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js
index 701b5702c..b6082f008 100644
--- a/app/javascript/mastodon/components/status_content.js
+++ b/app/javascript/mastodon/components/status_content.js
@@ -154,7 +154,7 @@ export default class StatusContent extends React.PureComponent {
       }
 
       return (
-        <div className={classNames} ref={this.setRef} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
+        <div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
           <p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
             <span dangerouslySetInnerHTML={spoilerContent} />
             {' '}
diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js
index f5f2475ea..5e21cf7c6 100644
--- a/app/javascript/mastodon/features/account_timeline/index.js
+++ b/app/javascript/mastodon/features/account_timeline/index.js
@@ -17,7 +17,7 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false })
 
   return {
     statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()),
-    featuredStatusIds: state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()),
+    featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()),
     isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
     hasMore: !!state.getIn(['timelines', `account:${path}`, 'next']),
   };
@@ -40,14 +40,18 @@ export default class AccountTimeline extends ImmutablePureComponent {
     const { params: { accountId }, withReplies } = this.props;
 
     this.props.dispatch(fetchAccount(accountId));
-    this.props.dispatch(refreshAccountFeaturedTimeline(accountId));
+    if (!withReplies) {
+      this.props.dispatch(refreshAccountFeaturedTimeline(accountId));
+    }
     this.props.dispatch(refreshAccountTimeline(accountId, withReplies));
   }
 
   componentWillReceiveProps (nextProps) {
     if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
       this.props.dispatch(fetchAccount(nextProps.params.accountId));
-      this.props.dispatch(refreshAccountFeaturedTimeline(nextProps.params.accountId));
+      if (!nextProps.withReplies) {
+        this.props.dispatch(refreshAccountFeaturedTimeline(nextProps.params.accountId));
+      }
       this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId, nextProps.params.withReplies));
     }
   }
diff --git a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js
index c8e74f5a1..43de8f213 100644
--- a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js
+++ b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js
@@ -9,7 +9,8 @@ import spring from 'react-motion/lib/spring';
 import { injectIntl, defineMessages } from 'react-intl';
 
 const messages = defineMessages({
-  title: { id: 'compose_form.sensitive', defaultMessage: 'Mark media as sensitive' },
+  marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' },
+  unmarked: { id: 'compose_form.sensitive.unmarked', defaultMessage: 'Media is not marked as sensitive' },
 });
 
 const mapStateToProps = state => ({
@@ -50,7 +51,7 @@ class SensitiveButton extends React.PureComponent {
             <div className={className} style={{ transform: `scale(${scale})` }}>
               <IconButton
                 className='compose-form__sensitive-button__icon'
-                title={intl.formatMessage(messages.title)}
+                title={intl.formatMessage(active ? messages.marked : messages.unmarked)}
                 icon={icon}
                 onClick={onClick}
                 size={18}
diff --git a/app/javascript/mastodon/features/compose/containers/spoiler_button_container.js b/app/javascript/mastodon/features/compose/containers/spoiler_button_container.js
index 4179b9706..0b9dc8df3 100644
--- a/app/javascript/mastodon/features/compose/containers/spoiler_button_container.js
+++ b/app/javascript/mastodon/features/compose/containers/spoiler_button_container.js
@@ -4,12 +4,13 @@ import { changeComposeSpoilerness } from '../../../actions/compose';
 import { injectIntl, defineMessages } from 'react-intl';
 
 const messages = defineMessages({
-  title: { id: 'compose_form.spoiler', defaultMessage: 'Hide text behind warning' },
+  marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Text is hidden behind warning' },
+  unmarked: { id: 'compose_form.spoiler.unmarked', defaultMessage: 'Text is not hidden' },
 });
 
 const mapStateToProps = (state, { intl }) => ({
   label: 'CW',
-  title: intl.formatMessage(messages.title),
+  title: intl.formatMessage(state.getIn(['compose', 'spoiler']) ? messages.marked : messages.unmarked),
   active: state.getIn(['compose', 'spoiler']),
   ariaControls: 'cw-spoiler-input',
 });
diff --git a/app/javascript/mastodon/features/compose/containers/warning_container.js b/app/javascript/mastodon/features/compose/containers/warning_container.js
index dbb80dfb0..8ee8ea190 100644
--- a/app/javascript/mastodon/features/compose/containers/warning_container.js
+++ b/app/javascript/mastodon/features/compose/containers/warning_container.js
@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
 import { FormattedMessage } from 'react-intl';
 import { me } from '../../../initial_state';
 
-const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\w*[a-zA-Z]\w*)/i;
+const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\w*[a-zA-Z·]\w*)/i;
 
 const mapStateToProps = state => ({
   needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js
index 138bc4e2e..d5cd854db 100644
--- a/app/javascript/mastodon/features/compose/index.js
+++ b/app/javascript/mastodon/features/compose/index.js
@@ -97,7 +97,7 @@ export default class Compose extends React.PureComponent {
             <ComposeFormContainer />
             {multiColumn && (
               <div className='drawer__inner__mastodon'>
-                <img alt='' src={elephantUIPlane} />
+                <img alt='' draggable='false' src={elephantUIPlane} />
               </div>
             )}
           </div>
diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js
index 1038e1864..21bf6d81b 100644
--- a/app/javascript/mastodon/features/ui/components/focal_point_modal.js
+++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.js
@@ -103,7 +103,7 @@ export default class FocalPointModal extends ImmutablePureComponent {
     const height = media.getIn(['meta', 'original', 'height']) || null;
 
     return (
-      <div className='modal-root__modal video-modal'>
+      <div className='modal-root__modal video-modal focal-point-modal'>
         <div className={classNames('focal-point', { dragging })} ref={this.setRef}>
           <ImageLoader
             previewSrc={media.get('preview_url')}
diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js
index 72ef32256..fb76270fa 100644
--- a/app/javascript/mastodon/features/ui/components/media_modal.js
+++ b/app/javascript/mastodon/features/ui/components/media_modal.js
@@ -130,6 +130,15 @@ export default class MediaModal extends ImmutablePureComponent {
       return null;
     }).toArray();
 
+    // you can't use 100vh, because the viewport height is taller
+    // than the visible part of the document in some mobile
+    // browsers when it's address bar is visible.
+    // https://developers.google.com/web/updates/2016/12/url-bar-resizing
+    const swipeableViewsStyle = {
+      width: '100%',
+      height: '100%',
+    };
+
     const containerStyle = {
       alignItems: 'center', // center vertically
     };
@@ -145,23 +154,15 @@ export default class MediaModal extends ImmutablePureComponent {
           role='presentation'
           onClick={onClose}
         >
-          <div className='media-modal__content'>
-            <ReactSwipeableViews
-              style={{
-                // you can't use 100vh, because the viewport height is taller
-                // than the visible part of the document in some mobile
-                // browsers when it's address bar is visible.
-                // https://developers.google.com/web/updates/2016/12/url-bar-resizing
-                height: `${document.body.clientHeight}px`,
-              }}
-              containerStyle={containerStyle}
-              onChangeIndex={this.handleSwipe}
-              onSwitching={this.handleSwitching}
-              index={index}
-            >
-              {content}
-            </ReactSwipeableViews>
-          </div>
+          <ReactSwipeableViews
+            style={swipeableViewsStyle}
+            containerStyle={containerStyle}
+            onChangeIndex={this.handleSwipe}
+            onSwitching={this.handleSwitching}
+            index={index}
+          >
+            {content}
+          </ReactSwipeableViews>
         </div>
         <div className={navigationClassName}>
           <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={40} />
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 33e223b2a..4928930fe 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -3,7 +3,7 @@
   "account.block_domain": "إخفاء كل شيئ قادم من إسم النطاق {domain}",
   "account.blocked": "محظور",
   "account.disclaimer_full": "قد لا تعكس المعلومات أدناه الملف الشخصي الكامل للمستخدم.",
-  "account.domain_blocked": "Domain hidden",
+  "account.domain_blocked": "النطاق مخفي",
   "account.edit_profile": "تعديل الملف الشخصي",
   "account.follow": "تابِع",
   "account.followers": "المتابعون",
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "فيمَ تفكّر؟",
   "compose_form.publish": "بوّق",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "ضع علامة على الوسيط باعتباره حسّاس",
-  "compose_form.spoiler": "أخفِ النص واعرض تحذيرا",
+  "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.spoiler_placeholder": "تنبيه عن المحتوى",
   "confirmation_modal.cancel": "إلغاء",
   "confirmations.block.confirm": "حجب",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "محتوى حساس",
   "status.share": "مشاركة",
   "status.show_less": "إعرض أقلّ",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "أظهر المزيد",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "فك الكتم عن المحادثة",
   "status.unpin": "فك التدبيس من الملف الشخصي",
   "tabs_bar.federated_timeline": "الموحَّد",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index a84e6e9d1..1dee16748 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "Какво си мислиш?",
   "compose_form.publish": "Раздумай",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Отбележи съдържанието като деликатно",
-  "compose_form.spoiler": "Скрий текста зад предупреждение",
+  "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.spoiler_placeholder": "Content warning",
   "confirmation_modal.cancel": "Cancel",
   "confirmations.block.confirm": "Block",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Деликатно съдържание",
   "status.share": "Share",
   "status.show_less": "Show less",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Show more",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Unmute conversation",
   "status.unpin": "Unpin from profile",
   "tabs_bar.federated_timeline": "Federated",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index bac807dbb..4923c1032 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "En què estàs pensant?",
   "compose_form.publish": "Toot",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Marca el contingut multimèdia com a sensible",
-  "compose_form.spoiler": "Amaga el text darrera darrere un avís",
+  "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.spoiler_placeholder": "Escriu l'avís aquí",
   "confirmation_modal.cancel": "Cancel·la",
   "confirmations.block.confirm": "Bloca",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Contingut sensible",
   "status.share": "Compartir",
   "status.show_less": "Mostra menys",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Mostra més",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Activar conversació",
   "status.unpin": "Deslliga del perfil",
   "tabs_bar.federated_timeline": "Federada",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 6267b1d71..e0fc0ee85 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "Worüber möchtest du schreiben?",
   "compose_form.publish": "Tröt",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Medien als heikel markieren",
-  "compose_form.spoiler": "Text hinter Warnung verbergen",
+  "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.spoiler_placeholder": "Inhaltswarnung",
   "confirmation_modal.cancel": "Abbrechen",
   "confirmations.block.confirm": "Blockieren",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Heikle Inhalte",
   "status.share": "Teilen",
   "status.show_less": "Weniger anzeigen",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Mehr anzeigen",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Stummschaltung von Thread aufheben",
   "status.unpin": "Vom Profil lösen",
   "tabs_bar.federated_timeline": "Föderation",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index c8ebf4b87..b61fc5eaf 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -768,8 +768,12 @@
   {
     "descriptors": [
       {
-        "defaultMessage": "Mark media as sensitive",
-        "id": "compose_form.sensitive"
+        "defaultMessage": "Media is marked as sensitive",
+        "id": "compose_form.sensitive.marked"
+      },
+      {
+        "defaultMessage": "Media is not marked as sensitive",
+        "id": "compose_form.sensitive.unmarked"
       }
     ],
     "path": "app/javascript/mastodon/features/compose/containers/sensitive_button_container.json"
@@ -777,8 +781,12 @@
   {
     "descriptors": [
       {
-        "defaultMessage": "Hide text behind warning",
-        "id": "compose_form.spoiler"
+        "defaultMessage": "Text is hidden behind warning",
+        "id": "compose_form.spoiler.marked"
+      },
+      {
+        "defaultMessage": "Text is not hidden",
+        "id": "compose_form.spoiler.unmarked"
       }
     ],
     "path": "app/javascript/mastodon/features/compose/containers/spoiler_button_container.json"
@@ -1376,6 +1384,14 @@
         "id": "confirmations.block.confirm"
       },
       {
+        "defaultMessage": "Show more for all",
+        "id": "status.show_more_all"
+      },
+      {
+        "defaultMessage": "Show less for all",
+        "id": "status.show_less_all"
+      },
+      {
         "defaultMessage": "Are you sure you want to block {name}?",
         "id": "confirmations.block.message"
       }
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index e6e0b012b..67453e170 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -64,8 +64,10 @@
   "compose_form.placeholder": "What is on your mind?",
   "compose_form.publish": "Toot",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Mark media as sensitive",
-  "compose_form.spoiler": "Hide text behind warning",
+  "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.spoiler_placeholder": "Write your warning here",
   "confirmation_modal.cancel": "Cancel",
   "confirmations.block.confirm": "Block",
@@ -259,7 +261,9 @@
   "status.sensitive_warning": "Sensitive content",
   "status.share": "Share",
   "status.show_less": "Show less",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Show more",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Unmute conversation",
   "status.unpin": "Unpin from profile",
   "tabs_bar.federated_timeline": "Federated",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 4f465bf40..fd687e8b1 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "Pri kio vi pensas?",
   "compose_form.publish": "Hup",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Marki aŭdovidaĵon tikla",
-  "compose_form.spoiler": "Kaŝi tekston malantaŭ averto",
+  "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.spoiler_placeholder": "Skribu vian averton ĉi tie",
   "confirmation_modal.cancel": "Nuligi",
   "confirmations.block.confirm": "Bloki",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Tikla enhavo",
   "status.share": "Diskonigi",
   "status.show_less": "Malgrandigi",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Grandigi",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Malsilentigi konversacion",
   "status.unpin": "Depingli de profilo",
   "tabs_bar.federated_timeline": "Fratara tempolinio",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index d172dff1c..2107a1525 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -1,9 +1,9 @@
 {
   "account.block": "Bloquear",
   "account.block_domain": "Ocultar todo de {domain}",
-  "account.blocked": "Blocked",
+  "account.blocked": "Bloqueado",
   "account.disclaimer_full": "La siguiente información del usuario puede estar incompleta.",
-  "account.domain_blocked": "Domain hidden",
+  "account.domain_blocked": "Dominio oculto",
   "account.edit_profile": "Editar perfil",
   "account.follow": "Seguir",
   "account.followers": "Seguidores",
@@ -15,9 +15,9 @@
   "account.moved_to": "{name} se ha mudado a:",
   "account.mute": "Silenciar a @{name}",
   "account.mute_notifications": "Silenciar notificaciones de @{name}",
-  "account.muted": "Muted",
-  "account.posts": "Publicaciones",
-  "account.posts_with_replies": "Toots with replies",
+  "account.muted": "Silenciado",
+  "account.posts": "Toots",
+  "account.posts_with_replies": "Toots con respuestas",
   "account.report": "Reportar a @{name}",
   "account.requested": "Esperando aprobación",
   "account.share": "Compartir el perfil de @{name}",
@@ -54,14 +54,16 @@
   "column_header.unpin": "Dejar de fijar",
   "column_subheading.navigation": "Navegación",
   "column_subheading.settings": "Ajustes",
-  "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
+  "compose_form.hashtag_warning": "Este toot no se mostrará bajo hashtags porque no es público. Sólo los toots públicos se pueden buscar por hashtag.",
   "compose_form.lock_disclaimer": "Tu cuenta no está bloqueada. Todos pueden seguirte para ver tus toots solo para seguidores.",
   "compose_form.lock_disclaimer.lock": "bloqueado",
   "compose_form.placeholder": "¿En qué estás pensando?",
   "compose_form.publish": "Tootear",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Marcar contenido como sensible",
-  "compose_form.spoiler": "Ocultar texto tras una advertencia",
+  "compose_form.sensitive.marked": "Material marcado como sensible",
+  "compose_form.sensitive.unmarked": "Material no marcado como sensible",
+  "compose_form.spoiler.marked": "Texto oculto tras la advertencia",
+  "compose_form.spoiler.unmarked": "Texto no oculto",
   "compose_form.spoiler_placeholder": "Advertencia de contenido",
   "confirmation_modal.cancel": "Cancelar",
   "confirmations.block.confirm": "Bloquear",
@@ -69,7 +71,7 @@
   "confirmations.delete.confirm": "Eliminar",
   "confirmations.delete.message": "¿Estás seguro de que quieres borrar este toot?",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.message": "¿Seguro que quieres borrar esta lista permanentemente?",
   "confirmations.domain_block.confirm": "Ocultar dominio entero",
   "confirmations.domain_block.message": "¿Seguro de que quieres bloquear al dominio entero? En algunos casos es preferible bloquear o silenciar objetivos determinados.",
   "confirmations.mute.confirm": "Silenciar",
@@ -132,18 +134,18 @@
   "lightbox.close": "Cerrar",
   "lightbox.next": "Siguiente",
   "lightbox.previous": "Anterior",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
+  "lists.account.add": "Añadir a lista",
+  "lists.account.remove": "Quitar de lista",
   "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among people you follow",
-  "lists.subheading": "Your lists",
+  "lists.edit": "Editar lista",
+  "lists.new.create": "Añadir lista",
+  "lists.new.title_placeholder": "Título de la nueva lista",
+  "lists.search": "Buscar entre la gente a la que sigues",
+  "lists.subheading": "Tus listas",
   "loading_indicator.label": "Cargando…",
   "media_gallery.toggle_visible": "Cambiar visibilidad",
   "missing_indicator.label": "No encontrado",
-  "missing_indicator.sublabel": "This resource could not be found",
+  "missing_indicator.sublabel": "No se encontró este recurso",
   "mute_modal.hide_notifications": "Ocultar notificaciones de este usuario?",
   "navigation_bar.blocks": "Usuarios bloqueados",
   "navigation_bar.community_timeline": "Historia local",
@@ -152,7 +154,7 @@
   "navigation_bar.follow_requests": "Solicitudes para seguirte",
   "navigation_bar.info": "Información adicional",
   "navigation_bar.keyboard_shortcuts": "Atajos de teclado",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "Listas",
   "navigation_bar.logout": "Cerrar sesión",
   "navigation_bar.mutes": "Usuarios silenciados",
   "navigation_bar.pins": "Toots fijados",
@@ -179,8 +181,8 @@
   "onboarding.page_four.home": "La línea de tiempo principal muestra toots de gente que sigues.",
   "onboarding.page_four.notifications": "Las notificaciones se muestran cuando alguien interactúa contigo.",
   "onboarding.page_one.federation": "Mastodon es una red de servidores federados que conforman una red social aún más grande. Llamamos a estos servidores instancias.",
-  "onboarding.page_one.full_handle": "Your full handle",
-  "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.",
+  "onboarding.page_one.full_handle": "Tu sobrenombre completo",
+  "onboarding.page_one.handle_hint": "Esto es lo que dirías a tus amistades que buscaran.",
   "onboarding.page_one.welcome": "¡Bienvenido a Mastodon!",
   "onboarding.page_six.admin": "El administrador de tu instancia es {admin}.",
   "onboarding.page_six.almost_done": "Ya casi…",
@@ -203,28 +205,28 @@
   "privacy.public.short": "Público",
   "privacy.unlisted.long": "No mostrar en la historia federada",
   "privacy.unlisted.short": "Sin federar",
-  "regeneration_indicator.label": "Loading…",
-  "regeneration_indicator.sublabel": "Your home feed is being prepared!",
+  "regeneration_indicator.label": "Cargando…",
+  "regeneration_indicator.sublabel": "¡Tu historia de inicio se está preparando!",
   "relative_time.days": "{number}d",
   "relative_time.hours": "{number}h",
   "relative_time.just_now": "ahora",
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Cancelar",
-  "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": "Reenviar a {target}",
+  "report.forward_hint": "Esta cuenta es de otro servidor. ¿Enviar una copia anonimizada del informe allí también?",
+  "report.hint": "El informe se enviará a los moderadores de tu instancia. Puedes proporcionar una explicación de por qué informas sobre esta cuenta a continuación:",
   "report.placeholder": "Comentarios adicionales",
   "report.submit": "Publicar",
   "report.target": "Reportando",
   "search.placeholder": "Buscar",
   "search_popout.search_format": "Formato de búsqueda avanzada",
-  "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": "Búsquedas de texto recuperan posts que has escrito, marcado como favoritos, retooteado o en los que has sido mencionado, así como usuarios, nombres y hashtags.",
   "search_popout.tips.hashtag": "etiqueta",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "El texto simple devuelve correspondencias de nombre, usuario y hashtag",
   "search_popout.tips.user": "usuario",
-  "search_results.accounts": "People",
+  "search_results.accounts": "Gente",
   "search_results.hashtags": "Hashtags",
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
@@ -238,11 +240,11 @@
   "status.media_hidden": "Contenido multimedia oculto",
   "status.mention": "Mencionar",
   "status.more": "Más",
-  "status.mute": "Mute @{name}",
+  "status.mute": "Silenciar @{name}",
   "status.mute_conversation": "Silenciar conversación",
   "status.open": "Expandir estado",
   "status.pin": "Fijar",
-  "status.pinned": "Pinned toot",
+  "status.pinned": "Toot fijado",
   "status.reblog": "Retootear",
   "status.reblogged_by": "Retooteado por {name}",
   "status.reply": "Responder",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Contenido sensible",
   "status.share": "Compartir",
   "status.show_less": "Mostrar menos",
+  "status.show_less_all": "Mostrar menos para todo",
   "status.show_more": "Mostrar más",
+  "status.show_more_all": "Mostrar más para todo",
   "status.unmute_conversation": "Dejar de silenciar conversación",
   "status.unpin": "Dejar de fijar",
   "tabs_bar.federated_timeline": "Federado",
@@ -263,7 +267,7 @@
   "upload_area.title": "Arrastra y suelta para subir",
   "upload_button.label": "Subir multimedia",
   "upload_form.description": "Describir para los usuarios con dificultad visual",
-  "upload_form.focus": "Crop",
+  "upload_form.focus": "Recortar",
   "upload_form.undo": "Deshacer",
   "upload_progress.label": "Subiendo…",
   "video.close": "Cerrar video",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index b5b81bff9..455dc5d9f 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "تازه چه خبر؟",
   "compose_form.publish": "بوق",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "تصاویر حساس هستند",
-  "compose_form.spoiler": "نوشته را پشت هشدار پنهان کنید",
+  "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.spoiler_placeholder": "هشدار محتوا",
   "confirmation_modal.cancel": "بی‌خیال",
   "confirmations.block.confirm": "مسدود کن",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "محتوای حساس",
   "status.share": "هم‌رسانی",
   "status.show_less": "نهفتن",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "نمایش",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "باصداکردن گفتگو",
   "status.unpin": "برداشتن نوشتهٔ ثابت نمایه",
   "tabs_bar.federated_timeline": "همگانی",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index aa97aae84..1dea42ed4 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -1,7 +1,7 @@
 {
   "account.block": "Estä @{name}",
   "account.block_domain": "Piilota kaikki sisältö verkkotunnuksesta {domain}",
-  "account.blocked": "Blocked",
+  "account.blocked": "Estetty",
   "account.disclaimer_full": "Alla olevat käyttäjän profiilitiedot saattavat olla epätäydellisiä.",
   "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Muokkaa",
@@ -15,9 +15,9 @@
   "account.moved_to": "{name} on muuttanut instanssiin:",
   "account.mute": "Mykistä @{name}",
   "account.mute_notifications": "Mykistä ilmoitukset käyttäjältä @{name}",
-  "account.muted": "Muted",
+  "account.muted": "Mykistetty",
   "account.posts": "Töötit",
-  "account.posts_with_replies": "Toots with replies",
+  "account.posts_with_replies": "Töötit ja vastaukset",
   "account.report": "Report @{name}",
   "account.requested": "Odottaa hyväksyntää. Klikkaa peruuttaaksesi seurauspyynnön",
   "account.share": "Jaa käyttäjän @{name} profiili",
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "Mitä sinulla on mielessä?",
   "compose_form.publish": "Toot",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Merkitse media herkäksi",
-  "compose_form.spoiler": "Piiloita teksti varoituksen taakse",
+  "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.spoiler_placeholder": "Content warning",
   "confirmation_modal.cancel": "Peruuta",
   "confirmations.block.confirm": "Estä",
@@ -211,22 +213,22 @@
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Peruuta",
-  "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": "Uudelleenohjaa kohteeseen {target}",
+  "report.forward_hint": "Tämä tili on toiselta serveriltä. Haluatko, että myös sinne lähetetään anonymisoitu kopio ilmiantoraportista?",
+  "report.hint": "Ilmianto lähetetään instanssisi moderaattoreille. Voit antaa kuvauksen käyttäjän ilmiantamisen syystä alle:",
   "report.placeholder": "Lisäkommentit",
   "report.submit": "Submit",
   "report.target": "Reporting",
   "search.placeholder": "Hae",
   "search_popout.search_format": "Tarkennettu haku",
-  "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": "Tekstihaku palauttaa statuspäivitykset jotka olet kirjoittanut, lisännyt suosikkeihisi, boostannut tai joissa sinut mainitaan, sekä käyttäjänimet, nimimerkit ja hastagit jotka sisältävät tekstin.",
   "search_popout.tips.hashtag": "hashtagi",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Pelkkä tekstihaku palauttaa hakua vastaavat nimimerkit, käyttäjänimet ja hastagit",
   "search_popout.tips.user": "käyttäjä",
-  "search_results.accounts": "People",
-  "search_results.hashtags": "Hashtags",
-  "search_results.statuses": "Toots",
+  "search_results.accounts": "Ihmiset",
+  "search_results.hashtags": "Hashtagit",
+  "search_results.statuses": "Töötit",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "standalone.public_title": "Kurkistus sisälle...",
   "status.block": "Block @{name}",
@@ -242,7 +244,7 @@
   "status.mute_conversation": "Mykistä keskustelu",
   "status.open": "Laajenna statuspäivitys",
   "status.pin": "Kiinnitä profiiliin",
-  "status.pinned": "Pinned toot",
+  "status.pinned": "Kiinnitetty töötti",
   "status.reblog": "Buustaa",
   "status.reblogged_by": "{name} buustasi",
   "status.reply": "Vastaa",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Arkaluontoista sisältöä",
   "status.share": "Jaa",
   "status.show_less": "Näytä vähemmän",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Näytä lisää",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Poista mykistys keskustelulta",
   "status.unpin": "Irrota profiilista",
   "tabs_bar.federated_timeline": "Federated",
@@ -263,7 +267,7 @@
   "upload_area.title": "Raahaa ja pudota tähän ladataksesi",
   "upload_button.label": "Lisää mediaa",
   "upload_form.description": "Anna kuvaus näkörajoitteisia varten",
-  "upload_form.focus": "Crop",
+  "upload_form.focus": "Rajaa",
   "upload_form.undo": "Peru",
   "upload_progress.label": "Ladataan...",
   "video.close": "Sulje video",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 4df1ef30f..40fd6163e 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "Qu’avez-vous en tête ?",
   "compose_form.publish": "Pouet",
   "compose_form.publish_loud": "{publish} !",
-  "compose_form.sensitive": "Marquer le média comme sensible",
-  "compose_form.spoiler": "Masquer le texte derrière un avertissement",
+  "compose_form.sensitive.marked": "Média marqué comme sensible",
+  "compose_form.sensitive.unmarked": "Média non marqué comme sensible",
+  "compose_form.spoiler.marked": "Le texte est caché derrière un avertissement",
+  "compose_form.spoiler.unmarked": "Le texte n'est pas caché",
   "compose_form.spoiler_placeholder": "Écrivez ici votre avertissement",
   "confirmation_modal.cancel": "Annuler",
   "confirmations.block.confirm": "Bloquer",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Contenu sensible",
   "status.share": "Partager",
   "status.show_less": "Replier",
+  "status.show_less_all": "Tout replier",
   "status.show_more": "Déplier",
+  "status.show_more_all": "Tout déplier",
   "status.unmute_conversation": "Ne plus masquer la conversation",
   "status.unpin": "Retirer du profil",
   "tabs_bar.federated_timeline": "Fil public global",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index e222ddaea..edfb9cfcb 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "A qué andas?",
   "compose_form.publish": "Toot",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Marcar medios como sensibles",
-  "compose_form.spoiler": "Agochar texto detrás de un aviso",
+  "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.spoiler_placeholder": "Escriba o aviso aquí",
   "confirmation_modal.cancel": "Cancelar",
   "confirmations.block.confirm": "Bloquear",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Contido sensible",
   "status.share": "Compartir",
   "status.show_less": "Mostrar menos",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Mostrar máis",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Non acalar a conversa",
   "status.unpin": "Despegar do perfil",
   "tabs_bar.federated_timeline": "Federado",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index b31976c42..b637ae414 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "מה עובר לך בראש?",
   "compose_form.publish": "ללחוש",
   "compose_form.publish_loud": "לחצרץ!",
-  "compose_form.sensitive": "סימון תוכן כרגיש",
-  "compose_form.spoiler": "הסתרה מאחורי אזהרת תוכן",
+  "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.spoiler_placeholder": "אזהרת תוכן",
   "confirmation_modal.cancel": "ביטול",
   "confirmations.block.confirm": "לחסום",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "תוכן רגיש",
   "status.share": "שיתוף",
   "status.show_less": "הראה פחות",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "הראה יותר",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "הסרת השתקת שיחה",
   "status.unpin": "לשחרר מקיבוע באודות",
   "tabs_bar.federated_timeline": "ציר זמן בין-קהילתי",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index d176a5df6..4b64d796d 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "Što ti je na umu?",
   "compose_form.publish": "Toot",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Označi media sadržaj kao osjetljiv",
-  "compose_form.spoiler": "Sakrij text iza upozorenja",
+  "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.spoiler_placeholder": "Upozorenje o sadržaju",
   "confirmation_modal.cancel": "Otkaži",
   "confirmations.block.confirm": "Blokiraj",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Osjetljiv sadržaj",
   "status.share": "Share",
   "status.show_less": "Pokaži manje",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Pokaži više",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Poništi utišavanje razgovora",
   "status.unpin": "Unpin from profile",
   "tabs_bar.federated_timeline": "Federalni",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index a4d2091ef..79888e41e 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "Mire gondolsz?",
   "compose_form.publish": "Tülk",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Tartalom érzékenynek jelölése",
-  "compose_form.spoiler": "Szöveg figyelmeztetés mögé rejtése",
+  "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.spoiler_placeholder": "Figyelmeztetését írja ide",
   "confirmation_modal.cancel": "Bezár",
   "confirmations.block.confirm": "Letilt",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Érzékeny tartalom",
   "status.share": "Megosztás",
   "status.show_less": "Kevesebb",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Többet",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Beszélgetés némításának elvonása",
   "status.unpin": "Kitűzés eltávolítása a profilról",
   "tabs_bar.federated_timeline": "Federált",
diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json
index b5e9a2b5a..932ff1565 100644
--- a/app/javascript/mastodon/locales/hy.json
+++ b/app/javascript/mastodon/locales/hy.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "Ի՞նչ կա մտքիդ",
   "compose_form.publish": "Թթել",
   "compose_form.publish_loud": "Թթե՜լ",
-  "compose_form.sensitive": "Նշել բովանդակությունը որպես կասկածելի",
-  "compose_form.spoiler": "Թաքցնել տեքստը նախազգուշացման ետեւում",
+  "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.spoiler_placeholder": "Գրիր նախազգուշացումդ այստեղ",
   "confirmation_modal.cancel": "Չեղարկել",
   "confirmations.block.confirm": "Արգելափակել",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Կասկածելի բովանդակություն",
   "status.share": "Կիսվել",
   "status.show_less": "Պակաս",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Ավելին",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Ապալռեցնել խոսակցությունը",
   "status.unpin": "Հանել անձնական էջից",
   "tabs_bar.federated_timeline": "Դաշնային",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index 596415cde..bc4294679 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -1,131 +1,133 @@
 {
   "account.block": "Blokir @{name}",
-  "account.block_domain": "Hide everything from {domain}",
-  "account.blocked": "Blocked",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
-  "account.domain_blocked": "Domain hidden",
+  "account.block_domain": "Sembunyikan segalanya dari {domain}",
+  "account.blocked": "Terblokir",
+  "account.disclaimer_full": "Informasi di bawah mungkin tidak mencerminkan profil user secara lengkap.",
+  "account.domain_blocked": "Domain disembunyikan",
   "account.edit_profile": "Ubah profil",
   "account.follow": "Ikuti",
   "account.followers": "Pengikut",
   "account.follows": "Mengikuti",
   "account.follows_you": "Mengikuti anda",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "Sembunyikan boosts dari @{name}",
   "account.media": "Media",
   "account.mention": "Balasan @{name}",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} telah pindah ke:",
   "account.mute": "Bisukan @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
-  "account.muted": "Muted",
-  "account.posts": "Postingan",
-  "account.posts_with_replies": "Toots with replies",
+  "account.mute_notifications": "Sembunyikan notifikasi dari @{name}",
+  "account.muted": "Dibisukan",
+  "account.posts": "Toots",
+  "account.posts_with_replies": "Postingan dengan balasan",
   "account.report": "Laporkan @{name}",
-  "account.requested": "Menunggu persetujuan",
-  "account.share": "Share @{name}'s profile",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.requested": "Menunggu persetujuan. Klik untuk membatalkan permintaan",
+  "account.share": "Bagikan profil @{name}",
+  "account.show_reblogs": "Tampilkan boost dari @{name}",
   "account.unblock": "Hapus blokir @{name}",
-  "account.unblock_domain": "Unhide {domain}",
+  "account.unblock_domain": "Tampilkan {domain}",
   "account.unfollow": "Berhenti mengikuti",
   "account.unmute": "Berhenti membisukan @{name}",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
-  "account.view_full_profile": "View full profile",
+  "account.unmute_notifications": "Munculkan notifikasi dari @{name}",
+  "account.view_full_profile": "Lihat profil lengkap",
   "boost_modal.combo": "Anda dapat menekan {combo} untuk melewati ini",
-  "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.body": "Kesalahan terjadi saat memuat komponen ini.",
+  "bundle_column_error.retry": "Coba lagi",
   "bundle_column_error.title": "Network error",
-  "bundle_modal_error.close": "Close",
-  "bundle_modal_error.message": "Something went wrong while loading this component.",
-  "bundle_modal_error.retry": "Try again",
+  "bundle_modal_error.close": "Tutup",
+  "bundle_modal_error.message": "Kesalahan terjadi saat memuat komponen ini.",
+  "bundle_modal_error.retry": "Coba lagi",
   "column.blocks": "Pengguna diblokir",
   "column.community": "Linimasa Lokal",
   "column.favourites": "Favorit",
   "column.follow_requests": "Permintaan mengikuti",
   "column.home": "Beranda",
-  "column.lists": "Lists",
-  "column.mutes": "Pengguna dibisukan",
+  "column.lists": "List",
+  "column.mutes": "Pengguna yang dibisukan",
   "column.notifications": "Notifikasi",
   "column.pins": "Pinned toot",
-  "column.public": "Linimasa gabunggan",
+  "column.public": "Linimasa gabungan",
   "column_back_button.label": "Kembali",
-  "column_header.hide_settings": "Hide settings",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
-  "column_header.pin": "Pin",
-  "column_header.show_settings": "Show settings",
-  "column_header.unpin": "Unpin",
+  "column_header.hide_settings": "Sembunyikan pengaturan",
+  "column_header.moveLeft_settings": "Pindahkan kolom ke kiri",
+  "column_header.moveRight_settings": "Pindahkan kolom ke kanan",
+  "column_header.pin": "Sematkan",
+  "column_header.show_settings": "Tampilkan pengaturan",
+  "column_header.unpin": "Lepaskan",
   "column_subheading.navigation": "Navigasi",
   "column_subheading.settings": "Pengaturan",
-  "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
+  "compose_form.hashtag_warning": "Toot ini tidak akan ada dalam daftar tagar manapun karena telah di set sebagai tidak terdaftar. Hanya postingan publik yang bisa dicari dengan tagar.",
   "compose_form.lock_disclaimer": "Akun anda tidak {locked}. Semua orang dapat mengikuti anda untuk melihat postingan khusus untuk pengikut anda.",
-  "compose_form.lock_disclaimer.lock": "dikunci",
+  "compose_form.lock_disclaimer.lock": "terkunci",
   "compose_form.placeholder": "Apa yang ada di pikiran anda?",
   "compose_form.publish": "Toot",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Tandai media sensitif",
-  "compose_form.spoiler": "Sembunyikan teks dibalik peringatan",
+  "compose_form.sensitive.marked": "Sumber ini telah ditandai sebagai sumber sensitif.",
+  "compose_form.sensitive.unmarked": "Sumber ini tidak ditandai sebagai sumber sensitif",
+  "compose_form.spoiler.marked": "Teks disembunyikan dibalik peringatan",
+  "compose_form.spoiler.unmarked": "Teks tidak tersembunyi",
   "compose_form.spoiler_placeholder": "Peringatan konten",
   "confirmation_modal.cancel": "Batal",
   "confirmations.block.confirm": "Blokir",
   "confirmations.block.message": "Apa anda yakin ingin memblokir {name}?",
   "confirmations.delete.confirm": "Hapus",
-  "confirmations.delete.message": "Apa anda yakin akan menghapus status ini?",
+  "confirmations.delete.message": "Apa anda yakin untuk menghapus status ini?",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
-  "confirmations.domain_block.confirm": "Hide entire domain",
-  "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
+  "confirmations.delete_list.message": "Apakah anda yakin untuk menghapus daftar ini secara permanen?",
+  "confirmations.domain_block.confirm": "Sembunyikan keseluruhan domain",
+  "confirmations.domain_block.message": "Apakah anda benar benar yakin untuk memblokir keseluruhan {domain}? Dalam kasus tertentu beberapa pemblokiran atau penyembunyian lebih baik.",
   "confirmations.mute.confirm": "Bisukan",
   "confirmations.mute.message": "Apa anda yakin ingin membisukan {name}?",
-  "confirmations.unfollow.confirm": "Unfollow",
-  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
+  "confirmations.unfollow.confirm": "Berhenti mengikuti",
+  "confirmations.unfollow.message": "Apakah anda ingin berhenti mengikuti {name}?",
+  "embed.instructions": "Sematkan status ini di website anda dengan menyalin kode di bawah ini.",
+  "embed.preview": "Seperti ini nantinya:",
   "emoji_button.activity": "Aktivitas",
-  "emoji_button.custom": "Custom",
+  "emoji_button.custom": "Kustom",
   "emoji_button.flags": "Bendera",
   "emoji_button.food": "Makanan & Minuman",
   "emoji_button.label": "Tambahkan emoji",
   "emoji_button.nature": "Alam",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "Katakan tidak pada emoji!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Benda-benda",
   "emoji_button.people": "Orang",
-  "emoji_button.recent": "Frequently used",
+  "emoji_button.recent": "Yang sering digunakan",
   "emoji_button.search": "Cari...",
-  "emoji_button.search_results": "Search results",
+  "emoji_button.search_results": "Hasil pencarian",
   "emoji_button.symbols": "Simbol",
   "emoji_button.travel": "Tempat Wisata",
   "empty_column.community": "Linimasa lokal masih kosong. Tulis sesuatu secara publik dan buat roda berputar!",
   "empty_column.hashtag": "Tidak ada apapun dalam hashtag ini.",
-  "empty_column.home": "Anda sedang tidak mengikuti siapapun. Kunjungi {public} atau gunakan pencarian untuk memulai dan bertemu pengguna lain.",
+  "empty_column.home": "Linimasa anda kosong! Kunjungi {public} atau gunakan pencarian untuk memulai dan bertemu pengguna lain.",
   "empty_column.home.public_timeline": "linimasa publik",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "Tidak ada postingan di list ini. Ketika anggota dari list ini memposting status baru, status tersebut akan tampil disini.",
   "empty_column.notifications": "Anda tidak memiliki notifikasi apapun. Berinteraksi dengan orang lain untuk memulai percakapan.",
-  "empty_column.public": "Tidak ada apapun disini! Tulis sesuatu, atau ikuti pengguna lain dari server lain untuk mengisinya secara manual",
+  "empty_column.public": "Tidak ada apapun disini! Tulis sesuatu, atau ikuti pengguna lain dari server lain untuk mengisi ini",
   "follow_request.authorize": "Izinkan",
   "follow_request.reject": "Tolak",
-  "getting_started.appsshort": "Apps",
+  "getting_started.appsshort": "Aplikasi",
   "getting_started.faq": "FAQ",
   "getting_started.heading": "Mulai",
-  "getting_started.open_source_notice": "Mastodon adalah perangkat lunak yang bersifat open source. Anda dapat berkontribusi atau melaporkan permasalahan/bug di Github {github}.",
-  "getting_started.userguide": "User Guide",
+  "getting_started.open_source_notice": "Mastodon adalah perangkat lunak yang bersifat terbuka. Anda dapat berkontribusi atau melaporkan permasalahan/bug di Github {github}.",
+  "getting_started.userguide": "Panduan Pengguna",
   "home.column_settings.advanced": "Tingkat Lanjut",
   "home.column_settings.basic": "Dasar",
-  "home.column_settings.filter_regex": "Penyaringan dengan Regular Expression",
-  "home.column_settings.show_reblogs": "Tampilkan Boost",
+  "home.column_settings.filter_regex": "Saring dengan regular expressions",
+  "home.column_settings.show_reblogs": "Tampilkan boost",
   "home.column_settings.show_replies": "Tampilkan balasan",
   "home.settings": "Pengaturan kolom",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
-  "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
-  "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
-  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.back": "untuk kembali",
+  "keyboard_shortcuts.boost": "untuk menyebarkan",
+  "keyboard_shortcuts.column": "untuk fokus kepada sebuah status di sebuah kolom",
+  "keyboard_shortcuts.compose": "untuk fokus ke area penulisan",
+  "keyboard_shortcuts.description": "Deskripsi",
+  "keyboard_shortcuts.down": "untuk pindah ke bawah dalam sebuah daftar",
+  "keyboard_shortcuts.enter": "untuk membuka status",
+  "keyboard_shortcuts.favourite": "untuk memfavoritkan",
+  "keyboard_shortcuts.heading": "Pintasan keyboard",
   "keyboard_shortcuts.hotkey": "Hotkey",
   "keyboard_shortcuts.legend": "to display this legend",
   "keyboard_shortcuts.mention": "to mention author",
   "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.search": "untuk fokus mencari",
   "keyboard_shortcuts.toot": "to start a brand new toot",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "to move up in the list",
@@ -197,14 +199,14 @@
   "privacy.change": "Tentukan privasi status",
   "privacy.direct.long": "Kirim hanya ke pengguna yang disebut",
   "privacy.direct.short": "Langsung",
-  "privacy.private.long": "Kirim hanya ke pengikut",
+  "privacy.private.long": "Kirim postingan hanya kepada pengikut",
   "privacy.private.short": "Pribadi",
   "privacy.public.long": "Kirim ke linimasa publik",
   "privacy.public.short": "Publik",
   "privacy.unlisted.long": "Tidak ditampilkan di linimasa publik",
   "privacy.unlisted.short": "Tak Terdaftar",
   "regeneration_indicator.label": "Loading…",
-  "regeneration_indicator.sublabel": "Your home feed is being prepared!",
+  "regeneration_indicator.sublabel": "Linimasa anda sedang disiapkan!",
   "relative_time.days": "{number}d",
   "relative_time.hours": "{number}h",
   "relative_time.just_now": "now",
@@ -220,14 +222,14 @@
   "search.placeholder": "Pencarian",
   "search_popout.search_format": "Advanced 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.hashtag": "hashtag",
+  "search_popout.tips.hashtag": "tagar",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
   "search_popout.tips.user": "user",
   "search_results.accounts": "People",
   "search_results.hashtags": "Hashtags",
   "search_results.statuses": "Toots",
-  "search_results.total": "{count} {count, plural, one {hasil} other {hasil}}",
+  "search_results.total": "{count, number} {count, plural, one {hasil} other {hasil}}",
   "standalone.public_title": "A look inside...",
   "status.block": "Block @{name}",
   "status.cannot_reblog": "This post cannot be boosted",
@@ -252,23 +254,25 @@
   "status.sensitive_warning": "Konten sensitif",
   "status.share": "Share",
   "status.show_less": "Tampilkan lebih sedikit",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Tampilkan semua",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Unmute conversation",
   "status.unpin": "Unpin from profile",
   "tabs_bar.federated_timeline": "Gabungan",
   "tabs_bar.home": "Beranda",
   "tabs_bar.local_timeline": "Lokal",
   "tabs_bar.notifications": "Notifikasi",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "ui.beforeunload": "Naskah anda akan hilang jika anda keluar dari Mastodon.",
   "upload_area.title": "Seret & lepaskan untuk mengunggah",
   "upload_button.label": "Tambahkan media",
-  "upload_form.description": "Describe for the visually impaired",
-  "upload_form.focus": "Crop",
+  "upload_form.description": "Deskripsikan untuk mereka yang tidak bisa melihat dengan jelas",
+  "upload_form.focus": "Potong",
   "upload_form.undo": "Undo",
   "upload_progress.label": "Mengunggah...",
   "video.close": "Close video",
-  "video.exit_fullscreen": "Exit full screen",
-  "video.expand": "Expand video",
+  "video.exit_fullscreen": "Keluar dari layar penuh",
+  "video.expand": "Perbesar video",
   "video.fullscreen": "Full screen",
   "video.hide": "Hide video",
   "video.mute": "Mute sound",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index 4f554b08f..5ea982f46 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "Quo esas en tua spirito?",
   "compose_form.publish": "Siflar",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Markizar kontenajo kom trubliva",
-  "compose_form.spoiler": "Celar texto dop averto",
+  "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.spoiler_placeholder": "Averto di kontenajo",
   "confirmation_modal.cancel": "Cancel",
   "confirmations.block.confirm": "Block",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Trubliva kontenajo",
   "status.share": "Share",
   "status.show_less": "Montrar mine",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Montrar plue",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Unmute conversation",
   "status.unpin": "Unpin from profile",
   "tabs_bar.federated_timeline": "Federata",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 6b2532512..068598de2 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "A cosa stai pensando?",
   "compose_form.publish": "Toot",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Segnala file come sensibile",
-  "compose_form.spoiler": "Nascondi testo con avvertimento",
+  "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.spoiler_placeholder": "Content warning",
   "confirmation_modal.cancel": "Cancel",
   "confirmations.block.confirm": "Block",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Materiale sensibile",
   "status.share": "Share",
   "status.show_less": "Mostra meno",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Mostra di più",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Unmute conversation",
   "status.unpin": "Unpin from profile",
   "tabs_bar.federated_timeline": "Federazione",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 04374abc3..3bf00fbc3 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -3,7 +3,7 @@
   "account.block_domain": "{domain}全体を非表示",
   "account.blocked": "ブロック済み",
   "account.disclaimer_full": "以下の情報は不正確な可能性があります。",
-  "account.domain_blocked": "Domain hidden",
+  "account.domain_blocked": "ドメイン非表示中",
   "account.edit_profile": "プロフィールを編集",
   "account.follow": "フォロー",
   "account.followers": "フォロワー",
@@ -64,8 +64,10 @@
   "compose_form.placeholder": "今なにしてる?",
   "compose_form.publish": "トゥート",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "メディアを閲覧注意としてマークする",
-  "compose_form.spoiler": "テキストを隠す",
+  "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": "ブロック",
@@ -259,7 +261,9 @@
   "status.sensitive_warning": "閲覧注意",
   "status.share": "共有",
   "status.show_less": "隠す",
+  "status.show_less_all": "全て隠す",
   "status.show_more": "もっと見る",
+  "status.show_more_all": "全て見る",
   "status.unmute_conversation": "会話のミュートを解除",
   "status.unpin": "プロフィールの固定表示を解除",
   "tabs_bar.federated_timeline": "連合",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 05c84fd37..532c1f04d 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "지금 무엇을 하고 있나요?",
   "compose_form.publish": "툿",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "이 미디어를 민감한 미디어로 취급",
-  "compose_form.spoiler": "텍스트 숨기기",
+  "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.spoiler_placeholder": "경고",
   "confirmation_modal.cancel": "취소",
   "confirmations.block.confirm": "차단",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "민감한 미디어",
   "status.share": "공유",
   "status.show_less": "숨기기",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "더 보기",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "이 대화의 뮤트 해제하기",
   "status.unpin": "고정 해제",
   "tabs_bar.federated_timeline": "연합",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 509382670..a83971f00 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "Wat wil je kwijt?",
   "compose_form.publish": "Toot",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Media als gevoelig markeren (nsfw)",
-  "compose_form.spoiler": "Tekst achter waarschuwing verbergen",
+  "compose_form.sensitive.marked": "Media is als gevoelig gemarkeerd",
+  "compose_form.sensitive.unmarked": "Media is niet als gevoelig gemarkeerd",
+  "compose_form.spoiler.marked": "Tekst is achter een waarschuwing verborgen",
+  "compose_form.spoiler.unmarked": "Tekst is niet verborgen",
   "compose_form.spoiler_placeholder": "Waarschuwingstekst",
   "confirmation_modal.cancel": "Annuleren",
   "confirmations.block.confirm": "Blokkeren",
@@ -219,7 +221,7 @@
   "report.target": "Rapporteer {target}",
   "search.placeholder": "Zoeken",
   "search_popout.search_format": "Geavanceerd zoeken",
-  "search_popout.tips.full_text": "Gebruik gewone tekst om te zoeken naar toots die jij hebt geschreven, als favoriet markeerde, hebt geboost of waarin jij bent vermeldt, en ook om te zoeken naar gebruikersnamen, weergavenamen en hashtags.",
+  "search_popout.tips.full_text": "Gebruik gewone tekst om te zoeken naar jouw toots, gebooste toots, favorieten en naar toots waarin jij bent vermeldt, en naar gebruikersnamen, weergavenamen en hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "toot",
   "search_popout.tips.text": "Gebruik gewone tekst om te zoeken op weergavenamen, gebruikersnamen en hashtags",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Gevoelige inhoud",
   "status.share": "Delen",
   "status.show_less": "Minder tonen",
+  "status.show_less_all": "Alles minder tonen",
   "status.show_more": "Meer tonen",
+  "status.show_more_all": "Alles meer tonen",
   "status.unmute_conversation": "Conversatie niet meer negeren",
   "status.unpin": "Van profielpagina losmaken",
   "tabs_bar.federated_timeline": "Globaal",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index b7ceb9f73..aaad033e2 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "Hva har du på hjertet?",
   "compose_form.publish": "Tut",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Merk media som følsomt",
-  "compose_form.spoiler": "Skjul tekst bak advarsel",
+  "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.spoiler_placeholder": "Innholdsadvarsel",
   "confirmation_modal.cancel": "Avbryt",
   "confirmations.block.confirm": "Blokkèr",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Følsomt innhold",
   "status.share": "Del",
   "status.show_less": "Vis mindre",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Vis mer",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Ikke demp samtale",
   "status.unpin": "Angre festing på profilen",
   "tabs_bar.federated_timeline": "Felles",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index c9a15c751..c7a26c1c6 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "A de qué pensatz ?",
   "compose_form.publish": "Tut",
   "compose_form.publish_loud": "{publish} !",
-  "compose_form.sensitive": "Marcar lo mèdia coma sensible",
-  "compose_form.spoiler": "Rescondre lo tèxte darrièr un avertiment",
+  "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.spoiler_placeholder": "Escrivètz l’avertiment aquí",
   "confirmation_modal.cancel": "Anullar",
   "confirmations.block.confirm": "Blocar",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Contengut sensible",
   "status.share": "Partejar",
   "status.show_less": "Tornar plegar",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Desplegar",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Tornar mostrar la conversacion",
   "status.unpin": "Tirar del perfil",
   "tabs_bar.federated_timeline": "Flux public global",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 391bc6a28..3feb44ad9 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -64,8 +64,10 @@
   "compose_form.placeholder": "Co Ci chodzi po głowie?",
   "compose_form.publish": "Wyślij",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Oznacz treści jako wrażliwe",
-  "compose_form.spoiler": "Ukryj tekst za ostrzeżeniem",
+  "compose_form.sensitive.marked": "Zawartość multimedia jest oznaczona jako wrażliwa",
+  "compose_form.sensitive.unmarked": "Zawartość multimedialna nie jest oznaczona jako wrażliwa",
+  "compose_form.spoiler.marked": "Tekst jest ukryty za ostrzeżeniem",
+  "compose_form.spoiler.unmarked": "Tekst nie jest ukryty",
   "compose_form.spoiler_placeholder": "Wprowadź swoje ostrzeżenie o zawartości",
   "confirmation_modal.cancel": "Anuluj",
   "confirmations.block.confirm": "Zablokuj",
@@ -258,8 +260,10 @@
   "status.sensitive_toggle": "Naciśnij aby wyświetlić",
   "status.sensitive_warning": "Wrażliwa zawartość",
   "status.share": "Udostępnij",
-  "status.show_less": "Pokaż mniej",
-  "status.show_more": "Pokaż więcej",
+  "status.show_less": "Zwiń",
+  "status.show_less_all": "Zwiń wszystkie",
+  "status.show_more": "Rozwiń",
+  "status.show_more_all": "Rozwiń wszystkie",
   "status.unmute_conversation": "Cofnij wyciszenie konwersacji",
   "status.unpin": "Odepnij z profilu",
   "tabs_bar.federated_timeline": "Globalne",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 6406dbea6..c90fb37a0 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "No que você está pensando?",
   "compose_form.publish": "Publicar",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Marcar mídia como conteúdo sensível",
-  "compose_form.spoiler": "Esconder texto com aviso de conteúdo",
+  "compose_form.sensitive.marked": "Mídia está marcada como sensível",
+  "compose_form.sensitive.unmarked": "Mídia não está marcada como sensível",
+  "compose_form.spoiler.marked": "O texto está escondido por um aviso de conteúdo",
+  "compose_form.spoiler.unmarked": "O texto não está escondido",
   "compose_form.spoiler_placeholder": "Aviso de conteúdo",
   "confirmation_modal.cancel": "Cancelar",
   "confirmations.block.confirm": "Bloquear",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Conteúdo sensível",
   "status.share": "Compartilhar",
   "status.show_less": "Mostrar menos",
+  "status.show_less_all": "Mostrar menos para todas as mensagens",
   "status.show_more": "Mostrar mais",
+  "status.show_more_all": "Mostrar mais para todas as mensagens",
   "status.unmute_conversation": "Desativar silêncio desta conversa",
   "status.unpin": "Desafixar do perfil",
   "tabs_bar.federated_timeline": "Global",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index f059e7c20..3b20cf4e6 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "Em que estás a pensar?",
   "compose_form.publish": "Publicar",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Marcar media como conteúdo sensível",
-  "compose_form.spoiler": "Esconder texto com aviso",
+  "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.spoiler_placeholder": "Aviso de conteúdo",
   "confirmation_modal.cancel": "Cancelar",
   "confirmations.block.confirm": "Block",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Conteúdo sensível",
   "status.share": "Compartilhar",
   "status.show_less": "Mostrar menos",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Mostrar mais",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Deixar de silenciar esta conversa",
   "status.unpin": "Não fixar no perfil",
   "tabs_bar.federated_timeline": "Global",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 06a7d732b..ec21b5d55 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "О чем Вы думаете?",
   "compose_form.publish": "Трубить",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Отметить как чувствительный контент",
-  "compose_form.spoiler": "Скрыть текст за предупреждением",
+  "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.spoiler_placeholder": "Напишите свое предупреждение здесь",
   "confirmation_modal.cancel": "Отмена",
   "confirmations.block.confirm": "Заблокировать",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Чувствительный контент",
   "status.share": "Поделиться",
   "status.show_less": "Свернуть",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Развернуть",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Снять глушение с треда",
   "status.unpin": "Открепить от профиля",
   "tabs_bar.federated_timeline": "Глобальная",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index bb86165ad..2cb249d2a 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "Na čo myslíš?",
   "compose_form.publish": "Toot",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Označ médiá ako chúlostivé",
-  "compose_form.spoiler": "Skryť text za varovanie",
+  "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.spoiler_placeholder": "Sem napíšte vaše varovanie",
   "confirmation_modal.cancel": "Zrušiť",
   "confirmations.block.confirm": "Blokovať",
@@ -217,8 +219,8 @@
   "report.placeholder": "Ďalšie komentáre",
   "report.submit": "Poslať",
   "report.target": "Nahlásenie {target}",
-  "search.placeholder": "Hľadať",
-  "search_popout.search_format": "Pokročilý formát vyhľadávania",
+  "search.placeholder": "Hľadaj",
+  "search_popout.search_format": "Pokročilé vyhľadávanie",
   "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.hashtag": "haštag",
   "search_popout.tips.status": "status",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Chúlostivý obsah",
   "status.share": "Zdieľať",
   "status.show_less": "Zobraz menej",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Zobraz viac",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Prestať ignorovať konverzáciu",
   "status.unpin": "Odopnúť z profilu",
   "tabs_bar.federated_timeline": "Federovaná",
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index a672ae6ca..c6512cda4 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "Šta Vam je na umu?",
   "compose_form.publish": "Tutni",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Obeleži multimediju kao osetljivu",
-  "compose_form.spoiler": "Sakrij tekst ispod upozorenja",
+  "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.spoiler_placeholder": "Ovde upišite upozorenje",
   "confirmation_modal.cancel": "Poništi",
   "confirmations.block.confirm": "Blokiraj",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Osetljiv sadržaj",
   "status.share": "Podeli",
   "status.show_less": "Prikaži manje",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Prikaži više",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Uključi prepisku",
   "status.unpin": "Otkači sa profila",
   "tabs_bar.federated_timeline": "Federisano",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index 1e3a3ce2b..93fbe5960 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "Шта Вам је на уму?",
   "compose_form.publish": "Тутни",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Обележи мултимедију као осетљиву",
-  "compose_form.spoiler": "Сакриј текст испод упозорења",
+  "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.spoiler_placeholder": "Овде упишите упозорење",
   "confirmation_modal.cancel": "Поништи",
   "confirmations.block.confirm": "Блокирај",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Осетљив садржај",
   "status.share": "Подели",
   "status.show_less": "Прикажи мање",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Прикажи више",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Укључи преписку",
   "status.unpin": "Откачи са профила",
   "tabs_bar.federated_timeline": "Федерисано",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 9c51d5b36..3451212d0 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -1,9 +1,9 @@
 {
   "account.block": "Blockera @{name}",
   "account.block_domain": "Dölj allt från {domain}",
-  "account.blocked": "Blocked",
+  "account.blocked": "Blockerad",
   "account.disclaimer_full": "Informationen nedan kan spegla användarens profil ofullständigt.",
-  "account.domain_blocked": "Domain hidden",
+  "account.domain_blocked": "Domän gömd",
   "account.edit_profile": "Redigera profil",
   "account.follow": "Följ",
   "account.followers": "Följare",
@@ -15,9 +15,9 @@
   "account.moved_to": "{name} har flyttat till:",
   "account.mute": "Tysta @{name}",
   "account.mute_notifications": "Stäng av notifieringar från @{name}",
-  "account.muted": "Muted",
+  "account.muted": "Nertystad",
   "account.posts": "Inlägg",
-  "account.posts_with_replies": "Toots with replies",
+  "account.posts_with_replies": "Toots med svar",
   "account.report": "Rapportera @{name}",
   "account.requested": "Inväntar godkännande. Klicka för att avbryta följförfrågan",
   "account.share": "Dela @{name}'s profil",
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "Vad funderar du på?",
   "compose_form.publish": "Toot",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Markera media som känslig",
-  "compose_form.spoiler": "Dölj text bakom varning",
+  "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.spoiler_placeholder": "Skriv din varning här",
   "confirmation_modal.cancel": "Ångra",
   "confirmations.block.confirm": "Blockera",
@@ -211,20 +213,20 @@
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Ångra",
-  "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": "Vidarebefordra till {target}",
+  "report.forward_hint": "Kontot är från en annan server. Skicka även en anonymiserad kopia av anmälan dit?",
+  "report.hint": "Anmälan skickas till din instans moderatorer. Du kan ge en förklaring till varför du har anmält detta konto nedan:",
   "report.placeholder": "Ytterligare kommentarer",
   "report.submit": "Skicka",
   "report.target": "Rapporterar {target}",
   "search.placeholder": "Sök",
   "search_popout.search_format": "Avancerat sökformat",
-  "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": "Enkel text returnerar statusar där du har skrivit, favoriserat, knuffat eller nämnts samt med matchande användarnamn, visningsnamn och hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Enkel text returnerar matchande visningsnamn, användarnamn och hashtags",
   "search_popout.tips.user": "användare",
-  "search_results.accounts": "People",
+  "search_results.accounts": "Människor",
   "search_results.hashtags": "Hashtags",
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, ett {result} andra {results}}",
@@ -242,7 +244,7 @@
   "status.mute_conversation": "Tysta konversation",
   "status.open": "Utvidga denna status",
   "status.pin": "Fäst i profil",
-  "status.pinned": "Pinned toot",
+  "status.pinned": "Fäst toot",
   "status.reblog": "Knuff",
   "status.reblogged_by": "{name} knuffade",
   "status.reply": "Svara",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Känsligt innehåll",
   "status.share": "Dela",
   "status.show_less": "Visa mindre",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Visa mer",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Öppna konversation",
   "status.unpin": "Ångra fäst i profil",
   "tabs_bar.federated_timeline": "Förenad",
@@ -263,7 +267,7 @@
   "upload_area.title": "Dra & släpp för att ladda upp",
   "upload_button.label": "Lägg till media",
   "upload_form.description": "Beskriv för synskadade",
-  "upload_form.focus": "Crop",
+  "upload_form.focus": "Beskär",
   "upload_form.undo": "Ångra",
   "upload_progress.label": "Laddar upp...",
   "video.close": "Stäng video",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index cab2ce089..95a933b40 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "What is on your mind?",
   "compose_form.publish": "Toot",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Mark media as sensitive",
-  "compose_form.spoiler": "Hide text behind warning",
+  "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.spoiler_placeholder": "Content warning",
   "confirmation_modal.cancel": "Cancel",
   "confirmations.block.confirm": "Block",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Sensitive content",
   "status.share": "Share",
   "status.show_less": "Show less",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Show more",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Unmute conversation",
   "status.unpin": "Unpin from profile",
   "tabs_bar.federated_timeline": "Federated",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 83c10de34..baaa5c97a 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "Ne düşünüyorsun?",
   "compose_form.publish": "Toot",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Görseli hassas olarak işaretle",
-  "compose_form.spoiler": "Metni uyarı arkasına gizle",
+  "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.spoiler_placeholder": "İçerik uyarısı",
   "confirmation_modal.cancel": "İptal",
   "confirmations.block.confirm": "Engelle",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Hassas içerik",
   "status.share": "Share",
   "status.show_less": "Daha azı",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Daha fazlası",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Unmute conversation",
   "status.unpin": "Unpin from profile",
   "tabs_bar.federated_timeline": "Federe",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index b49f707e1..1755c55b4 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "Що у Вас на думці?",
   "compose_form.publish": "Дмухнути",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "Відмітити як непристойний зміст",
-  "compose_form.spoiler": "Приховати текст за попередженням",
+  "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.spoiler_placeholder": "Попередження щодо прихованого тексту",
   "confirmation_modal.cancel": "Відмінити",
   "confirmations.block.confirm": "Заблокувати",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "Непристойний зміст",
   "status.share": "Share",
   "status.show_less": "Згорнути",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "Розгорнути",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "Зняти глушення з діалогу",
   "status.unpin": "Unpin from profile",
   "tabs_bar.federated_timeline": "Глобальна",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 5ccfbc4f4..d031c85f3 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "在想啥?",
   "compose_form.publish": "嘟嘟",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "将媒体文件标记为敏感内容",
-  "compose_form.spoiler": "折叠嘟文内容",
+  "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.spoiler_placeholder": "折叠部分的警告消息",
   "confirmation_modal.cancel": "取消",
   "confirmations.block.confirm": "屏蔽",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "敏感内容",
   "status.share": "分享",
   "status.show_less": "隐藏内容",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "显示内容",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "不再隐藏此对话",
   "status.unpin": "在个人资料页面取消置顶",
   "tabs_bar.federated_timeline": "跨站",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index b105fe426..d3ad238ad 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "你在想甚麼?",
   "compose_form.publish": "發文",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "將媒體檔案標示為「敏感內容」",
-  "compose_form.spoiler": "將部份文字藏於警告訊息之後",
+  "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.spoiler_placeholder": "敏感警告訊息",
   "confirmation_modal.cancel": "取消",
   "confirmations.block.confirm": "封鎖",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "敏感內容",
   "status.share": "Share",
   "status.show_less": "減少顯示",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "顯示更多",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "解禁對話",
   "status.unpin": "解除置頂",
   "tabs_bar.federated_timeline": "跨站",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index b0a94f67b..3a5eade41 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -60,8 +60,10 @@
   "compose_form.placeholder": "在想些什麼?",
   "compose_form.publish": "貼掉",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive": "將此媒體標為敏感",
-  "compose_form.spoiler": "將訊息隱藏在警告訊息之後",
+  "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.spoiler_placeholder": "內容警告",
   "confirmation_modal.cancel": "取消",
   "confirmations.block.confirm": "封鎖",
@@ -252,7 +254,9 @@
   "status.sensitive_warning": "敏感內容",
   "status.share": "Share",
   "status.show_less": "看少點",
+  "status.show_less_all": "Show less for all",
   "status.show_more": "看更多",
+  "status.show_more_all": "Show more for all",
   "status.unmute_conversation": "不消音對話",
   "status.unpin": "解除置頂",
   "tabs_bar.federated_timeline": "聯盟",
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 532f4b2a7..5eadebb81 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -35,6 +35,8 @@ import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrde
 import uuid from '../uuid';
 import { me } from '../initial_state';
 
+const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
+
 const initialState = ImmutableMap({
   mounted: 0,
   sensitive: false,
@@ -135,12 +137,14 @@ const updateSuggestionTags = (state, token) => {
 };
 
 const insertEmoji = (state, position, emojiData) => {
-  const emoji = emojiData.native;
+  const oldText = state.get('text');
+  const needsSpace = emojiData.custom && position > 0 && !allowedAroundShortCode.includes(oldText[position - 1]);
+  const emoji = needsSpace ? ' ' + emojiData.native : emojiData.native;
 
-  return state.withMutations(map => {
-    map.update('text', oldText => `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`);
-    map.set('focusDate', new Date());
-    map.set('idempotencyKey', uuid());
+  return state.merge({
+    text: `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`,
+    focusDate: new Date(),
+    idempotencyKey: uuid(),
   });
 };
 
diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js
index 5a47e7272..7b3141623 100644
--- a/app/javascript/mastodon/reducers/statuses.js
+++ b/app/javascript/mastodon/reducers/statuses.js
@@ -56,17 +56,21 @@ const normalizeStatus = (state, status) => {
     normalStatus.reblog = status.reblog.id;
   }
 
-  const searchContent = [status.spoiler_text, status.content].join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
+  // Only calculate these values when status first encountered
+  // Otherwise keep the ones already in the reducer
+  if (!state.has(status.id)) {
+    const searchContent = [status.spoiler_text, status.content].join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
 
-  const emojiMap = normalStatus.emojis.reduce((obj, emoji) => {
-    obj[`:${emoji.shortcode}:`] = emoji;
-    return obj;
-  }, {});
+    const emojiMap = normalStatus.emojis.reduce((obj, emoji) => {
+      obj[`:${emoji.shortcode}:`] = emoji;
+      return obj;
+    }, {});
 
-  normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
-  normalStatus.contentHtml  = emojify(normalStatus.content, emojiMap);
-  normalStatus.spoilerHtml  = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''), emojiMap);
-  normalStatus.hidden       = normalStatus.sensitive;
+    normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
+    normalStatus.contentHtml  = emojify(normalStatus.content, emojiMap);
+    normalStatus.spoilerHtml  = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''), emojiMap);
+    normalStatus.hidden       = normalStatus.sensitive;
+  }
 
   return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus)));
 };
diff --git a/app/javascript/mastodon/service_worker/entry.js b/app/javascript/mastodon/service_worker/entry.js
index eea4cfc3c..8b65f27a3 100644
--- a/app/javascript/mastodon/service_worker/entry.js
+++ b/app/javascript/mastodon/service_worker/entry.js
@@ -1,10 +1,48 @@
 import './web_push_notifications';
 
+function openCache() {
+  return caches.open('mastodon-web');
+}
+
+function fetchRoot() {
+  return fetch('/', { credentials: 'include' });
+}
+
 // Cause a new version of a registered Service Worker to replace an existing one
 // that is already installed, and replace the currently active worker on open pages.
 self.addEventListener('install', function(event) {
-  event.waitUntil(self.skipWaiting());
+  event.waitUntil(Promise.all([openCache(), fetchRoot()]).then(([cache, root]) => cache.put('/', root)));
 });
 self.addEventListener('activate', function(event) {
   event.waitUntil(self.clients.claim());
 });
+self.addEventListener('fetch', function(event) {
+  const url = new URL(event.request.url);
+
+  if (url.pathname.startsWith('/web/')) {
+    const asyncResponse = fetchRoot();
+    const asyncCache = openCache();
+
+    event.respondWith(asyncResponse.then(async response => {
+      if (response.ok) {
+        const cache = await asyncCache;
+        await cache.put('/', response);
+        return response.clone();
+      }
+
+      throw null;
+    }).catch(() => caches.match('/')));
+  } else if (url.pathname === '/auth/sign_out') {
+    const asyncResponse = fetch(event.request);
+    const asyncCache = openCache();
+
+    event.respondWith(asyncResponse.then(async response => {
+      if (response.ok || response.type === 'opaqueredirect') {
+        const cache = await asyncCache;
+        await cache.delete('/');
+      }
+
+      return response;
+    }));
+  }
+});
diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss
index 873963c90..dd82ab375 100644
--- a/app/javascript/styles/mastodon/accounts.scss
+++ b/app/javascript/styles/mastodon/accounts.scss
@@ -440,6 +440,7 @@
   text-align: center;
   padding: 60px 0;
   padding-top: 55px;
+  margin: 0 auto;
   cursor: default;
 }
 
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index cd0dbbab8..98c5ccbca 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1842,6 +1842,9 @@
     object-position: bottom left;
     width: 100%;
     height: 100%;
+    pointer-events: none;
+    user-drag: none;
+    user-select: none;
   }
 }
 
@@ -3422,8 +3425,12 @@ a.status-card {
   img,
   canvas,
   video {
-    max-width: 100vw;
-    max-height: 100vh;
+    max-width: 100%;
+    /*
+    put margins on top and bottom of image to avoid the screen coverd by
+    image.
+     */
+    max-height: 80%;
     width: auto;
     height: auto;
     margin: auto;
@@ -3435,11 +3442,6 @@ a.status-card {
     background: url('~images/void.png') repeat;
     object-fit: contain;
   }
-
-  .react-swipeable-view-container {
-    width: 100vw;
-    height: 100%;
-  }
 }
 
 .media-modal__closer {
@@ -4315,18 +4317,16 @@ a.status-card {
   display: block;
   text-decoration: none;
   color: $ui-secondary-color;
-  height: 100%;
   line-height: 0;
 
   &,
   img {
+    height: 100%;
     width: 100%;
   }
 
   img {
-    position: relative;
     object-fit: cover;
-    height: auto;
   }
 }
 
@@ -5076,6 +5076,12 @@ noscript {
   }
 }
 
+.focal-point-modal {
+  max-width: 80vw;
+  max-height: 80vh;
+  position: relative;
+}
+
 .focal-point {
   position: relative;
   cursor: pointer;
@@ -5085,6 +5091,14 @@ noscript {
     cursor: move;
   }
 
+  img {
+    max-width: 80vw;
+    max-height: 80vh;
+    width: auto;
+    height: auto;
+    margin: auto;
+  }
+
   &__reticle {
     position: absolute;
     width: 100px;
diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss
index 77420c84b..e9099a9e9 100644
--- a/app/javascript/styles/mastodon/rtl.scss
+++ b/app/javascript/styles/mastodon/rtl.scss
@@ -1,6 +1,22 @@
 body.rtl {
   direction: rtl;
 
+  .column-header > button {
+    text-align: right;
+    padding-left: 0;
+    padding-right: 15px;
+  }
+
+  .landing-page__logo {
+    margin-right: 0;
+    margin-left: 20px;
+  }
+
+  .landing-page .features-list .features-list__row .visual {
+    margin-left: 0;
+    margin-right: 15px;
+  }
+
   .column-link__icon,
   .column-header__icon {
     margin-right: 0;
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index 5a1c13d67..676e885c0 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -53,7 +53,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
       visibility: visibility_from_audience,
       thread: replied_to_status,
       conversation: conversation_from_uri(@object['conversation']),
-      media_attachments: process_attachments.take(4),
+      media_attachment_ids: process_attachments.take(4).map(&:id),
     }
   end
 
diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb
index aa46267dc..6235127b2 100644
--- a/app/lib/ostatus/activity/creation.rb
+++ b/app/lib/ostatus/activity/creation.rb
@@ -45,7 +45,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
         visibility: visibility_scope,
         conversation: find_or_create_conversation,
         thread: thread? ? find_status(thread.first) || find_activitypub_status(thread.first, thread.second) : nil,
-        media_attachments: media_attachments
+        media_attachment_ids: media_attachments.map(&:id)
       )
 
       save_mentions(status)
diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb
index 020303a2f..69685ec83 100644
--- a/app/models/concerns/remotable.rb
+++ b/app/models/concerns/remotable.rb
@@ -38,7 +38,7 @@ module Remotable
           send("#{attachment_name}_file_name=", basename + extname)
 
           self[attribute_name] = url if has_attribute?(attribute_name)
-        rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError => e
+        rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError => e
           Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}"
           nil
         end
diff --git a/app/models/tag.rb b/app/models/tag.rb
index dc2c8d129..9fa9405d7 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -12,7 +12,7 @@
 class Tag < ApplicationRecord
   has_and_belongs_to_many :statuses
 
-  HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_][[:word:]_]*'
+  HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_·][[:word:]_]*'
   HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i
 
   validates :name, presence: true, uniqueness: true, format: { with: /\A#{HASHTAG_NAME_RE}\z/i }
diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb
index fadc24a82..8492c1117 100644
--- a/app/services/backup_service.rb
+++ b/app/services/backup_service.rb
@@ -49,7 +49,7 @@ class BackupService < BaseService
       end
     end
 
-    archive_filename = ['archive', Time.now.utc.strftime('%Y%m%d%H%M%S'), SecureRandom.hex(2)].join('-') + '.tar.gz'
+    archive_filename = ['archive', Time.now.utc.strftime('%Y%m%d%H%M%S'), SecureRandom.hex(16)].join('-') + '.tar.gz'
 
     @backup.dump      = ActionDispatch::Http::UploadedFile.new(tempfile: tmp_file, filename: archive_filename)
     @backup.processed = true
diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml
index 2f0b31a9f..5594abecb 100644
--- a/app/views/about/show.html.haml
+++ b/app/views/about/show.html.haml
@@ -49,7 +49,7 @@
 
               %p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
 
-          .landing-page__call-to-action
+          .landing-page__call-to-action{ dir: 'ltr' }
             .row
               .row__information-board
                 .information-board__section
diff --git a/app/views/accounts/_og.html.haml b/app/views/accounts/_og.html.haml
index 26424a49c..a583b39c2 100644
--- a/app/views/accounts/_og.html.haml
+++ b/app/views/accounts/_og.html.haml
@@ -1,6 +1,6 @@
 = opengraph 'og:url', url
 = opengraph 'og:site_name', site_title
-= opengraph 'og:title', [yield(:page_title).strip.presence, site_title].compact.join(' - ')
+= opengraph 'og:title', yield(:page_title).strip
 = opengraph 'og:description', account_description(account)
 = opengraph 'og:image', full_asset_url(account.avatar.url(:original))
 = opengraph 'og:image:width', '120'
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index c62a573b0..bbf2139a5 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -1,5 +1,5 @@
 - content_for :page_title do
-  = "#{display_name(@account)} (@#{@account.username})"
+  = "#{display_name(@account)} (@#{@account.local_username_and_domain})"
 
 - content_for :header_tags do
   %meta{ name: 'description', content: account_description(@account) }/
diff --git a/app/views/stream_entries/_og_description.html.haml b/app/views/stream_entries/_og_description.html.haml
index 9c24e0a61..3d122b94e 100644
--- a/app/views/stream_entries/_og_description.html.haml
+++ b/app/views/stream_entries/_og_description.html.haml
@@ -1 +1 @@
-= opengraph 'og:description', [activity.spoiler_text, activity.text].reject(&:blank?).join("\n\n")
+= opengraph 'og:description', status_description(activity)
diff --git a/app/views/stream_entries/_og_image.html.haml b/app/views/stream_entries/_og_image.html.haml
index 526034faa..40530f567 100644
--- a/app/views/stream_entries/_og_image.html.haml
+++ b/app/views/stream_entries/_og_image.html.haml
@@ -1,4 +1,4 @@
-- if activity.is_a?(Status) && activity.media_attachments.any?
+- if activity.is_a?(Status) && activity.non_sensitive_with_media?
   - player_card = false
   - activity.media_attachments.each do |media|
     - if media.image?
diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml
index a87c51952..dfb83e747 100644
--- a/app/views/stream_entries/show.html.haml
+++ b/app/views/stream_entries/show.html.haml
@@ -11,8 +11,8 @@
 
   = opengraph 'og:site_name', site_title
   = opengraph 'og:type', 'article'
-  = opengraph 'og:title', "#{@account.display_name.presence || @account.username} on #{site_hostname}"
-  = opengraph 'og:url', account_stream_entry_url(@account, @stream_entry)
+  = opengraph 'og:title', "#{display_name(@account)} (@#{@account.local_username_and_domain})"
+  = opengraph 'og:url', short_account_status_url(@account, @stream_entry)
 
   = render 'stream_entries/og_description', activity: @stream_entry.activity
   = render 'stream_entries/og_image', activity: @stream_entry.activity, account: @account
diff --git a/app/workers/backup_worker.rb b/app/workers/backup_worker.rb
index ec6db4e9e..e4c609d70 100644
--- a/app/workers/backup_worker.rb
+++ b/app/workers/backup_worker.rb
@@ -3,7 +3,16 @@
 class BackupWorker
   include Sidekiq::Worker
 
-  sidekiq_options queue: 'pull'
+  sidekiq_options queue: 'pull', backtrace: true, retry: 5, dead: false
+
+  sidekiq_retries_exhausted do |msg|
+    backup_id = msg['args'].first
+
+    ActiveRecord::Base.connection_pool.with_connection do
+      backup = Backup.find(backup_id)
+      backup&.destroy
+    end
+  end
 
   def perform(backup_id)
     backup = Backup.find(backup_id)