about summary refs log tree commit diff
path: root/app/javascript
diff options
context:
space:
mode:
authorFire Demon <firedemon@creature.cafe>2020-06-27 14:22:30 -0500
committerFire Demon <firedemon@creature.cafe>2020-09-08 03:37:04 -0500
commit9d4f18b984d6699bdf96e5f5963edfe80063426c (patch)
treee00fb54963769a259cd9bbe97754a2a872d028be /app/javascript
parent437d71bddf967573df3912ee5976f7c5a5a7b4c7 (diff)
Monsterfork v2 Kaiju Commit 2020.06.27.1 - 2020.09.05.5
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/flavours/glitch/actions/accounts.js4
-rw-r--r--app/javascript/flavours/glitch/actions/compose.js5
-rw-r--r--app/javascript/flavours/glitch/actions/importer/normalizer.js9
-rw-r--r--app/javascript/flavours/glitch/actions/mutes.js7
-rw-r--r--app/javascript/flavours/glitch/actions/statuses.js74
-rw-r--r--app/javascript/flavours/glitch/actions/streaming.js5
-rw-r--r--app/javascript/flavours/glitch/actions/timelines.js13
-rw-r--r--app/javascript/flavours/glitch/components/media_gallery.js61
-rw-r--r--app/javascript/flavours/glitch/components/status.js8
-rw-r--r--app/javascript/flavours/glitch/components/status_action_bar.js25
-rw-r--r--app/javascript/flavours/glitch/components/status_content.js246
-rw-r--r--app/javascript/flavours/glitch/containers/status_container.js16
-rw-r--r--app/javascript/flavours/glitch/features/account/components/action_bar.js25
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/components/header.js8
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/index.js22
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/compose_form.js16
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/publisher.js18
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js5
-rw-r--r--app/javascript/flavours/glitch/features/status/components/action_bar.js17
-rw-r--r--app/javascript/flavours/glitch/features/status/components/detailed_status.js53
-rw-r--r--app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js16
-rw-r--r--app/javascript/flavours/glitch/features/status/index.js20
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/link_footer.js1
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/mute_modal.js27
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/report_modal.js6
-rw-r--r--app/javascript/flavours/glitch/features/ui/index.js6
-rw-r--r--app/javascript/flavours/glitch/locales/en-MP.js4
-rw-r--r--app/javascript/flavours/glitch/reducers/compose.js14
-rw-r--r--app/javascript/flavours/glitch/reducers/local_settings.js16
-rw-r--r--app/javascript/flavours/glitch/reducers/mutes.js5
-rw-r--r--app/javascript/flavours/glitch/reducers/statuses.js3
-rw-r--r--app/javascript/flavours/glitch/selectors/index.js5
-rw-r--r--app/javascript/flavours/glitch/styles/components/modal.scss2
-rw-r--r--app/javascript/flavours/glitch/styles/containers.scss15
-rw-r--r--app/javascript/flavours/glitch/styles/index.scss3
-rw-r--r--app/javascript/flavours/glitch/styles/monsterfork/about.scss9
-rw-r--r--app/javascript/flavours/glitch/styles/monsterfork/components/composer.scss11
-rw-r--r--app/javascript/flavours/glitch/styles/monsterfork/components/formatting.scss175
-rw-r--r--app/javascript/flavours/glitch/styles/monsterfork/components/index.scss3
-rw-r--r--app/javascript/flavours/glitch/styles/monsterfork/components/status.scss243
-rw-r--r--app/javascript/flavours/glitch/styles/monsterfork/index.scss2
-rw-r--r--app/javascript/flavours/glitch/styles/nightshade.scss3
-rw-r--r--app/javascript/flavours/glitch/styles/nightshade/diff.scss440
-rw-r--r--app/javascript/flavours/glitch/styles/nightshade/variables.scss41
-rw-r--r--app/javascript/flavours/glitch/styles/variables.scss8
-rw-r--r--app/javascript/flavours/glitch/styles/widgets.scss1
-rw-r--r--app/javascript/fonts/opensans/LICENSE.txt202
-rw-r--r--app/javascript/fonts/opensans/OpenSans-Bold.ttfbin0 -> 104120 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-Bold.woff2bin0 -> 46296 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-BoldItalic.ttfbin0 -> 92628 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-BoldItalic.woff2bin0 -> 42116 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-ExtraBold.ttfbin0 -> 102076 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-ExtraBold.woff2bin0 -> 46028 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-ExtraBoldItalic.ttfbin0 -> 92772 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-ExtraBoldItalic.woff2bin0 -> 42180 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-Italic.ttfbin0 -> 92240 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-Italic.woff2bin0 -> 42456 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-Light.ttfbin0 -> 101696 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-Light.woff2bin0 -> 45632 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-LightItalic.ttfbin0 -> 92488 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-LightItalic.woff2bin0 -> 41908 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-Regular.ttfbin0 -> 96932 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-Regular.woff2bin0 -> 44504 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-SemiBold.ttfbin0 -> 100820 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-SemiBold.woff2bin0 -> 46376 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-SemiBoldItalic.ttfbin0 -> 92180 bytes
-rw-r--r--app/javascript/fonts/opensans/OpenSans-SemiBoldItalic.woff2bin0 -> 43340 bytes
-rw-r--r--app/javascript/locales/locale-data/en-MP.js8
-rw-r--r--app/javascript/mastodon/actions/importer/normalizer.js5
-rw-r--r--app/javascript/mastodon/actions/streaming.js5
-rw-r--r--app/javascript/mastodon/components/status_action_bar.js6
-rw-r--r--app/javascript/mastodon/components/status_content.js20
-rw-r--r--app/javascript/mastodon/features/compose/components/poll_form.js4
-rw-r--r--app/javascript/mastodon/locales/en-MP.json176
-rw-r--r--app/javascript/mastodon/locales/locale-data/en-MP.js8
-rw-r--r--app/javascript/mastodon/locales/whitelist_en-MP.json2
-rw-r--r--app/javascript/mastodon/reducers/compose.js4
-rw-r--r--app/javascript/skins/glitch/nightshade/common.scss1
-rw-r--r--app/javascript/skins/glitch/nightshade/names.yml5
-rw-r--r--app/javascript/styles/fonts/montserrat.scss4
-rw-r--r--app/javascript/styles/fonts/opensans.scss134
-rw-r--r--app/javascript/styles/fonts/roboto-mono.scss2
-rw-r--r--app/javascript/styles/fonts/roboto.scss8
-rw-r--r--app/javascript/styles/mailer.scss2
-rw-r--r--app/javascript/styles/mastodon/variables.scss6
85 files changed, 2187 insertions, 131 deletions
diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js
index e1012a80b..32e533bd0 100644
--- a/app/javascript/flavours/glitch/actions/accounts.js
+++ b/app/javascript/flavours/glitch/actions/accounts.js
@@ -264,11 +264,11 @@ export function unblockAccountFail(error) {
 };
 
 
-export function muteAccount(id, notifications) {
+export function muteAccount(id, notifications, timelinesOnly) {
   return (dispatch, getState) => {
     dispatch(muteAccountRequest(id));
 
-    api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications }).then(response => {
+    api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, timelinesOnly }).then(response => {
       // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
       dispatch(muteAccountSuccess(response.data, getState().get('statuses')));
     }).catch(error => {
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js
index f83738093..4c2cca9eb 100644
--- a/app/javascript/flavours/glitch/actions/compose.js
+++ b/app/javascript/flavours/glitch/actions/compose.js
@@ -147,6 +147,9 @@ export function submitCompose(routerHistory) {
     let media  = getState().getIn(['compose', 'media_attachments']);
     const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']);
     let spoilerText = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : '';
+    const id = getState().getIn(['compose', 'id'], null);
+    const submit_url = id ? `/api/v1/statuses/${id}` : '/api/v1/statuses';
+    const submit_action = (res, body, config) => id ? api(getState).put(res, body, config) : api(getState).post(res, body, config);
 
     if ((!status || !status.length) && media.size === 0) {
       return;
@@ -156,7 +159,7 @@ export function submitCompose(routerHistory) {
     if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) {
       status = status + ' 👁️';
     }
-    api(getState).post('/api/v1/statuses', {
+    submit_action(submit_url, {
       status,
       content_type: getState().getIn(['compose', 'content_type']),
       in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js
index 05955963c..729c8d700 100644
--- a/app/javascript/flavours/glitch/actions/importer/normalizer.js
+++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js
@@ -12,7 +12,7 @@ const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
 
 export function searchTextFromRawStatus (status) {
   const spoilerText   = status.spoiler_text || '';
-  const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
+  const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).concat(status.tags ? status.tags.map(tag => tag.name) : []).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
   return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
 }
 
@@ -53,11 +53,15 @@ export function normalizeStatus(status, normalOldStatus) {
     normalStatus.poll = status.poll.id;
   }
 
+  const oldUpdatedAt = normalOldStatus ? normalOldStatus.updated_at || normalOldStatus.created_at : null;
+  const newUpdatedAt = normalStatus ? normalStatus.updated_at || normalStatus.created_at : null;
+
   // Only calculate these values when status first encountered
   // Otherwise keep the ones already in the reducer
-  if (normalOldStatus) {
+  if (normalOldStatus && oldUpdatedAt === newUpdatedAt) {
     normalStatus.search_index = normalOldStatus.get('search_index');
     normalStatus.contentHtml = normalOldStatus.get('contentHtml');
+    normalStatus.articleHtml = normalOldStatus.get('articleHtml');
     normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
   } else {
     const spoilerText   = normalStatus.spoiler_text || '';
@@ -66,6 +70,7 @@ export function normalizeStatus(status, normalOldStatus) {
 
     normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
     normalStatus.contentHtml  = emojify(normalStatus.content, emojiMap);
+    normalStatus.articleHtml  = normalStatus.article_content ? emojify(normalStatus.article_content, emojiMap) : normalStatus.contentHtml;
     normalStatus.spoilerHtml  = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
   }
 
diff --git a/app/javascript/flavours/glitch/actions/mutes.js b/app/javascript/flavours/glitch/actions/mutes.js
index 927fc7415..645261627 100644
--- a/app/javascript/flavours/glitch/actions/mutes.js
+++ b/app/javascript/flavours/glitch/actions/mutes.js
@@ -13,6 +13,7 @@ export const MUTES_EXPAND_FAIL    = 'MUTES_EXPAND_FAIL';
 
 export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
 export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
+export const MUTES_TOGGLE_TIMELINES_ONLY = 'MUTES_TOGGLE_TIMELINES_ONLY';
 
 export function fetchMutes() {
   return (dispatch, getState) => {
@@ -104,3 +105,9 @@ export function toggleHideNotifications() {
     dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
   };
 }
+
+export function toggleTimelinesOnly() {
+  return dispatch => {
+    dispatch({ type: MUTES_TOGGLE_TIMELINES_ONLY });
+  };
+}
diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js
index 4d2bda78b..018641fc7 100644
--- a/app/javascript/flavours/glitch/actions/statuses.js
+++ b/app/javascript/flavours/glitch/actions/statuses.js
@@ -12,6 +12,10 @@ export const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST';
 export const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS';
 export const STATUS_DELETE_FAIL    = 'STATUS_DELETE_FAIL';
 
+export const STATUS_PUBLISH_REQUEST = 'STATUS_PUBLISH_REQUEST';
+export const STATUS_PUBLISH_SUCCESS = 'STATUS_PUBLISH_SUCCESS';
+export const STATUS_PUBLISH_FAIL    = 'STATUS_PUBLISH_FAIL';
+
 export const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST';
 export const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS';
 export const CONTEXT_FETCH_FAIL    = 'CONTEXT_FETCH_FAIL';
@@ -34,9 +38,9 @@ export function fetchStatusRequest(id, skipLoading) {
   };
 };
 
-export function fetchStatus(id) {
+export function fetchStatus(id, skipLoading = null) {
   return (dispatch, getState) => {
-    const skipLoading = getState().getIn(['statuses', id], null) !== null;
+    skipLoading = skipLoading === null ? getState().getIn(['statuses', id], null) !== null : skipLoading;
 
     dispatch(fetchContext(id));
 
@@ -55,6 +59,59 @@ export function fetchStatus(id) {
   };
 };
 
+export function editStatus(status, routerHistory) {
+  return (dispatch, getState) => {
+    const id = status.get('id');
+
+    dispatch(fetchContext(id));
+    dispatch(fetchStatusRequest(id, false));
+
+    api(getState).get(`/api/v1/statuses/${id}`, { params: { source: 1 } }).then(response => {
+      dispatch(importFetchedStatus(response.data));
+      dispatch(fetchStatusSuccess(false));
+      dispatch(redraft(status, response.data.text, response.data.content_type, true));
+      ensureComposeIsVisible(getState, routerHistory);
+    }).catch(error => {
+      dispatch(fetchStatusFail(id, error, false));
+    });
+  };
+};
+
+export function publishStatus(id) {
+  return (dispatch, getState) => {
+    dispatch(publishStatusRequest(id));
+
+    api(getState).post(`/api/v1/statuses/${id}/publish`).then(() => {
+      dispatch(publishStatusSuccess(id));
+      dispatch(fetchStatus(id, false));
+    }).catch(error => {
+      dispatch(publishStatusFail(id, error));
+    });
+  };
+};
+
+export function publishStatusRequest(id) {
+  return {
+    type: STATUS_PUBLISH_REQUEST,
+    id: id,
+  };
+};
+
+export function publishStatusSuccess(id) {
+  return {
+    type: STATUS_PUBLISH_SUCCESS,
+    id: id,
+  };
+};
+
+export function publishStatusFail(id, error) {
+  return {
+    type: STATUS_PUBLISH_FAIL,
+    id: id,
+    error: error,
+  };
+};
+
 export function fetchStatusSuccess(skipLoading) {
   return {
     type: STATUS_FETCH_SUCCESS,
@@ -72,12 +129,13 @@ export function fetchStatusFail(id, error, skipLoading) {
   };
 };
 
-export function redraft(status, raw_text, content_type) {
+export function redraft(status, raw_text, content_type, inplace = false) {
   return {
     type: REDRAFT,
     status,
     raw_text,
     content_type,
+    inplace,
   };
 };
 
@@ -91,7 +149,7 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
 
     dispatch(deleteStatusRequest(id));
 
-    api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
+    api(getState).delete(`/api/v1/statuses/${id}`, { params: { redraft: withRedraft?1:0 } } ).then(response => {
       dispatch(deleteStatusSuccess(id));
       dispatch(deleteFromTimelines(id));
 
@@ -172,12 +230,16 @@ export function fetchContextFail(id, error) {
   };
 };
 
-export function muteStatus(id) {
+export function muteStatus(id, hide = false) {
   return (dispatch, getState) => {
     dispatch(muteStatusRequest(id));
 
-    api(getState).post(`/api/v1/statuses/${id}/mute`).then(() => {
+    api(getState).post(`/api/v1/statuses/${id}/mute`, { params: { hide: hide?1:0 } }).then(() => {
       dispatch(muteStatusSuccess(id));
+
+      if (hide) {
+        dispatch(deleteFromTimelines(id));
+      }
     }).catch(error => {
       dispatch(muteStatusFail(id, error));
     });
diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js
index 35db5dcc9..295896e55 100644
--- a/app/javascript/flavours/glitch/actions/streaming.js
+++ b/app/javascript/flavours/glitch/actions/streaming.js
@@ -18,6 +18,7 @@ import {
 } from './announcements';
 import { fetchFilters } from './filters';
 import { getLocale } from 'mastodon/locales';
+import { resetCompose } from 'flavours/glitch/actions/compose';
 
 const { messages } = getLocale();
 
@@ -96,6 +97,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
         case 'announcement.delete':
           dispatch(deleteAnnouncement(data.payload));
           break;
+        case 'refresh':
+          dispatch(resetCompose());
+          window.location.reload();
+          break;
         }
       },
     };
diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js
index b19666e62..bd79d64f5 100644
--- a/app/javascript/flavours/glitch/actions/timelines.js
+++ b/app/javascript/flavours/glitch/actions/timelines.js
@@ -133,7 +133,18 @@ export const expandHomeTimeline            = ({ maxId } = {}, done = noOp) => ex
 export const expandPublicTimeline          = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done);
 export const expandCommunityTimeline       = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
 export const expandDirectTimeline          = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
-export const expandAccountTimeline         = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
+export const expandAccountTimeline         = (accountId, { maxId, filter } = {}) => {
+  const path = filter ? filter : '';
+  const params = {
+    include_replies: filter === ':replies',
+    include_reblogs: filter === ':reblogs',
+    only_reblogs: filter === ':reblogs',
+    mentions: filter === ':mentions',
+    max_id: maxId,
+  };
+
+  return expandTimeline(`account:${accountId}${path}`, `/api/v1/accounts/${accountId}/statuses`, params);
+};
 export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
 export const expandAccountMediaTimeline    = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
 export const expandListTimeline            = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js
index 96042f07a..1ab9a6adb 100644
--- a/app/javascript/flavours/glitch/components/media_gallery.js
+++ b/app/javascript/flavours/glitch/components/media_gallery.js
@@ -384,6 +384,66 @@ class MediaGallery extends React.PureComponent {
       );
     }
 
+    let parts = {};
+
+    media.map(
+      (attachment, i) => {
+        if (attachment.get('description')) {
+          if (attachment.get('description') in parts) {
+            parts[attachment.get('description')].push([i, attachment.get('url'), attachment.get('id')]);
+          } else {
+            parts[attachment.get('description')] = [[i, attachment.get('url'), attachment.get('id')]];
+          }
+        }
+      },
+    );
+
+    let descriptions = Object.entries(parts).map(
+      part => {
+        const [desc, idx] = part;
+        if (idx.length === 1) {
+          const url = idx[0][1];
+          return (
+            <p key={idx[0][2]}>
+              <strong>
+                <a href={url} title={url} target='_blank' rel='nofollow noopener'>
+                  <FormattedMessage id='status.media.description' defaultMessage='Attachment #{index}: ' values={{ index: 1+idx[0][0] }} />
+                </a>
+              </strong>
+              <span>{desc}</span>
+            </p>
+          );
+        } else if (idx.length !== 0) {
+          const indexes = (
+            <React.Fragment>
+              {
+                idx.map((i, c) => {
+                  const url = i[1];
+                  return (<span key={i[2]}>{c === 0 ? ' ' : ', '}<a href={url} title={url} target='_blank' rel='nofollow noopener'>#{1+i[0]}</a></span>);
+                })
+              }
+            </React.Fragment>
+          );
+          return (
+            <p key={idx[0][2]}>
+              <strong>
+                <FormattedMessage id='status.media.descriptions' defaultMessage='Attachments {list}: ' values={{ list: indexes }} />
+              </strong>
+              <span>{desc}</span>
+            </p>
+          );
+        } else {
+          return null;
+        }
+      },
+    );
+
+    let description_wrapper = visible && (
+      <div className='media-caption'>
+        {descriptions}
+      </div>
+    );
+
     return (
       <div className={computedClass} style={style} ref={this.handleRef}>
         <div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}>
@@ -396,6 +456,7 @@ class MediaGallery extends React.PureComponent {
         </div>
 
         {children}
+        {description_wrapper}
       </div>
     );
   }
diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index 4e628a420..69f93a2f1 100644
--- a/app/javascript/flavours/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -73,6 +73,8 @@ class Status extends ImmutablePureComponent {
     onReblog: PropTypes.func,
     onBookmark: PropTypes.func,
     onDelete: PropTypes.func,
+    onEdit: PropTypes.func,
+    onPublish: PropTypes.func,
     onDirect: PropTypes.func,
     onMention: PropTypes.func,
     onPin: PropTypes.func,
@@ -368,7 +370,7 @@ class Status extends ImmutablePureComponent {
   }
 
   handleExpandedToggle = () => {
-    if (this.props.status.get('spoiler_text')) {
+    if (this.props.status.get('spoiler_text') || this.props.status.get('reblogSpoilerHtml')) {
       this.setExpansion(!this.state.isExpanded);
     }
   };
@@ -672,6 +674,9 @@ class Status extends ImmutablePureComponent {
     //  Users can use those for theming, hiding avatars etc via UserStyle
     const selectorAttribs = {
       'data-status-by': `@${status.getIn(['account', 'acct'])}`,
+      'data-nest-level': status.get('nest_level'),
+      'data-nest-deep': status.get('nest_level') >= 15,
+      'data-local-only': !!status.get('local_only'),
     };
 
     if (prepend && account) {
@@ -692,6 +697,7 @@ class Status extends ImmutablePureComponent {
 
     const computedClass = classNames('status', `status-${status.get('visibility')}`, {
       collapsed: isCollapsed,
+      unpublished: status.get('published') === false,
       'has-background': isCollapsed && background,
       'status__wrapper-reply': !!status.get('in_reply_to_id'),
       read: unread === false,
diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js
index cfb03c21b..0822239f5 100644
--- a/app/javascript/flavours/glitch/components/status_action_bar.js
+++ b/app/javascript/flavours/glitch/components/status_action_bar.js
@@ -13,6 +13,8 @@ import classNames from 'classnames';
 const messages = defineMessages({
   delete: { id: 'status.delete', defaultMessage: 'Delete' },
   redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
+  edit: { id: 'status.edit', defaultMessage: 'Edit' },
+  publish: { id: 'status.publish', defaultMessage: 'Publish' },
   direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
   mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
   mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
@@ -63,6 +65,8 @@ class StatusActionBar extends ImmutablePureComponent {
     onFavourite: PropTypes.func,
     onReblog: PropTypes.func,
     onDelete: PropTypes.func,
+    onEdit: PropTypes.func,
+    onPublish: PropTypes.func,
     onDirect: PropTypes.func,
     onMention: PropTypes.func,
     onMute: PropTypes.func,
@@ -125,7 +129,7 @@ class StatusActionBar extends ImmutablePureComponent {
 
   _openInteractionDialog = type => {
     window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
-   }
+  }
 
   handleDeleteClick = () => {
     this.props.onDelete(this.props.status, this.context.router.history);
@@ -135,6 +139,14 @@ class StatusActionBar extends ImmutablePureComponent {
     this.props.onDelete(this.props.status, this.context.router.history, true);
   }
 
+  handleEditClick = () => {
+    this.props.onEdit(this.props.status, this.context.router.history);
+  }
+
+  handlePublishClick = () => {
+    this.props.onPublish(this.props.status);
+  }
+
   handlePinClick = () => {
     this.props.onPin(this.props.status);
   }
@@ -221,10 +233,8 @@ class StatusActionBar extends ImmutablePureComponent {
 
     menu.push(null);
 
-    if (status.getIn(['account', 'id']) === me || withDismiss) {
-      menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
-      menu.push(null);
-    }
+    menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
+    menu.push(null);
 
     if (status.getIn(['account', 'id']) === me) {
       if (publicStatus) {
@@ -233,6 +243,11 @@ class StatusActionBar extends ImmutablePureComponent {
 
       menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
       menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
+      menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
+
+      if (status.get('published') === false) {
+        menu.push({ text: intl.formatMessage(messages.publish), action: this.handlePublishClick });
+      }
     } else {
       menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
       menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js
index a39f747b8..a4546edfd 100644
--- a/app/javascript/flavours/glitch/components/status_content.js
+++ b/app/javascript/flavours/glitch/components/status_content.js
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
 import { isRtl } from 'flavours/glitch/util/rtl';
 import { FormattedMessage } from 'react-intl';
 import Permalink from './permalink';
+import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
 import classnames from 'classnames';
 import Icon from 'flavours/glitch/components/icon';
 import { autoPlayGif } from 'flavours/glitch/util/initial_state';
@@ -13,7 +14,7 @@ const textMatchesTarget = (text, origin, host) => {
   return (text === origin || text === host
           || text.startsWith(origin + '/') || text.startsWith(host + '/')
           || 'www.' + text === host || ('www.' + text).startsWith(host + '/'));
-}
+};
 
 const isLinkMisleading = (link) => {
   let linkTextParts = [];
@@ -77,11 +78,13 @@ export default class StatusContent extends React.PureComponent {
     onUpdate: PropTypes.func,
     tagLinks: PropTypes.bool,
     rewriteMentions: PropTypes.string,
+    article: PropTypes.bool,
   };
 
   static defaultProps = {
     tagLinks: true,
     rewriteMentions: 'no',
+    article: false,
   };
 
   state = {
@@ -231,7 +234,7 @@ export default class StatusContent extends React.PureComponent {
 
     let element = e.target;
     while (element) {
-      if (['button', 'video', 'a', 'label', 'canvas'].includes(element.localName)) {
+      if (['button', 'video', 'a', 'label', 'canvas', 'details', 'summary'].includes(element.localName)) {
         return;
       }
       element = element.parentNode;
@@ -271,23 +274,213 @@ export default class StatusContent extends React.PureComponent {
       disabled,
       tagLinks,
       rewriteMentions,
+      article,
     } = this.props;
 
     const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
 
-    const content = { __html: status.get('contentHtml') };
-    const spoilerContent = { __html: status.get('spoilerHtml') };
+    const edited = (status.get('edited') === 0) ? null : (
+      <div className='status__notice status__edit-notice'>
+        <Icon id='pencil-square-o' />
+        <FormattedMessage
+          id='status.edited'
+          defaultMessage='{count, plural, one {# edit} other {# edits}} · last update: {updated_at}'
+          key={`edit-${status.get('id')}`}
+          values={{
+            count: status.get('edited'),
+            updated_at: <RelativeTimestamp timestamp={status.get('updated_at')} />,
+          }}
+        />
+      </div>
+    );
+
+    const unpublished = (status.get('published') === false) && (
+      <div className='status__notice status__unpublished-notice'>
+        <Icon id='chain-broken' />
+        <FormattedMessage
+          id='status.unpublished'
+          defaultMessage='Unpublished'
+          key={`unpublished-${status.get('id')}`}
+        />
+      </div>
+    );
+
+    const local_only = (status.get('local_only') === true) && (
+      <div className='status__notice status__localonly-notice'>
+        <Icon id='home' />
+        <FormattedMessage
+          id='advanced_options.local-only.short'
+          defaultMessage='Local-only'
+          key={`localonly-${status.get('id')}`}
+        />
+      </div>
+    );
+
+    const quiet = (status.get('notify') === false) && (
+      <div className='status__notice status__quiet-notice'>
+        <Icon id='bell-slash' />
+        <FormattedMessage
+          id='status.quiet'
+          defaultMessage='Quiet local publish'
+          key={`quiet-${status.get('id')}`}
+        />
+      </div>
+    );
+
+    const article_content = status.get('article') && (
+      <div className='status__notice status__article-notice'>
+        <Icon id='file-text-o' />
+        <Permalink
+          href={status.get('url')}
+          to={`/statuses/${status.get('id')}`}
+        >
+          <FormattedMessage
+            id='status.article'
+            defaultMessage='Article'
+            key={`article-${status.get('id')}`}
+          />
+        </Permalink>
+      </div>
+    );
+
+    const publish_at = status.get('publish_at') && (
+      <div className='status__notice status__publish-notice'>
+        <Icon id='bullhorn' />
+        <FormattedMessage
+          id='status.publish_at'
+          defaultMessage='Auto-publish: {publish_at}'
+          key={`publish-${status.get('id')}`}
+          values={{
+            publish_at: <RelativeTimestamp timestamp={status.get('publish_at')} futureDate />,
+          }}
+        />
+      </div>
+    );
+
+    const expires_at = !unpublished && status.get('expires_at') && (
+      <div className='status__notice status__expires-notice'>
+        <Icon id='clock-o' />
+        <FormattedMessage
+          id='status.expires_at'
+          defaultMessage='Self-destruct: {expires_at}'
+          key={`expires-${status.get('id')}`}
+          values={{
+            expires_at: <RelativeTimestamp timestamp={status.get('expires_at')} futureDate />,
+          }}
+        />
+      </div>
+    );
+
+    const status_notice_wrapper = (
+      <div className='status__notice-wrapper'>
+        {unpublished}
+        {publish_at}
+        {expires_at}
+        {quiet}
+        {edited}
+        {local_only}
+        {article_content}
+      </div>
+    );
+
+    const permissions_present = status.get('domain_permissions') && status.get('domain_permissions').size > 0;
+
+    const status_permission_items = permissions_present && status.get('domain_permissions').map((permission) => (
+      <li className='permission-status'>
+        <Icon id='eye-slash' />
+        <FormattedMessage
+          id='status.permissions.visibility.status'
+          defaultMessage='{visibility} 🡲 {domain}'
+          key={`permissions-visibility-${status.get('id')}`}
+          values={{
+            domain: <span>{permission.get('domain')}</span>,
+            visibility: <span>{permission.get('visibility')}</span>,
+          }}
+        />
+      </li>
+    ));
+
+    const permissions = status_permission_items && (
+      <details className='status__permissions' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
+        <summary>
+          <Icon id='unlock-alt' />
+          <FormattedMessage
+            id='status.permissions.title'
+            defaultMessage='Show extended permissions...'
+            key={`permissions-${status.get('id')}`}
+          />
+        </summary>
+        <ul>
+          {status_permission_items}
+        </ul>
+      </details>
+    );
+
+    const tag_items = (status.get('tags') && status.get('tags').size > 0) && status.get('tags').map(hashtag =>
+      (
+        <li>
+          <Icon id='tag' />
+          <Permalink
+            href={hashtag.get('url')}
+            to={`/timelines/tag/${hashtag.get('name')}`}
+          >
+            <span>{hashtag.get('name')}</span>
+          </Permalink>
+        </li>
+      ));
+
+    const tags = tag_items && (
+      <details className='status__tags' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
+        <summary>
+          <Icon id='tag' />
+          <FormattedMessage
+            id='status.tags'
+            defaultMessage='Show all tags...'
+            key={`tags-${status.get('id')}`}
+          />
+        </summary>
+        <ul>
+          {tag_items}
+        </ul>
+      </details>
+    );
+
+    const footers = (
+      <div className='status__footers'>
+        {permissions}
+        {tags}
+      </div>
+    );
+
+    const reblog_spoiler_html = status.get('reblogSpoilerPresent') && { __html: status.get('reblogSpoilerHtml') };
+    const reblog_spoiler = reblog_spoiler_html && (
+      <div className='reblog-spoiler spoiler'>
+        <Icon id='retweet' />
+        <span dangerouslySetInnerHTML={reblog_spoiler_html} />
+      </div>
+    );
+
+    const spoiler_html = status.get('spoiler_text').length > 0 && { __html: status.get('spoilerHtml') };
+    const spoiler = spoiler_html && (
+      <div className='spoiler'>
+        <Icon id='info-circle' />
+        <span dangerouslySetInnerHTML={spoiler_html} />
+      </div>
+    );
+
+    const spoiler_present = status.get('spoiler_text').length > 0 || status.get('reblogSpoilerPresent');
+    const content = { __html: article ? status.get('articleHtml') : status.get('contentHtml') };
     const directionStyle = { direction: 'ltr' };
     const classNames = classnames('status__content', {
       'status__content--with-action': parseClick && !disabled,
-      'status__content--with-spoiler': status.get('spoiler_text').length > 0,
+      'status__content--with-spoiler': spoiler_present,
     });
 
     if (isRtl(status.get('search_index'))) {
       directionStyle.direction = 'rtl';
     }
 
-    if (status.get('spoiler_text').length > 0) {
+    if (spoiler_present) {
       let mentionsPlaceholder = '';
 
       const mentionLinks = status.get('mentions').map(item => (
@@ -302,11 +495,19 @@ export default class StatusContent extends React.PureComponent {
       )).reduce((aggregate, item) => [...aggregate, item, ' '], []);
 
       const toggleText = hidden ? [
-        <FormattedMessage
-          id='status.show_more'
-          defaultMessage='Show more'
-          key='0'
-        />,
+        article ? (
+          <FormattedMessage
+            id='status.show_article'
+            defaultMessage='Show article'
+            key='0'
+          />
+        ) : (
+          <FormattedMessage
+            id='status.show_more'
+            defaultMessage='Show more'
+            key='0'
+          />
+        ),
         mediaIcon ? (
           <Icon
             fixedWidth
@@ -330,15 +531,18 @@ export default class StatusContent extends React.PureComponent {
 
       return (
         <div className={classNames} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} ref={this.setRef}>
-          <p
+          {status_notice_wrapper}
+          <div
             style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}
           >
-            <span dangerouslySetInnerHTML={spoilerContent} />
-            {' '}
-            <button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>
-              {toggleText}
-            </button>
-          </p>
+            {reblog_spoiler}
+            {spoiler}
+            <div class='spoiler-actions'>
+              <button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>
+                {toggleText}
+              </button>
+            </div>
+          </div>
 
           {mentionsPlaceholder}
 
@@ -354,6 +558,8 @@ export default class StatusContent extends React.PureComponent {
             {media}
           </div>
 
+          {footers}
+
         </div>
       );
     } else if (parseClick) {
@@ -366,6 +572,7 @@ export default class StatusContent extends React.PureComponent {
           tabIndex='0'
           ref={this.setRef}
         >
+          {status_notice_wrapper}
           <div
             ref={this.setContentsRef}
             key={`contents-${tagLinks}-${rewriteMentions}`}
@@ -374,6 +581,7 @@ export default class StatusContent extends React.PureComponent {
             tabIndex='0'
           />
           {media}
+          {footers}
         </div>
       );
     } else {
@@ -384,8 +592,10 @@ export default class StatusContent extends React.PureComponent {
           tabIndex='0'
           ref={this.setRef}
         >
+          {status_notice_wrapper}
           <div ref={this.setContentsRef} key={`contents-${tagLinks}`} className='status__content__text' dangerouslySetInnerHTML={content} tabIndex='0' />
           {media}
+          {footers}
         </div>
       );
     }
diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js
index 2cbe3d094..bccaba92d 100644
--- a/app/javascript/flavours/glitch/containers/status_container.js
+++ b/app/javascript/flavours/glitch/containers/status_container.js
@@ -17,7 +17,7 @@ import {
   pin,
   unpin,
 } from 'flavours/glitch/actions/interactions';
-import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
+import { muteStatus, unmuteStatus, deleteStatus, editStatus, publishStatus } from 'flavours/glitch/actions/statuses';
 import { initMuteModal } from 'flavours/glitch/actions/mutes';
 import { initBlockModal } from 'flavours/glitch/actions/blocks';
 import { initReport } from 'flavours/glitch/actions/reports';
@@ -38,6 +38,8 @@ const messages = defineMessages({
   redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' },
   replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
   replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
+  publishConfirm: { id: 'confirmations.publish.confirm', defaultMessage: 'Publish' },
+  publishMessage: { id: 'confirmations.publish.message', defaultMessage: 'Are you ready to publish your post?' },
   unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' },
   author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' },
   matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' },
@@ -166,6 +168,18 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
     }
   },
 
+  onEdit (status, history) {
+    dispatch(editStatus(status, history));
+  },
+
+  onPublish (status) {
+    dispatch(openModal('CONFIRM', {
+      message: intl.formatMessage(messages.publishMessage),
+      confirm: intl.formatMessage(messages.publishConfirm),
+      onConfirm: () => dispatch(publishStatus(status.get('id'))),
+    }));
+  },
+
   onDirect (account, router) {
     dispatch(directCompose(account, router));
   },
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 6576bff8e..2f5a943fd 100644
--- a/app/javascript/flavours/glitch/features/account/components/action_bar.js
+++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js
@@ -31,14 +31,20 @@ class ActionBar extends React.PureComponent {
     if (account.get('acct') !== account.get('username')) {
       extraInfo = (
         <div className='account__disclaimer'>
-          <Icon id='info-circle' fixedWidth /> <FormattedMessage
-            id='account.disclaimer_full'
-            defaultMessage="Information below may reflect the user's profile incompletely."
-          />
-          {' '}
-          <a target='_blank' rel='noopener' href={account.get('url')}>
-            <FormattedMessage id='account.view_full_profile' defaultMessage='View full profile' />
-          </a>
+          <p>
+            <Icon id='info-circle' fixedWidth /> <FormattedMessage
+              id='account.disclaimer_full'
+              defaultMessage="Information below may reflect the user's profile incompletely."
+            />
+          </p>
+          <p>
+            <Icon id='link' fixedWidth /> <a target='_blank' rel='noopener' href={account.get('url')}>
+              <FormattedMessage
+                id='account.view_full_profile'
+                defaultMessage='View full profile'
+              />
+            </a>
+          </p>
         </div>
       );
     }
@@ -51,17 +57,14 @@ class ActionBar extends React.PureComponent {
           <div className='account__action-bar-links'>
             <NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}>
               <FormattedMessage id='account.posts' defaultMessage='Posts' />
-              <strong><FormattedNumber value={account.get('statuses_count')} /></strong>
             </NavLink>
 
             <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}>
               <FormattedMessage id='account.follows' defaultMessage='Follows' />
-              <strong><FormattedNumber value={account.get('following_count')} /></strong>
             </NavLink>
 
             <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}>
               <FormattedMessage id='account.followers' defaultMessage='Followers' />
-              <strong>{ account.get('followers_count') < 0 ? '-' : <FormattedNumber value={account.get('followers_count')} /> }</strong>
             </NavLink>
           </div>
         </div>
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 8195735a1..591f8dffc 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js
@@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 import { FormattedMessage } from 'react-intl';
 import { NavLink } from 'react-router-dom';
 import MovedNote from './moved_note';
+import { me } from 'flavours/glitch/util/initial_state';
 
 export default class Header extends ImmutablePureComponent {
 
@@ -123,9 +124,12 @@ export default class Header extends ImmutablePureComponent {
 
         {!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')}`}><FormattedMessage id='account.threads' defaultMessage='Threads' /></NavLink>
+            { (account.get('id') === me || account.get('show_replies')) &&
+                (<NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>) }
+            { (account.get('id') !== me) && (<NavLink exact to={`/accounts/${account.get('id')}/mentions`}><FormattedMessage id='account.mentions' defaultMessage='Mentions' /></NavLink>) }
             <NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
+            <NavLink exact to={`/accounts/${account.get('id')}/reblogs`}><FormattedMessage id='account.reblogs' defaultMessage='Boosts' /></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 5558ba2a3..66bf55ec4 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/index.js
@@ -17,15 +17,15 @@ import { fetchAccountIdentityProofs } from '../../actions/identity_proofs';
 import MissingIndicator from 'flavours/glitch/components/missing_indicator';
 import TimelineHint from 'flavours/glitch/components/timeline_hint';
 
-const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => {
-  const path = withReplies ? `${accountId}:with_replies` : accountId;
+const mapStateToProps = (state, { params: { accountId }, filter = '' }) => {
+  const path = `${accountId}${filter}`;
 
   return {
     remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
     remoteUrl: state.getIn(['accounts', accountId, 'url']),
     isAccount: !!state.getIn(['accounts', accountId]),
     statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()),
-    featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()),
+    featuredStatusIds: !filter ? state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()) : ImmutableList(),
     isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
     hasMore:   state.getIn(['timelines', `account:${path}`, 'hasMore']),
   };
@@ -49,7 +49,7 @@ class AccountTimeline extends ImmutablePureComponent {
     featuredStatusIds: ImmutablePropTypes.list,
     isLoading: PropTypes.bool,
     hasMore: PropTypes.bool,
-    withReplies: PropTypes.bool,
+    filter: PropTypes.string,
     isAccount: PropTypes.bool,
     remote: PropTypes.bool,
     remoteUrl: PropTypes.string,
@@ -57,24 +57,24 @@ class AccountTimeline extends ImmutablePureComponent {
   };
 
   componentWillMount () {
-    const { params: { accountId }, withReplies } = this.props;
+    const { params: { accountId }, filter } = this.props;
 
     this.props.dispatch(fetchAccount(accountId));
     this.props.dispatch(fetchAccountIdentityProofs(accountId));
-    if (!withReplies) {
+    if (!filter) {
       this.props.dispatch(expandAccountFeaturedTimeline(accountId));
     }
-    this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
+    this.props.dispatch(expandAccountTimeline(accountId, { filter }));
   }
 
   componentWillReceiveProps (nextProps) {
-    if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
+    if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.filter !== this.props.filter) {
       this.props.dispatch(fetchAccount(nextProps.params.accountId));
       this.props.dispatch(fetchAccountIdentityProofs(nextProps.params.accountId));
-      if (!nextProps.withReplies) {
+      if (!nextProps.filter) {
         this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId));
       }
-      this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies }));
+      this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { filter: nextProps.params.filter }));
     }
   }
 
@@ -83,7 +83,7 @@ class AccountTimeline extends ImmutablePureComponent {
   }
 
   handleLoadMore = maxId => {
-    this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies }));
+    this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, filter: this.props.filter }));
   }
 
   setRef = c => {
diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
index a7cb95222..1c05fdafc 100644
--- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js
+++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
@@ -71,10 +71,12 @@ class ComposeForm extends ImmutablePureComponent {
     onChangeVisibility: PropTypes.func,
     onPaste: PropTypes.func,
     onMediaDescriptionConfirm: PropTypes.func,
+    clearTimeout: PropTypes.bool,
   };
 
   static defaultProps = {
     showSearch: false,
+    clearTimeout: null,
   };
 
   handleChange = (e) => {
@@ -149,6 +151,17 @@ class ComposeForm extends ImmutablePureComponent {
     this.handleSubmit(sideArm === 'none' ? null : sideArm);
   }
 
+  handleClearAll = () => {
+    if(!this.clearTimeout || this.clearTimeout === null) {
+      this.clearTimeout = window.setTimeout(() => {
+        this.clearTimeout = null;
+      }, 500);
+    } else {
+      this.clearTimeout = null;
+      this.props.onClearAll();
+    }
+  }
+
   //  Selects a suggestion from the autofill.
   onSuggestionSelected = (tokenStart, token, value) => {
     this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
@@ -256,6 +269,7 @@ class ComposeForm extends ImmutablePureComponent {
       handleSecondarySubmit,
       handleSelect,
       handleSubmit,
+      handleClearAll,
       handleRefTextarea,
     } = this;
     const {
@@ -281,6 +295,7 @@ class ComposeForm extends ImmutablePureComponent {
       suggestions,
       text,
       spoilersAlwaysOn,
+      clearTimeout,
     } = this.props;
 
     let disabledButton = isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia);
@@ -356,6 +371,7 @@ class ComposeForm extends ImmutablePureComponent {
           disabled={disabledButton}
           onSecondarySubmit={handleSecondarySubmit}
           onSubmit={handleSubmit}
+          onClearAll={handleClearAll}
           privacy={privacy}
           sideArm={sideArm}
         />
diff --git a/app/javascript/flavours/glitch/features/compose/components/publisher.js b/app/javascript/flavours/glitch/features/compose/components/publisher.js
index 97890f40d..e5a3d023f 100644
--- a/app/javascript/flavours/glitch/features/compose/components/publisher.js
+++ b/app/javascript/flavours/glitch/features/compose/components/publisher.js
@@ -23,6 +23,10 @@ const messages = defineMessages({
     defaultMessage: '{publish}!',
     id: 'compose_form.publish_loud',
   },
+  clear: {
+    defaultMessage: 'Double-click to clear',
+    id: 'compose_form.clear',
+  },
 });
 
 export default @injectIntl
@@ -34,6 +38,7 @@ class Publisher extends ImmutablePureComponent {
     intl: PropTypes.object.isRequired,
     onSecondarySubmit: PropTypes.func,
     onSubmit: PropTypes.func,
+    onClearAll: PropTypes.func,
     privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']),
     sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']),
   };
@@ -43,7 +48,7 @@ class Publisher extends ImmutablePureComponent {
   };
 
   render () {
-    const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm } = this.props;
+    const { countText, disabled, intl, onClearAll, onSecondarySubmit, privacy, sideArm } = this.props;
 
     const diff = maxChars - length(countText || '');
     const computedClass = classNames('composer--publisher', {
@@ -53,6 +58,17 @@ class Publisher extends ImmutablePureComponent {
 
     return (
       <div className={computedClass}>
+        <Button
+          className='clear'
+          onClick={onClearAll}
+          style={{ padding: null }}
+          title={intl.formatMessage(messages.clear)}
+          text={
+            <span>
+              <Icon id='trash-o' />
+            </span>
+          }
+        />
         {sideArm && sideArm !== 'none' ? (
           <Button
             className='side_arm'
diff --git a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
index fcd2caf1b..3c641d7ec 100644
--- a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
@@ -12,6 +12,7 @@ import {
   selectComposeSuggestion,
   submitCompose,
   uploadCompose,
+  resetCompose,
 } from 'flavours/glitch/actions/compose';
 import {
   openModal,
@@ -82,6 +83,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     dispatch(submitCompose(routerHistory));
   },
 
+  onClearAll() {
+    dispatch(resetCompose());
+  },
+
   onClearSuggestions() {
     dispatch(clearComposeSuggestions());
   },
diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js
index 0f16d93fe..b2c8ac87f 100644
--- a/app/javascript/flavours/glitch/features/status/components/action_bar.js
+++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js
@@ -11,6 +11,8 @@ import classNames from 'classnames';
 const messages = defineMessages({
   delete: { id: 'status.delete', defaultMessage: 'Delete' },
   redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
+  edit: { id: 'status.edit', defaultMessage: 'Edit' },
+  publish: { id: 'status.publish', defaultMessage: 'Publish' },
   direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
   mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
   reply: { id: 'status.reply', defaultMessage: 'Reply' },
@@ -52,6 +54,8 @@ class ActionBar extends React.PureComponent {
     onMuteConversation: PropTypes.func,
     onBlock: PropTypes.func,
     onDelete: PropTypes.func.isRequired,
+    onEdit: PropTypes.func.isRequired,
+    onPublish: PropTypes.func.isRequired,
     onDirect: PropTypes.func.isRequired,
     onMention: PropTypes.func.isRequired,
     onReport: PropTypes.func,
@@ -84,6 +88,14 @@ class ActionBar extends React.PureComponent {
     this.props.onDelete(this.props.status, this.context.router.history, true);
   }
 
+  handleEditClick = () => {
+    this.props.onEdit(this.props.status, this.context.router.history);
+  }
+
+  handlePublishClick = () => {
+    this.props.onPublish(this.props.status);
+  }
+
   handleDirectClick = () => {
     this.props.onDirect(this.props.status.get('account'), this.context.router.history);
   }
@@ -166,6 +178,11 @@ class ActionBar extends React.PureComponent {
       menu.push(null);
       menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
       menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
+      menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
+
+      if (status.get('published') === false) {
+        menu.push({ text: intl.formatMessage(messages.publish), action: this.handlePublishClick });
+      }
     } else {
       menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
       menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
index e4aecbf94..4344e9cce 100644
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
@@ -17,7 +17,7 @@ import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task';
 import classNames from 'classnames';
 import PollContainer from 'flavours/glitch/containers/poll_container';
 import Icon from 'flavours/glitch/components/icon';
-import AnimatedNumber from 'flavours/glitch/components/animated_number';
+import { me } from 'flavours/glitch/util/initial_state';
 
 export default class DetailedStatus extends ImmutablePureComponent {
 
@@ -195,7 +195,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
       applicationLink = <React.Fragment> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></React.Fragment>;
     }
 
-    const visibilityLink = <React.Fragment> · <VisibilityIcon visibility={status.get('visibility')} /></React.Fragment>;
+    const visibilityLink = <React.Fragment><VisibilityIcon visibility={status.get('visibility')} /> · </React.Fragment>;
 
     if (status.get('visibility') === 'direct') {
       reblogIcon = 'envelope';
@@ -203,7 +203,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
       reblogIcon = 'lock';
     }
 
-    if (!['unlisted', 'public'].includes(status.get('visibility'))) {
+    if (status.getIn(['account', 'id']) !== me || !['unlisted', 'public'].includes(status.get('visibility'))) {
       reblogLink = null;
     } else if (this.context.router) {
       reblogLink = (
@@ -211,9 +211,6 @@ export default class DetailedStatus extends ImmutablePureComponent {
           <React.Fragment> · </React.Fragment>
           <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
             <Icon id={reblogIcon} />
-            <span className='detailed-status__reblogs'>
-              <AnimatedNumber value={status.get('reblogs_count')} />
-            </span>
           </Link>
         </React.Fragment>
       );
@@ -223,37 +220,43 @@ export default class DetailedStatus extends ImmutablePureComponent {
           <React.Fragment> · </React.Fragment>
           <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
             <Icon id={reblogIcon} />
-            <span className='detailed-status__reblogs'>
-              <AnimatedNumber value={status.get('reblogs_count')} />
-            </span>
           </a>
         </React.Fragment>
       );
     }
 
-    if (this.context.router) {
+    if (status.getIn(['account', 'id']) !== me) {
+      favouriteLink = null;
+    } else if (this.context.router) {
       favouriteLink = (
-        <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
-          <Icon id='star' />
-          <span className='detailed-status__favorites'>
-            <AnimatedNumber value={status.get('favourites_count')} />
-          </span>
-        </Link>
+        <React.Fragment>
+          <React.Fragment> · </React.Fragment>
+          <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
+            <Icon id='star' />
+          </Link>
+        </React.Fragment>
       );
     } else {
       favouriteLink = (
-        <a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
-          <Icon id='star' />
-          <span className='detailed-status__favorites'>
-            <AnimatedNumber value={status.get('favourites_count')} />
-          </span>
-        </a>
+        <React.Fragment>
+          <React.Fragment> · </React.Fragment>
+          <a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
+            <Icon id='star' />
+          </a>
+        </React.Fragment>
       );
     }
 
+    const selectorAttribs = {
+      'data-status-by': `@${status.getIn(['account', 'acct'])}`,
+      'data-nest-level': status.get('nest_level'),
+      'data-nest-deep': status.get('nest_level') >= 15,
+      'data-local-only': !!status.get('local_only'),
+    };
+
     return (
       <div style={outerStyle}>
-        <div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })} data-status-by={status.getIn(['account', 'acct'])}>
+        <div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact, unpublished: status.get('published') === false })} {...selectorAttribs}>
           <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
             <div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div>
             <DisplayName account={status.get('account')} localDomain={this.props.domain} />
@@ -270,13 +273,15 @@ export default class DetailedStatus extends ImmutablePureComponent {
             onUpdate={this.handleChildUpdate}
             tagLinks={settings.get('tag_misleading_links')}
             rewriteMentions={settings.get('rewrite_mentions')}
+            article
             disabled
           />
 
           <div className='detailed-status__meta'>
+            {visibilityLink}
             <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
               <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
-            </a>{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
+            </a>{applicationLink}{reblogLink}{favouriteLink}
           </div>
         </div>
       </div>
diff --git a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
index 9d11f37e0..124de903a 100644
--- a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
+++ b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
@@ -17,7 +17,9 @@ import {
 import {
   muteStatus,
   unmuteStatus,
+  editStatus,
   deleteStatus,
+  publishStatus,
   hideStatus,
   revealStatus,
 } from 'flavours/glitch/actions/statuses';
@@ -34,6 +36,8 @@ const messages = defineMessages({
   deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
   redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
   redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
+  publishConfirm: { id: 'confirmations.publish.confirm', defaultMessage: 'Publish' },
+  publishMessage: { id: 'confirmations.publish.message', defaultMessage: 'Are you ready to publish your post?' },
   replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
   replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
 });
@@ -118,6 +122,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     }
   },
 
+  onEdit (status, history) {
+    dispatch(editStatus(status, history));
+  },
+
+  onPublish (status) {
+    dispatch(openModal('CONFIRM', {
+      message: intl.formatMessage(messages.publishMessage),
+      confirm: intl.formatMessage(messages.publishConfirm),
+      onConfirm: () => dispatch(publishStatus(status.get('id'))),
+    }));
+  },
+
   onDirect (account, router) {
     dispatch(directCompose(account, router));
   },
diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js
index 3e2e95f35..3a6847e8d 100644
--- a/app/javascript/flavours/glitch/features/status/index.js
+++ b/app/javascript/flavours/glitch/features/status/index.js
@@ -26,7 +26,7 @@ import {
   directCompose,
 } from 'flavours/glitch/actions/compose';
 import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
-import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
+import { muteStatus, unmuteStatus, deleteStatus, editStatus, publishStatus } from 'flavours/glitch/actions/statuses';
 import { initMuteModal } from 'flavours/glitch/actions/mutes';
 import { initBlockModal } from 'flavours/glitch/actions/blocks';
 import { initReport } from 'flavours/glitch/actions/reports';
@@ -50,6 +50,8 @@ const messages = defineMessages({
   deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
   redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
   redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' },
+  publishConfirm: { id: 'confirmations.publish.confirm', defaultMessage: 'Publish' },
+  publishMessage: { id: 'confirmations.publish.message', defaultMessage: 'Are you ready to publish your post?' },
   revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
   hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
   detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
@@ -304,6 +306,20 @@ class Status extends ImmutablePureComponent {
     }
   }
 
+  handleEditClick = (status, history) => {
+    this.props.dispatch(editStatus(status, history));
+  }
+
+  handlePublishClick = (status) => {
+    const { dispatch, intl } = this.props;
+
+    dispatch(openModal('CONFIRM', {
+      message: intl.formatMessage(messages.publishMessage),
+      confirm: intl.formatMessage(messages.publishConfirm),
+      onConfirm: () => dispatch(publishStatus(status.get('id'))),
+    }));
+  }
+
   handleDirectClick = (account, router) => {
     this.props.dispatch(directCompose(account, router));
   }
@@ -588,6 +604,8 @@ class Status extends ImmutablePureComponent {
                   onReblog={this.handleReblogClick}
                   onBookmark={this.handleBookmarkClick}
                   onDelete={this.handleDeleteClick}
+                  onEdit={this.handleEditClick}
+                  onPublish={this.handlePublishClick}
                   onDirect={this.handleDirectClick}
                   onMention={this.handleMentionClick}
                   onMute={this.handleMuteClick}
diff --git a/app/javascript/flavours/glitch/features/ui/components/link_footer.js b/app/javascript/flavours/glitch/features/ui/components/link_footer.js
index 4d7fc36c2..f8a61d2fb 100644
--- a/app/javascript/flavours/glitch/features/ui/components/link_footer.js
+++ b/app/javascript/flavours/glitch/features/ui/components/link_footer.js
@@ -60,6 +60,7 @@ class LinkFooter extends React.PureComponent {
             id='getting_started.open_source_notice'
             defaultMessage='Glitchsoc is open source software, a friendly fork of {Mastodon}. You can contribute or report issues on GitHub at {github}.'
             values={{
+              monsterware: <span><a href='https://monsterware.dev/monsterpit/monsterpit-mastodon' rel='noopener noreferrer' target='_blank'>MonsterWare</a></span>,
               github: <span><a href='https://github.com/glitch-soc/mastodon' rel='noopener noreferrer' target='_blank'>glitch-soc/mastodon</a> (v{version})</span>,
               Mastodon: <a href='https://github.com/tootsuite/mastodon' rel='noopener noreferrer' target='_blank'>Mastodon</a> }}
           />
diff --git a/app/javascript/flavours/glitch/features/ui/components/mute_modal.js b/app/javascript/flavours/glitch/features/ui/components/mute_modal.js
index 2aab82751..eb4bc02d2 100644
--- a/app/javascript/flavours/glitch/features/ui/components/mute_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/mute_modal.js
@@ -7,19 +7,21 @@ import Button from 'flavours/glitch/components/button';
 import { closeModal } from 'flavours/glitch/actions/modal';
 import { muteAccount } from 'flavours/glitch/actions/accounts';
 import { toggleHideNotifications } from 'flavours/glitch/actions/mutes';
+import { toggleTimelinesOnly } from 'flavours/glitch/actions/mutes';
 
 
 const mapStateToProps = state => {
   return {
     account: state.getIn(['mutes', 'new', 'account']),
     notifications: state.getIn(['mutes', 'new', 'notifications']),
+    timelinesOnly: state.getIn(['mutes', 'new', 'timelines_only']),
   };
 };
 
 const mapDispatchToProps = dispatch => {
   return {
-    onConfirm(account, notifications) {
-      dispatch(muteAccount(account.get('id'), notifications));
+    onConfirm(account, notifications, timelinesOnly) {
+      dispatch(muteAccount(account.get('id'), notifications, timelinesOnly));
     },
 
     onClose() {
@@ -29,6 +31,10 @@ const mapDispatchToProps = dispatch => {
     onToggleNotifications() {
       dispatch(toggleHideNotifications());
     },
+
+    onToggleTimelinesOnly() {
+      dispatch(toggleTimelinesOnly());
+    },
   };
 };
 
@@ -39,9 +45,11 @@ class MuteModal extends React.PureComponent {
   static propTypes = {
     account: PropTypes.object.isRequired,
     notifications: PropTypes.bool.isRequired,
+    timelinesOnly: PropTypes.bool.isRequired,
     onClose: PropTypes.func.isRequired,
     onConfirm: PropTypes.func.isRequired,
     onToggleNotifications: PropTypes.func.isRequired,
+    onTimelinesOnly: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
   };
 
@@ -51,7 +59,7 @@ class MuteModal extends React.PureComponent {
 
   handleClick = () => {
     this.props.onClose();
-    this.props.onConfirm(this.props.account, this.props.notifications);
+    this.props.onConfirm(this.props.account, this.props.notifications, this.props.timelinesOnly);
   }
 
   handleCancel = () => {
@@ -66,8 +74,12 @@ class MuteModal extends React.PureComponent {
     this.props.onToggleNotifications();
   }
 
+  toggleTimelinesOnly = () => {
+    this.props.onToggleTimelinesOnly();
+  }
+
   render () {
-    const { account, notifications } = this.props;
+    const { account, notifications, timelinesOnly } = this.props;
 
     return (
       <div className='modal-root__modal mute-modal'>
@@ -91,6 +103,13 @@ class MuteModal extends React.PureComponent {
               <FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
             </label>
           </div>
+          <div>
+            <label htmlFor='mute-modal__timelines-only-checkbox'>
+              <FormattedMessage id='mute_modal.timelines_only' defaultMessage='Hide from timelines only?' />
+              {' '}
+              <Toggle id='mute-modal__timelines-only-checkbox' checked={timelinesOnly} onChange={this.toggleTimelinesOnly} />
+            </label>
+          </div>
         </div>
 
         <div className='mute-modal__action-bar'>
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 9016b08d7..7473cfbe0 100644
--- a/app/javascript/flavours/glitch/features/ui/components/report_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/report_modal.js
@@ -30,7 +30,7 @@ const makeMapStateToProps = () => {
       account: getAccount(state, accountId),
       comment: state.getIn(['reports', 'new', 'comment']),
       forward: state.getIn(['reports', 'new', 'forward']),
-      statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])),
+      statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:replies`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])),
     };
   };
 
@@ -70,12 +70,12 @@ class ReportModal extends ImmutablePureComponent {
   }
 
   componentDidMount () {
-    this.props.dispatch(expandAccountTimeline(this.props.account.get('id'), { withReplies: true }));
+    this.props.dispatch(expandAccountTimeline(this.props.account.get('id'), { filter: ':replies' }));
   }
 
   componentWillReceiveProps (nextProps) {
     if (this.props.account !== nextProps.account && nextProps.account) {
-      this.props.dispatch(expandAccountTimeline(nextProps.account.get('id'), { withReplies: true }));
+      this.props.dispatch(expandAccountTimeline(nextProps.account.get('id'), { filter: ':replies' }));
     }
   }
 
diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js
index bf76c0e57..ee1d898bb 100644
--- a/app/javascript/flavours/glitch/features/ui/index.js
+++ b/app/javascript/flavours/glitch/features/ui/index.js
@@ -214,8 +214,10 @@ class SwitchingColumnsArea extends React.PureComponent {
           <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
           <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' exact component={AccountTimeline} content={children} componentParams={{ filter: '' }} />
+          <WrappedRoute path='/accounts/:accountId/mentions' component={AccountTimeline} content={children} componentParams={{ filter: ':mentions' }} />
+          <WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ filter: ':replies' }} />
+          <WrappedRoute path='/accounts/:accountId/reblogs' component={AccountTimeline} content={children} componentParams={{ filter: ':reblogs' }} />
           <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/locales/en-MP.js b/app/javascript/flavours/glitch/locales/en-MP.js
new file mode 100644
index 000000000..a84552467
--- /dev/null
+++ b/app/javascript/flavours/glitch/locales/en-MP.js
@@ -0,0 +1,4 @@
+import messages from 'flavours/glitch/locales/en';
+import messages_mp from 'mastodon/locales/en-MP.json';
+
+export default Object.assign({}, messages, messages_mp);
\ No newline at end of file
diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js
index e081c31ad..e0ab9f9ab 100644
--- a/app/javascript/flavours/glitch/reducers/compose.js
+++ b/app/javascript/flavours/glitch/reducers/compose.js
@@ -66,6 +66,7 @@ const initialState = ImmutableMap({
     do_not_federate: false,
     threaded_mode: false,
   }),
+  id: null,
   sensitive: false,
   elefriend: Math.random() < glitchProbability ? Math.floor(Math.random() * totalElefriends) : totalElefriends,
   spoiler: false,
@@ -149,6 +150,7 @@ function apiStatusToTextHashtags (state, status) {
 
 function clearAll(state) {
   return state.withMutations(map => {
+    map.set('id', null);
     map.set('text', '');
     if (defaultContentType) map.set('content_type', defaultContentType);
     map.set('spoiler', false);
@@ -286,7 +288,9 @@ const expandMentions = status => {
   const fragment = domParser.parseFromString(status.get('content'), 'text/html').documentElement;
 
   status.get('mentions').forEach(mention => {
-    fragment.querySelector(`a[href="${mention.get('url')}"]`).textContent = `@${mention.get('acct')}`;
+    const selection = fragment.querySelector(`a[href="${mention.get('url')}"]`);
+    if (!selection) return;
+    selection.textContent = `@${mention.get('acct')}`;
   });
 
   return fragment.innerHTML;
@@ -403,9 +407,14 @@ export default function compose(state = initialState, action) {
       }
     });
   case COMPOSE_REPLY_CANCEL:
-    state = state.setIn(['advanced_options', 'threaded_mode'], false);
+    return state.withMutations(map => {
+      map.set('id', null);
+      map.set('in_reply_to', null);
+      map.set('idempotencyKey', uuid());
+    });
   case COMPOSE_RESET:
     return state.withMutations(map => {
+      map.set('id', null);
       map.set('in_reply_to', null);
       if (defaultContentType) map.set('content_type', defaultContentType);
       map.set('text', '');
@@ -505,6 +514,7 @@ export default function compose(state = initialState, action) {
     let text = action.raw_text || unescapeHTML(expandMentions(action.status));
     if (do_not_federate) text = text.replace(/ ?👁\ufe0f?\u200b?$/, '');
     return state.withMutations(map => {
+      map.set('id', action.inplace ? action.status.get('id') : null);
       map.set('text', text);
       map.set('content_type', action.content_type || 'text/plain');
       map.set('in_reply_to', action.status.get('in_reply_to_id'));
diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js
index 3d94d665c..9f383abae 100644
--- a/app/javascript/flavours/glitch/reducers/local_settings.js
+++ b/app/javascript/flavours/glitch/reducers/local_settings.js
@@ -10,18 +10,18 @@ const initialState = ImmutableMap({
   stretch   : true,
   navbar_under : false,
   swipe_to_change_columns: true,
-  side_arm  : 'none',
-  side_arm_reply_mode : 'keep',
-  show_reply_count : false,
-  always_show_spoilers_field: false,
-  confirm_missing_media_description: false,
+  side_arm  : 'private',
+  side_arm_reply_mode : 'restrict',
+  show_reply_count : true,
+  always_show_spoilers_field: true,
+  confirm_missing_media_description: true,
   confirm_boost_missing_media_description: false,
   confirm_before_clearing_draft: true,
   prepend_cw_re: true,
   preselect_on_reply: true,
   inline_preview_cards: true,
-  hicolor_privacy_icons: false,
-  show_content_type_choice: false,
+  hicolor_privacy_icons: true,
+  show_content_type_choice: true,
   filtering_behavior: 'hide',
   tag_misleading_links: true,
   rewrite_mentions: 'no',
@@ -51,7 +51,7 @@ const initialState = ImmutableMap({
     reveal_behind_cw : false,
   }),
   notifications : ImmutableMap({
-    favicon_badge : false,
+    favicon_badge : true,
     tab_badge     : true,
   }),
 });
diff --git a/app/javascript/flavours/glitch/reducers/mutes.js b/app/javascript/flavours/glitch/reducers/mutes.js
index 7111bb710..d170c2594 100644
--- a/app/javascript/flavours/glitch/reducers/mutes.js
+++ b/app/javascript/flavours/glitch/reducers/mutes.js
@@ -3,12 +3,14 @@ import Immutable from 'immutable';
 import {
   MUTES_INIT_MODAL,
   MUTES_TOGGLE_HIDE_NOTIFICATIONS,
+  MUTES_TOGGLE_TIMELINES_ONLY,
 } from 'flavours/glitch/actions/mutes';
 
 const initialState = Immutable.Map({
   new: Immutable.Map({
     account: null,
     notifications: true,
+    timelinesOnly: false,
   }),
 });
 
@@ -18,9 +20,12 @@ export default function mutes(state = initialState, action) {
     return state.withMutations((state) => {
       state.setIn(['new', 'account'], action.account);
       state.setIn(['new', 'notifications'], true);
+      state.setIn(['new', 'timelinesOnly'], false);
     });
   case MUTES_TOGGLE_HIDE_NOTIFICATIONS:
     return state.updateIn(['new', 'notifications'], (old) => !old);
+  case MUTES_TOGGLE_TIMELINES_ONLY:
+    return state.updateIn(['new', 'timelines_only'], (old) => !old);
   default:
     return state;
   }
diff --git a/app/javascript/flavours/glitch/reducers/statuses.js b/app/javascript/flavours/glitch/reducers/statuses.js
index 5db766b96..20822b4cb 100644
--- a/app/javascript/flavours/glitch/reducers/statuses.js
+++ b/app/javascript/flavours/glitch/reducers/statuses.js
@@ -10,6 +10,7 @@ import {
 import {
   STATUS_MUTE_SUCCESS,
   STATUS_UNMUTE_SUCCESS,
+  STATUS_PUBLISH_SUCCESS,
 } from 'flavours/glitch/actions/statuses';
 import {
   TIMELINE_DELETE,
@@ -56,6 +57,8 @@ export default function statuses(state = initialState, action) {
     return state.setIn([action.id, 'muted'], true);
   case STATUS_UNMUTE_SUCCESS:
     return state.setIn([action.id, 'muted'], false);
+  case STATUS_PUBLISH_SUCCESS:
+    return state.setIn([action.id, 'published'], true);
   case TIMELINE_DELETE:
     return deleteStatus(state, action.id, action.references);
   default:
diff --git a/app/javascript/flavours/glitch/selectors/index.js b/app/javascript/flavours/glitch/selectors/index.js
index bb9180d12..3571aea3e 100644
--- a/app/javascript/flavours/glitch/selectors/index.js
+++ b/app/javascript/flavours/glitch/selectors/index.js
@@ -141,6 +141,11 @@ export const makeGetStatus = () => {
         }
       }
 
+      if (statusReblog) {
+        statusReblog = statusReblog.set('reblogSpoilerPresent', statusBase.get('spoiler_text').length > 0);
+        statusReblog = statusReblog.set('reblogSpoilerHtml', statusBase.get('spoilerHtml'));
+      }
+
       return statusBase.withMutations(map => {
         map.set('reblog', statusReblog);
         map.set('account', accountBase);
diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss
index d0be730ac..f80045505 100644
--- a/app/javascript/flavours/glitch/styles/components/modal.scss
+++ b/app/javascript/flavours/glitch/styles/components/modal.scss
@@ -847,7 +847,7 @@
       width: 100%;
       border: none;
       padding: 10px;
-      font-family: 'mastodon-font-monospace', monospace;
+      font-family: 'roboto-mono', monospace;
       background: $ui-base-color;
       color: $primary-text-color;
       font-size: 14px;
diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss
index d1c6c33d7..eab6e480c 100644
--- a/app/javascript/flavours/glitch/styles/containers.scss
+++ b/app/javascript/flavours/glitch/styles/containers.scss
@@ -208,6 +208,18 @@
     margin-bottom: 10px;
   }
 
+  @media screen and (max-width: 800px) {
+    .column-3 {
+      grid-column: 3 / 5;
+      grid-row: 3;
+    }
+
+    .column-4 {
+      grid-column: 1/3;
+      grid-row: 3;
+    }
+  }
+
   @media screen and (max-width: 738px) {
     grid-template-columns: minmax(0, 50%) minmax(0, 50%);
 
@@ -656,7 +668,7 @@
           box-sizing: border-box;
           flex: 0 0 auto;
           color: $darker-text-color;
-          padding: 10px;
+          margin: 15px 0px;
           border-right: 1px solid lighten($ui-base-color, 4%);
           cursor: default;
           text-align: center;
@@ -707,6 +719,7 @@
 
           .counter-label {
             font-size: 12px;
+            font-weight: bold;
             display: block;
           }
 
diff --git a/app/javascript/flavours/glitch/styles/index.scss b/app/javascript/flavours/glitch/styles/index.scss
index af73feb89..c1ed4a6f1 100644
--- a/app/javascript/flavours/glitch/styles/index.scss
+++ b/app/javascript/flavours/glitch/styles/index.scss
@@ -1,6 +1,7 @@
 @import 'mixins';
 @import 'variables';
 @import 'styles/fonts/roboto';
+@import 'styles/fonts/opensans';
 @import 'styles/fonts/roboto-mono';
 @import 'styles/fonts/montserrat';
 
@@ -23,3 +24,5 @@
 @import 'accessibility';
 @import 'rtl';
 @import 'dashboard';
+
+@import 'monsterfork/index';
diff --git a/app/javascript/flavours/glitch/styles/monsterfork/about.scss b/app/javascript/flavours/glitch/styles/monsterfork/about.scss
new file mode 100644
index 000000000..4ab9cfa7c
--- /dev/null
+++ b/app/javascript/flavours/glitch/styles/monsterfork/about.scss
@@ -0,0 +1,9 @@
+.box-widget {
+  .simple_form p.lead {
+    color: $darker-text-color;
+    font-size: 15px;
+    line-height: 20px;
+    font-weight: bold;
+    margin-bottom: 25px;
+  }
+}
\ No newline at end of file
diff --git a/app/javascript/flavours/glitch/styles/monsterfork/components/composer.scss b/app/javascript/flavours/glitch/styles/monsterfork/components/composer.scss
new file mode 100644
index 000000000..ba347b1cc
--- /dev/null
+++ b/app/javascript/flavours/glitch/styles/monsterfork/components/composer.scss
@@ -0,0 +1,11 @@
+.composer--publisher {
+  .clear {
+    background: darken($ui-base-color, 8%);
+    color: $secondary-text-color;
+    margin: 0 2px;
+    padding: 0;
+    width: 36px;
+    text-align: center;
+    float: left;
+  }
+}
diff --git a/app/javascript/flavours/glitch/styles/monsterfork/components/formatting.scss b/app/javascript/flavours/glitch/styles/monsterfork/components/formatting.scss
new file mode 100644
index 000000000..44df7efc9
--- /dev/null
+++ b/app/javascript/flavours/glitch/styles/monsterfork/components/formatting.scss
@@ -0,0 +1,175 @@
+.status__content__text,
+.reply-indicator__content,
+.composer--reply > .content,
+.account__header__content,
+.status__content > .e-content
+{
+  .emojione {
+    width: 20px;
+    height: 20px;
+    margin: -3px 0 0;
+  }
+
+  & > ul,
+  & > ol {
+    margin-bottom: 20px;
+  }
+
+  h1, h2, h3, h4, h5 {
+    margin-top: 20px;
+    margin-bottom: 20px;
+  }
+
+  h1, h2 {
+    font-weight: 700;
+    font-size: 1.2em;
+  }
+
+  h2 {
+    font-size: 1.1em;
+  }
+
+  h3, h4, h5 {
+    font-weight: 500;
+  }
+
+  blockquote {
+    padding-left: 10px;
+    border-left: 3px solid $darker-text-color;
+    color: $darker-text-color;
+    white-space: normal;
+
+    p:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  b, strong {
+    font-weight: 700;
+  }
+
+  em, i {
+    font-style: italic;
+  }
+
+  sub {
+    font-size: smaller;
+    text-align: sub;
+  }
+
+  sup {
+    font-size: smaller;
+    vertical-align: super;
+  }
+
+  ul, ol {
+    margin-left: 1em;
+
+    p {
+      margin: 0;
+    }
+  }
+
+  ul {
+    list-style-type: disc;
+  }
+
+  ol {
+    list-style-type: decimal;
+  }
+
+  a {
+    color: $secondary-text-color;
+    text-decoration: none;
+
+    &:hover {
+      text-decoration: underline;
+
+      .fa {
+        color: lighten($dark-text-color, 7%);
+      }
+    }
+
+    &.mention {
+      &:hover {
+        text-decoration: underline;
+
+        span {
+          text-decoration: none;
+        }
+      }
+    }
+
+    .fa {
+      color: $dark-text-color;
+    }
+  }
+
+  a.unhandled-link {
+    color: lighten($ui-highlight-color, 8%);
+
+    .link-origin-tag {
+      color: $gold-star;
+      font-size: 0.8em;
+    }
+  }
+
+  s { text-decoration: line-through; }
+  del { text-decoration: line-through; }
+  h6 { font-size: 8px; font-weight: bold; }
+  hr { border-color: lighten($dark-text-color, 10%); }
+  pre, code {
+    color: #6c6;
+    text-shadow: 0 0 4px #0f0;
+
+    background: linear-gradient(
+      to bottom,
+      #121 1px,
+      #232 1px
+    );
+    background-size: 100% 2px;
+  }
+  pre {
+    & > code {
+      background: transparent;
+    }
+    padding: 10px;
+    border: 2px solid darken($ui-base-color, 20%);
+  }
+  mark {
+    background-color: #ccff15;
+    color: black;
+  }
+  blockquote {
+    font-style: italic;
+  }
+  .center, .centered, center {
+    text-align: center;
+  }
+  summary {
+    color: lighten($primary-text-color, 33%);
+    font-weight: bold;
+
+    &:focus, &:active {
+      outline: none;
+    }
+  }
+  details > p, details > span {
+    padding-top: 5px;
+    padding-left: 10px;
+    border-left: 3px solid $darker-text-color;
+    color: $darker-text-color;
+    white-space: normal;
+
+    p:last-child {
+      margin-bottom: 0;
+    };
+  }
+  p[data-name="footer"] {
+    color: lighten($dark-text-color, 10%);
+    font-style: italic;
+    font-size: 12px;
+    text-align: right;
+    margin-top: 0px;
+  }
+}
\ No newline at end of file
diff --git a/app/javascript/flavours/glitch/styles/monsterfork/components/index.scss b/app/javascript/flavours/glitch/styles/monsterfork/components/index.scss
new file mode 100644
index 000000000..84da74f82
--- /dev/null
+++ b/app/javascript/flavours/glitch/styles/monsterfork/components/index.scss
@@ -0,0 +1,3 @@
+@import 'composer';
+@import 'status';
+@import 'formatting';
diff --git a/app/javascript/flavours/glitch/styles/monsterfork/components/status.scss b/app/javascript/flavours/glitch/styles/monsterfork/components/status.scss
new file mode 100644
index 000000000..1d2f053c0
--- /dev/null
+++ b/app/javascript/flavours/glitch/styles/monsterfork/components/status.scss
@@ -0,0 +1,243 @@
+.status__notice-wrapper:empty,
+.status__footers:empty {
+  display: none;
+}
+
+.status__notice {
+  display: flex;
+  align-items: center;
+
+  & > span, & > a {
+    display: inline-flex;
+    align-items: center;
+    line-height: normal;
+    font-style: italic;
+    font-weight: bold;
+    font-size: 12px;
+    padding-left: 8px;
+    height: 1.5em;
+  }
+
+  & > span {
+    color: $dark-text-color;
+
+    & > time:before {
+      content: " ";
+      white-space: pre;
+    }
+  }
+
+  & > i {
+    display: inline-flex;
+    align-items: center;
+    color: lighten($dark-text-color, 4%);
+    width: 1.1em;
+    height: 1.5em;
+  }
+}
+
+.status__footers {
+  font-size: 12px;
+  margin-top: 1em;
+
+  & > details {
+    & > summary {
+      &:focus, &:active {
+        outline: none;
+      }
+    }
+
+    & > summary > span,
+    & > ul > li > span,
+    & > ul > li > a {
+      color: lighten($dark-text-color, 4%);
+      padding-left: 8px;
+    }
+  }
+
+  .status__tags {
+    & > ul {
+      display: flex;
+      flex-direction: row;
+      flex-wrap: wrap;
+    }
+
+    & > ul > li {
+      list-style: none;
+      display: inline-block;
+      width: 50%;
+    }
+
+    & > summary > i,
+    & > ul > li > i {
+      color: #669999;
+    }
+  }
+
+  .status__permissions {
+    & > summary > i {
+      color: #999966;
+    }
+
+    & > ul > li {
+      &.permission-status > i {
+        color: #99cccc;
+      }
+
+      &.permission-account > i {
+        color: #cc99cc;
+      }
+
+      & > span {
+        & > span, & > code {
+          color: lighten($primary-text-color, 30%);
+        }
+
+        & > span:first-child {
+          display: inline-block;
+          text-transform: capitalize;
+          min-width: 5em;
+        }
+      }
+    }
+  }
+}
+
+.status, .detailed-status {
+  &.unpublished {
+    background: darken($ui-base-color, 4%);
+
+    &:focus {
+      background: lighten($ui-base-color, 4%);
+    }
+  }
+
+  &[data-local-only="true"] {
+    background: lighten($ui-base-color, 4%);
+  }
+}
+
+div[data-nest-level] {
+  border-style: solid;
+}
+
+@for $i from 0 through 15 {
+  div[data-nest-level="#{$i}"] {
+    border-left-width: #{$i * 3}px;
+    border-left-color: darken($ui-base-color, 8%);
+  }
+}
+
+div[data-nest-deep="true"] {
+  border-left-width: 75px;
+  border-left-color: darken($ui-base-color, 8%);
+}
+
+.status__content {
+  .status__content__text,
+  .e-content {
+    img:not(.emojione) {
+      max-width: 100%;
+      margin: 1em auto;
+    }
+  }
+
+  p:first-child,
+  pre:first-child,
+  blockquote:first-child,
+  div.status__notice-wrapper + p {
+    margin-top: 0px;
+  }
+
+  p, pre, blockquote {
+    margin-top: 1em;
+    margin-bottom: 0px;
+  }
+
+  .status__content__spoiler--visible {
+    margin-top: 1em;
+    margin-bottom: 1em;
+  }
+
+  .spoiler {
+    & > i {
+      width: 1.1em;
+      color: lighten($dark-text-color, 4%);
+    }
+
+    & > span {
+      padding-left: 8px;
+    }
+  }
+
+  .reblog-spoiler {
+    font-style: italic;
+
+    & > span {
+      color: lighten($ui-highlight-color, 8%);
+    }
+  }
+}
+
+div.media-caption {
+  background: $ui-base-color;
+
+  strong {
+    font-weight: bold;
+  }
+
+  p {
+    font-size: 12px !important;
+    padding: 0px 10px;
+    text-align: center;
+  }
+  a {
+		color: $secondary-text-color;
+		text-decoration: none;
+		font-weight: bold;
+
+		&:hover {
+			text-decoration: underline;
+
+			.fa {
+				color: lighten($dark-text-color, 7%);
+			}
+		}
+
+		&.mention {
+			&:hover {
+				text-decoration: none;
+
+				span {
+					text-decoration: underline;
+				}
+			}
+		}
+
+		.fa {
+			color: $dark-text-color;
+		}
+	}
+}
+
+.status__prepend {
+  margin-left: 0px;
+
+  .status__prepend-icon-wrapper {
+    left: 4px;
+  }
+
+  & > span {
+    margin-left: 25px;
+  }
+}
+
+.embed .status__prepend,
+.public-layout .status__prepend {
+  margin: -10px 0px 0px 5px;
+}
+
+.public-layout .status__prepend-icon-wrapper {
+  left: unset;
+  right: 4px;
+}
\ No newline at end of file
diff --git a/app/javascript/flavours/glitch/styles/monsterfork/index.scss b/app/javascript/flavours/glitch/styles/monsterfork/index.scss
new file mode 100644
index 000000000..9888adfe4
--- /dev/null
+++ b/app/javascript/flavours/glitch/styles/monsterfork/index.scss
@@ -0,0 +1,2 @@
+@import 'components/index';
+@import 'about';
\ No newline at end of file
diff --git a/app/javascript/flavours/glitch/styles/nightshade.scss b/app/javascript/flavours/glitch/styles/nightshade.scss
new file mode 100644
index 000000000..bc8069e59
--- /dev/null
+++ b/app/javascript/flavours/glitch/styles/nightshade.scss
@@ -0,0 +1,3 @@
+@import 'nightshade/variables';
+@import 'index';
+@import 'nightshade/diff';
diff --git a/app/javascript/flavours/glitch/styles/nightshade/diff.scss b/app/javascript/flavours/glitch/styles/nightshade/diff.scss
new file mode 100644
index 000000000..de1278114
--- /dev/null
+++ b/app/javascript/flavours/glitch/styles/nightshade/diff.scss
@@ -0,0 +1,440 @@
+// Notes!
+// Sass color functions, "darken" and "lighten" are automatically replaced.
+
+.glitch.local-settings {
+  background: darken($ui-base-color, 80%);
+
+  &__navigation {
+    background: darken($ui-base-color, 30%);
+  }
+
+  &__navigation__item {
+    background: darken($ui-base-color, 50%);
+
+    &:hover {
+      background: $ui-base-color;
+      color: $primary-text-color;
+    }
+  }
+}
+
+.notification__dismiss-overlay {
+  .wrappy {
+    box-shadow: unset;
+  }
+
+  .ckbox {
+    text-shadow: unset;
+  }
+}
+
+.status.status-direct:not(.read) {
+  background: darken($ui-base-color, 8%);
+  border-bottom-color: darken($ui-base-color, 12%);
+
+  &.collapsed> .status__content:after {
+    background: linear-gradient(rgba(darken($ui-base-color, 8%), 0), rgba(darken($ui-base-color, 8%), 1));
+  }
+}
+
+.focusable:focus.status.status-direct:not(.read) {
+  background: darken($ui-base-color, 4%);
+
+  &.collapsed> .status__content:after {
+    background: linear-gradient(rgba(darken($ui-base-color, 4%), 0), rgba(darken($ui-base-color, 4%), 1));
+  }
+}
+
+// Change columns' default background colors
+.column {
+  > .scrollable {
+    background: darken($ui-base-color, 13%);
+  }
+}
+
+.status.collapsed .status__content:after {
+  background: linear-gradient(rgba(darken($ui-base-color, 13%), 0), rgba(darken($ui-base-color, 13%), 1));
+}
+
+.drawer__inner {
+  background: $ui-base-color;
+}
+
+.drawer__inner__mastodon {
+  background: $ui-base-color url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color(darken($ui-base-color, 13%))}"/></svg>') no-repeat bottom / 100% auto !important;
+
+  .mastodon {
+    filter: contrast(75%) brightness(75%) !important;
+  }
+}
+
+// Change the default appearance of the content warning button
+.status__content {
+
+  .status__content__spoiler-link {
+
+    background: darken($ui-base-color, 30%);
+
+    &:hover {
+      background: lighten($ui-base-color, 35%);
+      color: $primary-text-color;
+      text-decoration: none;
+    }
+
+  }
+
+}
+
+// Change the background colors of media and video spoilers
+.media-spoiler,
+.video-player__spoiler,
+.account-gallery__item a {
+  background: $ui-base-color;
+}
+
+// Change the colors used in the dropdown menu
+.dropdown-menu {
+  background: $ui-base-color;
+}
+
+.dropdown-menu__arrow {
+
+  &.left {
+    border-left-color: $ui-base-color;
+  }
+
+  &.top {
+    border-top-color: $ui-base-color;
+  }
+
+  &.bottom {
+    border-bottom-color: $ui-base-color;
+  }
+
+  &.right {
+    border-right-color: $ui-base-color;
+  }
+
+}
+
+.dropdown-menu__item {
+  a {
+    background: $ui-base-color;
+    color: $ui-secondary-color;
+  }
+}
+
+// Change the default color of several parts of the compose form
+.composer {
+
+  .composer--spoiler input, .compose-form__autosuggest-wrapper textarea {
+    color: lighten($ui-base-color, 80%);
+
+    &:disabled { background: lighten($simple-background-color, 10%) }
+
+    &::placeholder {
+      color: lighten($ui-base-color, 70%);
+    }
+  }
+
+  .compose-form__modifiers {
+    background: darken($ui-base-color, 60%);
+
+    .autosuggest-input input, select {
+      background: darken($ui-base-color, 70%);
+    }
+  }
+
+  .composer--options-wrapper {
+    background: lighten($ui-base-color, 10%);
+  }
+
+  .composer--options > hr {
+    display: none;
+  }
+
+  .composer--options--dropdown--content--item {
+    color: $ui-primary-color;
+
+    strong {
+      color: $ui-primary-color;
+    }
+
+  }
+
+  header > .account.small {
+    color: $primary-text-color;
+  }
+
+  .composer--reply > .content {
+    color: $primary-text-color;
+  }
+}
+
+.composer--upload_form--actions .icon-button {
+  color: lighten($white, 7%);
+
+  &:active,
+  &:focus,
+  &:hover {
+    color: $white;
+  }
+}
+
+.composer--upload_form--item > div input {
+  color: lighten($white, 7%);
+
+  &::placeholder {
+    color: lighten($white, 10%);
+  }
+}
+
+.dropdown-menu__separator {
+  border-bottom-color: lighten($ui-base-color, 12%);
+}
+
+.status__content,
+.reply-indicator__content {
+  a {
+    color: $highlight-text-color;
+  }
+}
+
+.emoji-mart-bar {
+  border-color: darken($ui-base-color, 4%);
+
+  &:first-child {
+    background: lighten($ui-base-color, 10%);
+  }
+}
+
+.emoji-mart-search input {
+  background: rgba($ui-base-color, 0.3);
+  border-color: $ui-base-color;
+}
+
+.autosuggest-textarea__suggestions {
+  background: darken($ui-base-color, 40%)
+}
+
+.autosuggest-textarea__suggestions__item {
+  &:hover,
+  &:focus,
+  &:active,
+  &.selected {
+    background: darken($ui-base-color, 4%);
+    color: $primary-text-color;
+  }
+}
+
+.react-toggle-track {
+  background: $ui-secondary-color;
+}
+
+.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
+  background: lighten($ui-secondary-color, 10%);
+}
+
+.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track {
+  background: darken($ui-highlight-color, 10%);
+}
+
+// Change the background colors of modals
+.actions-modal,
+.boost-modal,
+.favourite-modal,
+.confirmation-modal,
+.mute-modal,
+.block-modal,
+.report-modal,
+.embed-modal,
+.error-modal,
+.onboarding-modal,
+.report-modal__comment .setting-text__wrapper,
+.report-modal__comment .setting-text {
+  background: $primary-text-color;
+  border: 1px solid lighten($ui-base-color, 8%);
+}
+
+.report-modal__comment {
+  border-right-color: lighten($ui-base-color, 8%);
+}
+
+.report-modal__container {
+  border-top-color: lighten($ui-base-color, 8%);
+}
+
+.boost-modal__action-bar,
+.confirmation-modal__action-bar,
+.mute-modal__action-bar,
+.block-modal__action-bar,
+.onboarding-modal__paginator,
+.error-modal__footer {
+  background: darken($ui-base-color, 20%);
+
+  .onboarding-modal__nav,
+  .error-modal__nav {
+    &:hover,
+    &:focus,
+    &:active {
+      background-color: darken($ui-base-color, 12%);
+    }
+  }
+}
+
+// Change the default color used for the text in an empty column or on the error column
+.empty-column-indicator,
+.error-column {
+  color: darken($ui-base-color, 60%);
+}
+
+// Change the default colors used on some parts of the profile pages
+.activity-stream-tabs {
+
+  background: $account-background-color;
+
+  a {
+    &.active {
+      color: $ui-primary-color;
+      }
+  }
+
+}
+
+.activity-stream {
+
+  .entry {
+    background: $account-background-color;
+  }
+
+  .status.light {
+
+    .status__content {
+      color: $primary-text-color;
+    }
+
+    .display-name {
+      strong {
+        color: $primary-text-color;
+      }
+    }
+
+  }
+
+}
+
+.accounts-grid {
+  .account-grid-card {
+
+    .controls {
+      .icon-button {
+        color: $ui-secondary-color;
+      }
+    }
+
+    .name {
+      a {
+        color: $primary-text-color;
+      }
+    }
+
+    .username {
+      color: $ui-secondary-color;
+    }
+
+    .account__header__content {
+      color: $primary-text-color;
+    }
+
+  }
+}
+
+.button.logo-button {
+  color: $white;
+
+  svg {
+    fill: $white;
+  }
+}
+
+.public-layout {
+  .header,
+  .public-account-header,
+  .public-account-bio {
+    box-shadow: none;
+  }
+
+  .header {
+    background: lighten($ui-base-color, 12%);
+  }
+
+  .public-account-header {
+    &__image {
+      background: lighten($ui-base-color, 12%);
+
+      &::after {
+        box-shadow: none;
+      }
+    }
+
+    &__tabs {
+      &__name {
+        h1,
+        h1 small {
+          color: $white;
+        }
+      }
+    }
+  }
+}
+
+.account__section-headline a.active::after {
+  border-color: transparent transparent $white;
+}
+
+.hero-widget,
+.box-widget,
+.contact-widget,
+.landing-page__information.contact-widget,
+.moved-account-widget,
+.memoriam-widget,
+.activity-stream,
+.nothing-here,
+.directory__tag > a,
+.directory__tag > div {
+  box-shadow: none;
+}
+
+.admin-wrapper {
+  .sidebar ul .simple-navigation-active-leaf a {
+    color: $black;
+  }
+}
+
+.simple_form button, .button {
+  color: $black;
+}
+
+.poll__input {
+  border: 1px solid pink;
+}
+
+.poll .button.button-secondary {
+  background: $primary-text-color;
+  color: $black;
+}
+
+button.icon-button {
+  color: $ui-secondary-color;
+}
+
+button.icon-button i.fa-retweet {
+  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="209"><path d="M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z" fill="#{hex-color($ui-secondary-color)}" stroke-width="0"/><path d="M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z" fill="#{hex-color($ui-secondary-color)}" stroke-width="0"/></svg>');
+}
+
+button.icon-button.active i.fa-retweet {
+  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="209"><path d="M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z" fill="pink" stroke-width="0"/><path d="M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z" fill="pink" stroke-width="0"/></svg>');
+  box-shadow: 0px 0px 5px pink, inset 0px 0px 5px pink;
+  border-radius: 20px;
+}
+
diff --git a/app/javascript/flavours/glitch/styles/nightshade/variables.scss b/app/javascript/flavours/glitch/styles/nightshade/variables.scss
new file mode 100644
index 000000000..46f055a8f
--- /dev/null
+++ b/app/javascript/flavours/glitch/styles/nightshade/variables.scss
@@ -0,0 +1,41 @@
+// Dependent colors
+$black: #000000;
+$white: #ffffff;
+
+$classic-base-color: #c8b7c1;
+$classic-primary-color: #4C3A45;
+$classic-secondary-color: #2C2028;
+$classic-highlight-color: #bca9b4;
+
+$ui-base-color: $classic-secondary-color !default;
+$ui-base-lighter-color: darken($ui-base-color, 57%);
+$ui-highlight-color: $classic-highlight-color !default;
+$ui-primary-color: $classic-primary-color !default;
+$ui-secondary-color: $classic-base-color !default;
+
+$primary-text-color: #e9e2e6 !default;
+$darker-text-color: $classic-base-color !default;
+$dark-text-color: #a68c9c;
+$action-button-color: #606984;
+
+$success-green: #80b38b;
+$error-red: #b38080;
+$warning-red: #b38c80;
+
+$base-overlay-background: $black !default;
+
+$inverted-text-color: #291822 !default;
+$lighter-text-color: $classic-base-color !default;
+$light-text-color: #6A5160;
+
+$account-background-color: #4C3A45 !default;
+
+@function darken($color, $amount) {
+  @return hsl(hue($color), saturation($color), lightness($color) + $amount);
+}
+
+@function lighten($color, $amount) {
+  @return hsl(hue($color), saturation($color), lightness($color) - $amount);
+}
+
+//$emojis-requiring-inversion: 'chains';
diff --git a/app/javascript/flavours/glitch/styles/variables.scss b/app/javascript/flavours/glitch/styles/variables.scss
index 1ed1a5778..9ddabe6f4 100644
--- a/app/javascript/flavours/glitch/styles/variables.scss
+++ b/app/javascript/flavours/glitch/styles/variables.scss
@@ -49,11 +49,11 @@ $media-modal-media-max-width: 100%;
 // put margins on top and bottom of image to avoid the screen covered by image.
 $media-modal-media-max-height: 80%;
 
-$no-gap-breakpoint: 415px;
+$no-gap-breakpoint: 700px;
 
-$font-sans-serif: 'mastodon-font-sans-serif' !default;
-$font-display: 'mastodon-font-display' !default;
-$font-monospace: 'mastodon-font-monospace' !default;
+$font-sans-serif: 'opensans' !default;
+$font-display: 'montserrat' !default;
+$font-monospace: 'roboto-mono' !default;
 
 // Avatar border size (8% default, 100% for rounded avatars)
 $ui-avatar-border-size: 8%;
diff --git a/app/javascript/flavours/glitch/styles/widgets.scss b/app/javascript/flavours/glitch/styles/widgets.scss
index 531425573..da136da03 100644
--- a/app/javascript/flavours/glitch/styles/widgets.scss
+++ b/app/javascript/flavours/glitch/styles/widgets.scss
@@ -556,7 +556,6 @@ $fluid-breakpoint: $maximum-width + 20px;
 
 .table-of-contents {
   background: darken($ui-base-color, 4%);
-  min-height: 100%;
   font-size: 14px;
   border-radius: 4px;
 
diff --git a/app/javascript/fonts/opensans/LICENSE.txt b/app/javascript/fonts/opensans/LICENSE.txt
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/app/javascript/fonts/opensans/LICENSE.txt
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/app/javascript/fonts/opensans/OpenSans-Bold.ttf b/app/javascript/fonts/opensans/OpenSans-Bold.ttf
new file mode 100644
index 000000000..efdd5e84a
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-Bold.ttf
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-Bold.woff2 b/app/javascript/fonts/opensans/OpenSans-Bold.woff2
new file mode 100644
index 000000000..e98487337
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-Bold.woff2
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-BoldItalic.ttf b/app/javascript/fonts/opensans/OpenSans-BoldItalic.ttf
new file mode 100644
index 000000000..9bf9b4e97
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-BoldItalic.ttf
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-BoldItalic.woff2 b/app/javascript/fonts/opensans/OpenSans-BoldItalic.woff2
new file mode 100644
index 000000000..68666ea6f
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-BoldItalic.woff2
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-ExtraBold.ttf b/app/javascript/fonts/opensans/OpenSans-ExtraBold.ttf
new file mode 100644
index 000000000..67fcf0fb2
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-ExtraBold.ttf
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-ExtraBold.woff2 b/app/javascript/fonts/opensans/OpenSans-ExtraBold.woff2
new file mode 100644
index 000000000..abdc7b7ca
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-ExtraBold.woff2
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-ExtraBoldItalic.ttf b/app/javascript/fonts/opensans/OpenSans-ExtraBoldItalic.ttf
new file mode 100644
index 000000000..086722809
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-ExtraBoldItalic.ttf
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-ExtraBoldItalic.woff2 b/app/javascript/fonts/opensans/OpenSans-ExtraBoldItalic.woff2
new file mode 100644
index 000000000..6e8337523
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-ExtraBoldItalic.woff2
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-Italic.ttf b/app/javascript/fonts/opensans/OpenSans-Italic.ttf
new file mode 100644
index 000000000..117856707
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-Italic.ttf
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-Italic.woff2 b/app/javascript/fonts/opensans/OpenSans-Italic.woff2
new file mode 100644
index 000000000..9398fd5da
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-Italic.woff2
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-Light.ttf b/app/javascript/fonts/opensans/OpenSans-Light.ttf
new file mode 100644
index 000000000..6580d3a16
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-Light.ttf
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-Light.woff2 b/app/javascript/fonts/opensans/OpenSans-Light.woff2
new file mode 100644
index 000000000..8496eb0f9
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-Light.woff2
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-LightItalic.ttf b/app/javascript/fonts/opensans/OpenSans-LightItalic.ttf
new file mode 100644
index 000000000..1e0c33198
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-LightItalic.ttf
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-LightItalic.woff2 b/app/javascript/fonts/opensans/OpenSans-LightItalic.woff2
new file mode 100644
index 000000000..3ccefa9cb
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-LightItalic.woff2
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-Regular.ttf b/app/javascript/fonts/opensans/OpenSans-Regular.ttf
new file mode 100644
index 000000000..29bfd35a2
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-Regular.ttf
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-Regular.woff2 b/app/javascript/fonts/opensans/OpenSans-Regular.woff2
new file mode 100644
index 000000000..a8b531989
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-Regular.woff2
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-SemiBold.ttf b/app/javascript/fonts/opensans/OpenSans-SemiBold.ttf
new file mode 100644
index 000000000..54e7059cf
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-SemiBold.ttf
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-SemiBold.woff2 b/app/javascript/fonts/opensans/OpenSans-SemiBold.woff2
new file mode 100644
index 000000000..90d827308
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-SemiBold.woff2
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-SemiBoldItalic.ttf b/app/javascript/fonts/opensans/OpenSans-SemiBoldItalic.ttf
new file mode 100644
index 000000000..aebcf1421
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-SemiBoldItalic.ttf
Binary files differdiff --git a/app/javascript/fonts/opensans/OpenSans-SemiBoldItalic.woff2 b/app/javascript/fonts/opensans/OpenSans-SemiBoldItalic.woff2
new file mode 100644
index 000000000..ca7c2011a
--- /dev/null
+++ b/app/javascript/fonts/opensans/OpenSans-SemiBoldItalic.woff2
Binary files differdiff --git a/app/javascript/locales/locale-data/en-MP.js b/app/javascript/locales/locale-data/en-MP.js
new file mode 100644
index 000000000..a2defe09a
--- /dev/null
+++ b/app/javascript/locales/locale-data/en-MP.js
@@ -0,0 +1,8 @@
+/*eslint eqeqeq: "off"*/
+/*eslint no-nested-ternary: "off"*/
+/*eslint quotes: "off"*/
+
+export default [{
+  locale: 'en-MP',
+  parentLocale: 'en',
+}];
\ No newline at end of file
diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js
index dca44917a..d0a55538f 100644
--- a/app/javascript/mastodon/actions/importer/normalizer.js
+++ b/app/javascript/mastodon/actions/importer/normalizer.js
@@ -53,9 +53,12 @@ export function normalizeStatus(status, normalOldStatus) {
     normalStatus.poll = status.poll.id;
   }
 
+  const oldUpdatedAt = normalOldStatus ? normalOldStatus.updated_at || normalOldStatus.created_at : null;
+  const newUpdatedAt = normalStatus ? normalStatus.updated_at || normalStatus.created_at : null;
+
   // Only calculate these values when status first encountered
   // Otherwise keep the ones already in the reducer
-  if (normalOldStatus) {
+  if (normalOldStatus && oldUpdatedAt === newUpdatedAt) {
     normalStatus.search_index = normalOldStatus.get('search_index');
     normalStatus.contentHtml = normalOldStatus.get('contentHtml');
     normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index beb5c6a4a..1adc1b815 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -18,6 +18,7 @@ import {
 } from './announcements';
 import { fetchFilters } from './filters';
 import { getLocale } from '../locales';
+import { resetCompose } from '../actions/compose';
 
 const { messages } = getLocale();
 
@@ -96,6 +97,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
         case 'announcement.delete':
           dispatch(deleteAnnouncement(data.payload));
           break;
+        case 'refresh':
+          dispatch(resetCompose());
+          window.location.reload();
+          break;
         }
       },
     };
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
index b7babd4ad..88fde4ee0 100644
--- a/app/javascript/mastodon/components/status_action_bar.js
+++ b/app/javascript/mastodon/components/status_action_bar.js
@@ -250,10 +250,8 @@ class StatusActionBar extends ImmutablePureComponent {
     menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
     menu.push(null);
 
-    if (status.getIn(['account', 'id']) === me || withDismiss) {
-      menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
-      menu.push(null);
-    }
+    menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
+    menu.push(null);
 
     if (status.getIn(['account', 'id']) === me) {
       if (publicStatus) {
diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js
index 3200f2d82..df05d8515 100644
--- a/app/javascript/mastodon/components/status_content.js
+++ b/app/javascript/mastodon/components/status_content.js
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
 import { isRtl } from '../rtl';
 import { FormattedMessage } from 'react-intl';
 import Permalink from './permalink';
+import RelativeTimestamp from './relative_timestamp';
 import classnames from 'classnames';
 import PollContainer from 'mastodon/containers/poll_container';
 import Icon from 'mastodon/components/icon';
@@ -180,6 +181,20 @@ export default class StatusContent extends React.PureComponent {
       return null;
     }
 
+    const edited = (status.get('edited') === 0) ? null : (
+      <div className='status__edit-notice'>
+        <FormattedMessage
+          id='status.edited'
+          defaultMessage='{count, plural, one {# edit} other {# edits}} · last update: {updated_at}'
+          key={`edit-${status.get('id')}`}
+          values={{
+            count: status.get('edited'),
+            updated_at: <RelativeTimestamp timestamp={status.get('updated_at')} />,
+          }}
+        />
+      </div>
+    );
+
     const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
     const renderReadMore = this.props.onClick && status.get('collapsed');
     const renderViewThread = this.props.showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']);
@@ -232,6 +247,7 @@ export default class StatusContent extends React.PureComponent {
             <button tabIndex='0' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick}>{toggleText}</button>
           </p>
 
+          {edited}
           {mentionsPlaceholder}
 
           <div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
@@ -244,6 +260,8 @@ export default class StatusContent extends React.PureComponent {
     } else if (this.props.onClick) {
       const output = [
         <div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content'>
+          {edited}
+
           <div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} />
 
           {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
@@ -260,6 +278,8 @@ export default class StatusContent extends React.PureComponent {
     } else {
       return (
         <div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle}>
+          {edited}
+
           <div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} />
 
           {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
diff --git a/app/javascript/mastodon/features/compose/components/poll_form.js b/app/javascript/mastodon/features/compose/components/poll_form.js
index 88894ae59..72ffeff09 100644
--- a/app/javascript/mastodon/features/compose/components/poll_form.js
+++ b/app/javascript/mastodon/features/compose/components/poll_form.js
@@ -89,7 +89,7 @@ class Option extends React.PureComponent {
 
           <AutosuggestInput
             placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
-            maxLength={100}
+            maxlength={202}
             value={title}
             onChange={this.handleOptionTitleChange}
             suggestions={this.props.suggestions}
@@ -157,7 +157,7 @@ class PollForm extends ImmutablePureComponent {
         </ul>
 
         <div className='poll__footer'>
-          <button disabled={options.size >= 5} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
+          <button disabled={options.size >= 33} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
 
           {/* eslint-disable-next-line jsx-a11y/no-onchange */}
           <select value={expiresIn} onChange={this.handleSelectDuration}>
diff --git a/app/javascript/mastodon/locales/en-MP.json b/app/javascript/mastodon/locales/en-MP.json
new file mode 100644
index 000000000..ca175119e
--- /dev/null
+++ b/app/javascript/mastodon/locales/en-MP.json
@@ -0,0 +1,176 @@
+{
+  "account.add_account_note": "Add note for @{name}",
+  "account.disclaimer_full": "You're viewing the cached version of a profile from another server.",
+  "account.followers.empty": "No one follows this creature yet.",
+  "account.follows.empty": "This creature doesn't follow anyone yet.",
+  "account.follows": "Follows",
+  "account.locked_info": "This creature manually reviews who can follow them.",
+  "account.media": "Media",
+  "account.mentions": "Mentions",
+  "account.posts_with_replies": "Replies",
+  "account.posts": "Blog",
+  "account.reblogs": "Boosts",
+  "account.statuses_counter": "{count, plural, one {{counter} Roar} other {{counter} Roars}}",
+  "account.threads": "Threads",
+  "account.view_full_profile": "View the original",
+  "advanced_options.local-only.long": "Do not post to other servers",
+  "column_header.profile": "Creature",
+  "column.blocks": "Blocked creatures",
+  "column.community": "Monsterpit",
+  "column.directory": "Creature directory",
+  "column.favourites": "Admirations",
+  "column.mutes": "Muted creatures",
+  "column.pins": "Pins",
+  "column.public": "Fediverse",
+  "column.toot": "Roars & Growls",
+  "community.column_settings.local_only": "Monsterpit only",
+  "community.column_settings.remote_only": "Rowdy tavern mode",
+  "compose_form.clear": "Double-click to clear",
+  "compose_form.direct_message_warning": "This roar will only be sent to the mentioned creatures.",
+  "compose_form.hashtag_warning": "This roar won't be listed under any hashtag as it is unlisted. Only public roars can be searched by hashtag.",
+  "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
+  "compose_form.placeholder": "Roar shamelessly!",
+  "compose_form.publish": "Roar",
+  "compose_form.spoiler_placeholder": "Enter content notes here",
+  "compose_form.spoiler.marked": "Text is hidden behind content notes",
+  "compose_form.spoiler": "Enter content notes here",
+  "confirmations.delete.message": "Are you sure you want to delete this roar?",
+  "confirmations.mute.explanation": "This will hide roars from them and roars mentioning them, but it will still allow them to see your roars and follow you.",
+  "confirmations.publish.confirm": "Publish",
+  "confirmations.publish.message": "Are you ready to publish your roar?",
+  "confirmations.redraft.message": "Are you sure you want to delete and redraft this roar? Admirations and boosts will be lost, and replies to the original roar will be orphaned.",
+  "content-type.change": "Content type",
+  "directory.federated": "From Fediverse",
+  "directory.local": "From Monsterpit",
+  "embed.instructions": "Embed this roar on your website by copying the code below.",
+  "empty_column.account_timeline": "No roars here!",
+  "empty_column.blocks": "You haven't blocked any creatures yet.",
+  "empty_column.bookmarked_statuses": "You don't have any bookmarked roars yet. When you bookmark one, it will show up here.",
+  "empty_column.community": "The Monsterpit timeline is empty. Write something publicly to get the ball rolling!",
+  "empty_column.favourited_statuses": "You don't have any admired roars yet. When you admire one, it will show up here.",
+  "empty_column.favourites": "No one has admired this roar yet. When someone does, they will show up here.",
+  "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other creatures.",
+  "empty_column.list": "There is nothing in this list yet. When members of this list post new roars, they will appear here.",
+  "empty_column.mutes": "You haven't muted any creatures yet.",
+  "empty_column.public": "There is nothing here! Write something publicly, or manually follow creatures from other servers to fill it up",
+  "error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Monsterpit through a different browser or native app.",
+  "follow_request.authorize": "Accept",
+  "getting_started.directory": "Creature directory",
+  "getting_started.invite": "Invite creatures",
+  "getting_started.open_source_notice": "Monsterfork is open source software.  If you'd like to explore its code, you may visit the repository on {monsterware}.",
+  "introduction.federation.federated.headline": "Fediverse",
+  "introduction.federation.federated.text": "Public roars from other servers will appear in the Fediverse timeline.",
+  "introduction.federation.home.text": "Roars from creatures you follow will appear in your home feed.",
+  "introduction.federation.local.headline": "Monsterpit",
+  "introduction.federation.local.text": "Public roars from people on Monsterpit will appear in the Monsterpit timeline.",
+  "introduction.interactions.action": "Finish tutorial",
+  "introduction.interactions.favourite.headline": "Admire",
+  "introduction.interactions.favourite.text": "You can save a roar for later, and let the author know that you liked it, by admiring it.",
+  "introduction.interactions.reblog.text": "You can share other creature's roars with your followers by boosting them.",
+  "introduction.interactions.reply.text": "You can reply to other creature's and your own roars, which will chain them together in a conversation.",
+  "keyboard_shortcuts.blocked": "to open blocked creatures list",
+  "keyboard_shortcuts.column": "to focus a roar in one of the columns",
+  "keyboard_shortcuts.enter": "to open roar",
+  "keyboard_shortcuts.favourite": "to admire",
+  "keyboard_shortcuts.favourites": "to open admirations list",
+  "keyboard_shortcuts.federated": "to open Fediverse timeline",
+  "keyboard_shortcuts.local": "to open Monsterpit timeline",
+  "keyboard_shortcuts.muted": "to open muted creatures list",
+  "keyboard_shortcuts.pinned": "to open pinned roars list",
+  "keyboard_shortcuts.spoilers": "to show/hide content note field",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind content notes",
+  "keyboard_shortcuts.toot": "to start a new roar",
+  "lists.search": "Search among creatures you follow",
+  "mute_modal.hide_notifications": "Hide notifications from this creature?",
+  "navigation_bar.blocks": "Blocked creatures",
+  "navigation_bar.community_timeline": "Monsterpit",
+  "navigation_bar.compose": "Compose new roar",
+  "navigation_bar.favourites": "Admirations",
+  "navigation_bar.logout": "Sleep",
+  "navigation_bar.mutes": "Muted creatures",
+  "navigation_bar.pins": "Pins",
+  "navigation_bar.public_timeline": "Fediverse",
+  "notification_purge.start": "Enter notification cleaning mode",
+  "notification.favourite": "{name} admired your roar",
+  "notification.follow_request": "{name} wants to follow you",
+  "notification.follow": "{name} followed you",
+  "notification.mention": "{name} mentioned you",
+  "notification.own_poll": "Your poll has ended",
+  "notification.poll": "A poll you have voted in has ended",
+  "notification.reblog": "{name} boosted your roar",
+  "notifications.clear": "Clear notifications",
+  "notifications.column_settings.favourite": "Admirations:",
+  "notifications.filter.favourites": "Admirations",
+  "poll.total_people": "{count, plural, one {# creature} other {# creatures}}",
+  "privacy.change": "Adjust roar privacy",
+  "privacy.direct.long": "Visible for mentioned creatures only",
+  "report.forward_hint": "The creature is from another server. Send an anonymized copy of the report there as well?",
+  "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this creature below:",
+  "search_popout.tips.full_text": "Simple text returns roars you have written, admired, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
+  "search_popout.tips.status": "roar",
+  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+  "search_popout.tips.user": "creature",
+  "search_results.accounts": "Creatures",
+  "search_results.statuses_fts_disabled": "Searching roars by their content is not enabled on this Mastodon server.",
+  "search_results.statuses": "Roars",
+  "settings.always_show_spoilers_field": "Always show content notes field",
+  "settings.auto_collapse_lengthy": "Lengthy roars",
+  "settings.auto_collapse_media": "Media",
+  "settings.collapsed_statuses": "Collapsed roars",
+  "settings.confirm_boost_missing_media_description": "Show confirmation dialog before boosting roars lacking media descriptions",
+  "settings.confirm_missing_media_description": "Show confirmation dialog before sending roars lacking media descriptions",
+  "settings.content_warnings_filter": "Avoid expanding roars with content notes containing:",
+  "settings.content_warnings": "Content notes",
+  "settings.enable_collapsed": "Enable collapsed roars",
+  "settings.enable_content_warnings_auto_unfold": "Auto-expand roars with content notes",
+  "settings.filtering_behavior.cw": "Add the filtered phrase to the roar's content notes",
+  "settings.image_backgrounds_media": "Preview collapsed media",
+  "settings.image_backgrounds_users": "Give collapsed roars an image background",
+  "settings.prepend_cw_re": "Prepend \"re:\" to content notes when replying",
+  "settings.rewrite_mentions": "Rewrite mentions in roars:",
+  "settings.show_action_bar": "Show action buttons in collapsed roars",
+  "settings.show_content_type_choice": "Show content-type choice when authoring roars",
+  "settings.side_arm_reply_mode.copy": "Copy privacy setting of the roar being replied to",
+  "settings.side_arm_reply_mode.keep": "Keep secondary roar button to set privacy",
+  "settings.side_arm_reply_mode.restrict": "Restrict privacy setting to that of the roar being replied to",
+  "settings.side_arm_reply_mode": "When replying to a roar:",
+  "settings.side_arm": "Secondary roar button:",
+  "status.admin_account": "Moderate @{name}",
+  "status.admin_status": "Moderate roar",
+  "status.article": "Article",
+  "status.cannot_reblog": "This roar cannot be boosted",
+  "status.copy": "Copy link to roar",
+  "status.edit": "Edit",
+  "status.edited": "{count, plural, one {# edit} other {# edits}} · last update: {updated_at}",
+  "status.favourite": "Admire",
+  "status.has_pictures": "Features attached pictures",
+  "status.in_reply_to": "This roar is a reply",
+  "status.is_poll": "This roar is a poll",
+  "status.local_only": "Monsterpit-only",
+  "status.media.description": "Attachment #{index}: ",
+  "status.media.descriptions": "Attachments {list}: ",
+  "status.open": "Open this roar",
+  "status.permissions.title": "Show extended permissions...",
+  "status.permissions.visibility.account": "{visibility} 🡲 {domain}",
+  "status.permissions.visibility.status": "{visibility} 🡲 {domain}",
+  "status.pinned": "Pinned",
+  "status.publish": "Publish",
+  "status.reblogged_by": "{name}",
+  "status.reblogs.empty": "No one has boosted this roar yet. When someone does, they will show up here.",
+  "status.show_article": "Show article",
+  "status.show_less_all": "Hide all",
+  "status.show_less": "Hide",
+  "status.show_more_all": "Reveal all",
+  "status.show_more": "Reveal",
+  "status.show_thread": "Reveal thread",
+  "status.tags": "Show all tags...",
+  "status.unpublished": "Unpublished",
+  "tabs_bar.federated_timeline": "Fediverse",
+  "tabs_bar.local_timeline": "Monsterpit",
+  "timeline_hint.resources.statuses": "Older roars",
+  "trends.counter_by_accounts": "{count, plural, one {{counter} creature} other {{counter} creatures}} talking",
+  "ui.beforeunload": "Your draft will be lost if you leave the web page.",
+  "upload_form.edit": "Add description text",
+  "upload_modal.edit_media": "Add description text",
+  "video.expand": "Open video"
+}
diff --git a/app/javascript/mastodon/locales/locale-data/en-MP.js b/app/javascript/mastodon/locales/locale-data/en-MP.js
new file mode 100644
index 000000000..a2defe09a
--- /dev/null
+++ b/app/javascript/mastodon/locales/locale-data/en-MP.js
@@ -0,0 +1,8 @@
+/*eslint eqeqeq: "off"*/
+/*eslint no-nested-ternary: "off"*/
+/*eslint quotes: "off"*/
+
+export default [{
+  locale: 'en-MP',
+  parentLocale: 'en',
+}];
\ No newline at end of file
diff --git a/app/javascript/mastodon/locales/whitelist_en-MP.json b/app/javascript/mastodon/locales/whitelist_en-MP.json
new file mode 100644
index 000000000..32960f8ce
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_en-MP.json
@@ -0,0 +1,2 @@
+[
+]
\ No newline at end of file
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 4c0ba1c36..67ce96feb 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -205,7 +205,9 @@ const expandMentions = status => {
   const fragment = domParser.parseFromString(status.get('content'), 'text/html').documentElement;
 
   status.get('mentions').forEach(mention => {
-    fragment.querySelector(`a[href="${mention.get('url')}"]`).textContent = `@${mention.get('acct')}`;
+    const selection = fragment.querySelector(`a[href="${mention.get('url')}"]`);
+    if (!selection) return;
+    selection.textContent = `@${mention.get('acct')}`;
   });
 
   return fragment.innerHTML;
diff --git a/app/javascript/skins/glitch/nightshade/common.scss b/app/javascript/skins/glitch/nightshade/common.scss
new file mode 100644
index 000000000..ada0fd156
--- /dev/null
+++ b/app/javascript/skins/glitch/nightshade/common.scss
@@ -0,0 +1 @@
+@import 'flavours/glitch/styles/nightshade';
diff --git a/app/javascript/skins/glitch/nightshade/names.yml b/app/javascript/skins/glitch/nightshade/names.yml
new file mode 100644
index 000000000..db7010ec5
--- /dev/null
+++ b/app/javascript/skins/glitch/nightshade/names.yml
@@ -0,0 +1,5 @@
+en:
+  skins:
+    glitch:
+      nightshade: Nightshade
+
diff --git a/app/javascript/styles/fonts/montserrat.scss b/app/javascript/styles/fonts/montserrat.scss
index 80c2329b0..103dee529 100644
--- a/app/javascript/styles/fonts/montserrat.scss
+++ b/app/javascript/styles/fonts/montserrat.scss
@@ -1,5 +1,5 @@
 @font-face {
-  font-family: 'mastodon-font-display';
+  font-family: 'montserrat';
   src: local('Montserrat'),
     url('~fonts/montserrat/Montserrat-Regular.woff2') format('woff2'),
     url('~fonts/montserrat/Montserrat-Regular.woff') format('woff'),
@@ -9,7 +9,7 @@
 }
 
 @font-face {
-  font-family: 'mastodon-font-display';
+  font-family: 'montserrat';
   src: local('Montserrat Medium'),
     url('~fonts/montserrat/Montserrat-Medium.ttf') format('truetype');
   font-weight: 500;
diff --git a/app/javascript/styles/fonts/opensans.scss b/app/javascript/styles/fonts/opensans.scss
new file mode 100644
index 000000000..6da41e30a
--- /dev/null
+++ b/app/javascript/styles/fonts/opensans.scss
@@ -0,0 +1,134 @@
+@font-face {
+  font-family: 'opensans';
+  src: local('Open Sans ExtraBold'),
+    url('~fonts/opensans/OpenSans-ExtraBold.woff2') format('woff2'),
+    url('~fonts/opensans/OpenSans-ExtraBold.ttf') format('truetype');
+  font-weight: bolder;
+  font-style: normal;
+}
+
+@font-face {
+  font-family: 'opensans';
+  src: local('Open Sans Bold'),
+    url('~fonts/opensans/OpenSans-Bold.woff2') format('woff2'),
+    url('~fonts/opensans/OpenSans-Bold.ttf') format('truetype');
+  font-weight: bold;
+  font-style: normal;
+}
+
+@font-face {
+  font-family: 'opensans';
+  src: local('Open Sans Bold Italic'),
+    url('~fonts/opensans/OpenSans-BoldItalic.woff2') format('woff2'),
+    url('~fonts/opensans/OpenSans-BoldItalic.ttf') format('truetype');
+  font-weight: bold;
+  font-style: italic;
+}
+
+@font-face {
+  font-family: 'opensans';
+  src: local('Open Sans SemiBold'),
+    url('~fonts/opensans/OpenSans-SemiBold.woff2') format('woff2'),
+    url('~fonts/opensans/OpenSans-SemiBold.ttf') format('truetype');
+  font-weight: 500;
+  font-style: normal;
+}
+
+@font-face {
+  font-family: 'opensans';
+  src: local('Open Sans SemiBold Italic'),
+    url('~fonts/opensans/OpenSans-SemiBoldItalic.woff2') format('woff2'),
+    url('~fonts/opensans/OpenSans-SemiBoldItalic.ttf') format('truetype');
+  font-weight: 500;
+  font-style: italic;
+}
+
+@font-face {
+  font-family: 'opensans';
+  src: local('Open Sans Regular'),
+    url('~fonts/opensans/OpenSans-Regular.woff2') format('woff2'),
+    url('~fonts/opensans/OpenSans-Regular.ttf') format('truetype');
+  font-weight: normal;
+  font-style: normal;
+}
+
+@font-face {
+  font-family: 'opensans';
+  src: local('Open Sans Italic'),
+    url('~fonts/opensans/OpenSans-Italic.woff2') format('woff2'),
+    url('~fonts/opensans/OpenSans-Italic.ttf') format('truetype');
+  font-weight: normal;
+  font-style: italic;
+}
+
+@font-face {
+  font-family: 'opensans';
+  src: local('Open Sans Regular'),
+    url('~fonts/opensans/OpenSans-Regular.woff2') format('woff2'),
+    url('~fonts/opensans/OpenSans-Regular.ttf') format('truetype');
+  font-weight: lighter;
+  font-style: normal;
+}
+
+@font-face {
+  font-family: 'opensans';
+  src: local('Open Sans Italic'),
+    url('~fonts/opensans/OpenSans-Italic.woff2') format('woff2'),
+    url('~fonts/opensans/OpenSans-Italic.ttf') format('truetype');
+  font-weight: lighter;
+  font-style: italic;
+}
+
+@font-face {
+  font-family: 'opensans';
+  src: local('Open Sans Light'),
+    url('~fonts/opensans/OpenSans-Light.woff2') format('woff2'),
+    url('~fonts/opensans/OpenSans-Light.ttf') format('truetype');
+  font-weight: 300;
+  font-style: normal;
+}
+
+@font-face {
+  font-family: 'opensans';
+  src: local('Open Sans Light Italic'),
+    url('~fonts/opensans/OpenSans-LightItalic.woff2') format('woff2'),
+    url('~fonts/opensans/OpenSans-LightItalic.ttf') format('truetype');
+  font-weight: 300;
+  font-style: italic;
+}
+
+@font-face {
+  font-family: 'opensans';
+  src: local('Open Sans Light'),
+    url('~fonts/opensans/OpenSans-Light.woff2') format('woff2'),
+    url('~fonts/opensans/OpenSans-Light.ttf') format('truetype');
+  font-weight: 200;
+  font-style: normal;
+}
+
+@font-face {
+  font-family: 'opensans';
+  src: local('Open Sans Light Italic'),
+    url('~fonts/opensans/OpenSans-LightItalic.woff2') format('woff2'),
+    url('~fonts/opensans/OpenSans-LightItalic.ttf') format('truetype');
+  font-weight: 200;
+  font-style: italic;
+}
+
+@font-face {
+  font-family: 'opensans';
+  src: local('Open Sans Light'),
+    url('~fonts/opensans/OpenSans-Light.woff2') format('woff2'),
+    url('~fonts/opensans/OpenSans-Light.ttf') format('truetype');
+  font-weight: 100;
+  font-style: normal;
+}
+
+@font-face {
+  font-family: 'opensans';
+  src: local('Open Sans Light Italic'),
+    url('~fonts/opensans/OpenSans-LightItalic.woff2') format('woff2'),
+    url('~fonts/opensans/OpenSans-LightItalic.ttf') format('truetype');
+  font-weight: 100;
+  font-style: italic;
+}
\ No newline at end of file
diff --git a/app/javascript/styles/fonts/roboto-mono.scss b/app/javascript/styles/fonts/roboto-mono.scss
index c793aa6ed..b689c87fe 100644
--- a/app/javascript/styles/fonts/roboto-mono.scss
+++ b/app/javascript/styles/fonts/roboto-mono.scss
@@ -1,5 +1,5 @@
 @font-face {
-  font-family: 'mastodon-font-monospace';
+  font-family: 'roboto-mono';
   src: local('Roboto Mono'),
     url('~fonts/roboto-mono/robotomono-regular-webfont.woff2') format('woff2'),
     url('~fonts/roboto-mono/robotomono-regular-webfont.woff') format('woff'),
diff --git a/app/javascript/styles/fonts/roboto.scss b/app/javascript/styles/fonts/roboto.scss
index b75fdb927..a34cc693c 100644
--- a/app/javascript/styles/fonts/roboto.scss
+++ b/app/javascript/styles/fonts/roboto.scss
@@ -1,5 +1,5 @@
 @font-face {
-  font-family: 'mastodon-font-sans-serif';
+  font-family: 'roboto';
   src: local('Roboto Italic'),
     url('~fonts/roboto/roboto-italic-webfont.woff2') format('woff2'),
     url('~fonts/roboto/roboto-italic-webfont.woff') format('woff'),
@@ -10,7 +10,7 @@
 }
 
 @font-face {
-  font-family: 'mastodon-font-sans-serif';
+  font-family: 'roboto';
   src: local('Roboto Bold'),
     url('~fonts/roboto/roboto-bold-webfont.woff2') format('woff2'),
     url('~fonts/roboto/roboto-bold-webfont.woff') format('woff'),
@@ -21,7 +21,7 @@
 }
 
 @font-face {
-  font-family: 'mastodon-font-sans-serif';
+  font-family: 'roboto';
   src: local('Roboto Medium'),
     url('~fonts/roboto/roboto-medium-webfont.woff2') format('woff2'),
     url('~fonts/roboto/roboto-medium-webfont.woff') format('woff'),
@@ -32,7 +32,7 @@
 }
 
 @font-face {
-  font-family: 'mastodon-font-sans-serif';
+  font-family: 'roboto';
   src: local('Roboto'),
     url('~fonts/roboto/roboto-regular-webfont.woff2') format('woff2'),
     url('~fonts/roboto/roboto-regular-webfont.woff') format('woff'),
diff --git a/app/javascript/styles/mailer.scss b/app/javascript/styles/mailer.scss
index e25a80c04..3b3ca000d 100644
--- a/app/javascript/styles/mailer.scss
+++ b/app/javascript/styles/mailer.scss
@@ -1,5 +1,7 @@
 @import 'mastodon/variables';
+@import 'fonts/opensans';
 @import 'fonts/roboto';
+@import 'fonts/roboto-mono';
 
 table,
 td,
diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/variables.scss
index 8602c3dde..1b2499aa6 100644
--- a/app/javascript/styles/mastodon/variables.scss
+++ b/app/javascript/styles/mastodon/variables.scss
@@ -51,6 +51,6 @@ $media-modal-media-max-height: 80%;
 
 $no-gap-breakpoint: 415px;
 
-$font-sans-serif: 'mastodon-font-sans-serif' !default;
-$font-display: 'mastodon-font-display' !default;
-$font-monospace: 'mastodon-font-monospace' !default;
+$font-sans-serif: 'roboto' !default;
+$font-display: 'montserrat' !default;
+$font-monospace: 'roboto-mono' !default;