about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours/glitch')
-rw-r--r--app/javascript/flavours/glitch/actions/accounts.js4
-rw-r--r--app/javascript/flavours/glitch/actions/compose.js9
-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/notifications.js7
-rw-r--r--app/javascript/flavours/glitch/actions/push_notifications/registerer.js4
-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.js24
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/publisher.js24
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js9
-rw-r--r--app/javascript/flavours/glitch/features/getting_started/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/list_adder/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/lists/index.js5
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/follow.js12
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js7
-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/list_panel.js5
-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/settings.js3
-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/columns.scss14
-rw-r--r--app/javascript/flavours/glitch/styles/components/composer.scss13
-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/flavours/glitch/util/initial_state.js2
58 files changed, 1681 insertions, 139 deletions
diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js
index 912a3d179..a4eef2fd2 100644
--- a/app/javascript/flavours/glitch/actions/accounts.js
+++ b/app/javascript/flavours/glitch/actions/accounts.js
@@ -274,11 +274,11 @@ export function unblockAccountFail(error) {
 };
 
 
-export function muteAccount(id, notifications, duration=0) {
+export function muteAccount(id, notifications, timelinesOnly, duration=0) {
   return (dispatch, getState) => {
     dispatch(muteAccountRequest(id));
 
-    api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, duration }).then(response => {
+    api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, timelinesOnly, duration }).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..8c126c6e2 100644
--- a/app/javascript/flavours/glitch/actions/compose.js
+++ b/app/javascript/flavours/glitch/actions/compose.js
@@ -147,16 +147,16 @@ 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;
     }
 
     dispatch(submitComposeRequest());
-    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),
@@ -165,6 +165,7 @@ export function submitCompose(routerHistory) {
       spoiler_text: spoilerText,
       visibility: getState().getIn(['compose', 'privacy']),
       poll: getState().getIn(['compose', 'poll'], null),
+      local_only: getState().getIn(['compose', 'advanced_options', 'do_not_federate']),
     }, {
       headers: {
         'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
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 2bacfadb7..b85cc7863 100644
--- a/app/javascript/flavours/glitch/actions/mutes.js
+++ b/app/javascript/flavours/glitch/actions/mutes.js
@@ -14,6 +14,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_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
+export const MUTES_TOGGLE_TIMELINES_ONLY = 'MUTES_TOGGLE_TIMELINES_ONLY';
 
 export function fetchMutes() {
   return (dispatch, getState) => {
@@ -114,3 +115,9 @@ export function changeMuteDuration(duration) {
     });
   };
 }
+
+export function toggleTimelinesOnly() {
+  return dispatch => {
+    dispatch({ type: MUTES_TOGGLE_TIMELINES_ONLY });
+  };
+}
diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js
index 7273191b2..091a4b435 100644
--- a/app/javascript/flavours/glitch/actions/notifications.js
+++ b/app/javascript/flavours/glitch/actions/notifications.js
@@ -13,7 +13,7 @@ import { defineMessages } from 'react-intl';
 import { List as ImmutableList } from 'immutable';
 import { unescapeHTML } from 'flavours/glitch/util/html';
 import { getFiltersRegex } from 'flavours/glitch/selectors';
-import { usePendingItems as preferPendingItems } from 'flavours/glitch/util/initial_state';
+import { usePendingItems as preferPendingItems, webPushEnabled } from 'flavours/glitch/util/initial_state';
 import compareId from 'flavours/glitch/util/compare_id';
 import { searchTextFromRawStatus } from 'flavours/glitch/actions/importer/normalizer';
 import { requestNotificationPermission } from 'flavours/glitch/util/notifications';
@@ -337,6 +337,11 @@ export function markNotificationsAsRead() {
 // Browser support
 export function setupBrowserNotifications() {
   return dispatch => {
+    if (!webPushEnabled) {
+      dispatch(setBrowserSupport(false));
+      return;
+    }
+
     dispatch(setBrowserSupport('Notification' in window));
     if ('Notification' in window) {
       dispatch(setBrowserPermission(Notification.permission));
diff --git a/app/javascript/flavours/glitch/actions/push_notifications/registerer.js b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js
index 8fdb239f7..1f9ebe569 100644
--- a/app/javascript/flavours/glitch/actions/push_notifications/registerer.js
+++ b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js
@@ -1,7 +1,9 @@
 import api from 'flavours/glitch/util/api';
+import { webPushEnabled } from 'flavours/glitch/util/initial_state';
 import { pushNotificationsSetting } from 'flavours/glitch/util/settings';
 import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
 
+
 // Taken from https://www.npmjs.com/package/web-push
 const urlBase64ToUint8Array = (base64String) => {
   const padding = '='.repeat((4 - base64String.length % 4) % 4);
@@ -49,7 +51,7 @@ const sendSubscriptionToBackend = (getState, subscription, me) => {
 };
 
 // Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
-const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
+const supportsPushNotifications = (webPushEnabled && 'serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
 
 export function register () {
   return (dispatch, getState) => {
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 890a422d3..7521d4645 100644
--- a/app/javascript/flavours/glitch/components/media_gallery.js
+++ b/app/javascript/flavours/glitch/components/media_gallery.js
@@ -385,6 +385,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 })}>
@@ -397,6 +457,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 1b7dce4c4..0de36ce2e 100644
--- a/app/javascript/flavours/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -74,6 +74,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,
@@ -373,7 +375,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);
     }
   };
@@ -689,6 +691,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) {
@@ -710,6 +715,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'),
       unread,
diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js
index 2ccb02c62..b844590c0 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}' },
@@ -53,6 +55,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,
@@ -115,7 +119,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);
@@ -125,6 +129,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);
   }
@@ -211,10 +223,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) {
@@ -223,6 +233,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 7782246a6..5d60ffd97 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';
@@ -39,6 +39,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}}' },
@@ -168,6 +170,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 2d4cc7f49..0f1b83b2d 100644
--- a/app/javascript/flavours/glitch/features/account/components/action_bar.js
+++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js
@@ -44,14 +44,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>
       );
     }
@@ -64,17 +70,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 a6b57d331..d399d4aa9 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 {
 
@@ -128,9 +129,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 c56cc9b8e..c88f6ac89 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/index.js
@@ -19,15 +19,15 @@ import TimelineHint from 'flavours/glitch/components/timeline_hint';
 
 const emptyList = ImmutableList();
 
-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']),
     suspended: state.getIn(['accounts', accountId, 'suspended'], false),
@@ -52,7 +52,7 @@ class AccountTimeline extends ImmutablePureComponent {
     featuredStatusIds: ImmutablePropTypes.list,
     isLoading: PropTypes.bool,
     hasMore: PropTypes.bool,
-    withReplies: PropTypes.bool,
+    filter: PropTypes.string,
     isAccount: PropTypes.bool,
     suspended: PropTypes.bool,
     remote: PropTypes.bool,
@@ -61,24 +61,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 }));
     }
   }
 
@@ -87,7 +87,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..e812ba982 100644
--- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js
+++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
@@ -63,6 +63,8 @@ class ComposeForm extends ImmutablePureComponent {
     layout: PropTypes.string,
     media: ImmutablePropTypes.list,
     sideArm: PropTypes.string,
+    sideArmWarning: PropTypes.bool,
+    privacyWarning: PropTypes.bool,
     sensitive: PropTypes.bool,
     spoilersAlwaysOn: PropTypes.bool,
     mediaDescriptionConfirmation: PropTypes.bool,
@@ -71,10 +73,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 +153,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 +271,7 @@ class ComposeForm extends ImmutablePureComponent {
       handleSecondarySubmit,
       handleSelect,
       handleSubmit,
+      handleClearAll,
       handleRefTextarea,
     } = this;
     const {
@@ -273,19 +289,22 @@ class ComposeForm extends ImmutablePureComponent {
       onFetchSuggestions,
       onPaste,
       privacy,
+      privacyWarning,
       sensitive,
       showSearch,
       sideArm,
+      sideArmWarning,
       spoiler,
       spoilerText,
       suggestions,
       text,
       spoilersAlwaysOn,
+      clearTimeout,
     } = this.props;
 
     let disabledButton = isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia);
 
-    const countText = `${spoilerText}${countableText(text)}${advancedOptions && advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`;
+    const countText = `${spoilerText}${countableText(text)}`;
 
     return (
       <div className='composer'>
@@ -356,8 +375,11 @@ class ComposeForm extends ImmutablePureComponent {
           disabled={disabledButton}
           onSecondarySubmit={handleSecondarySubmit}
           onSubmit={handleSubmit}
+          onClearAll={handleClearAll}
           privacy={privacy}
+          privacyWarning={privacyWarning}
           sideArm={sideArm}
+          sideArmWarning={sideArmWarning}
         />
       </div>
     );
diff --git a/app/javascript/flavours/glitch/features/compose/components/publisher.js b/app/javascript/flavours/glitch/features/compose/components/publisher.js
index 97890f40d..d42c578aa 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,8 +38,11 @@ class Publisher extends ImmutablePureComponent {
     intl: PropTypes.object.isRequired,
     onSecondarySubmit: PropTypes.func,
     onSubmit: PropTypes.func,
+    onClearAll: PropTypes.func,
     privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']),
+    privacyWarning: PropTypes.bool,
     sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']),
+    sideArmWarning: PropTypes.bool,
   };
 
   handleSubmit = () => {
@@ -43,7 +50,7 @@ class Publisher extends ImmutablePureComponent {
   };
 
   render () {
-    const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm } = this.props;
+    const { countText, disabled, intl, onClearAll, onSecondarySubmit, privacy, privacyWarning, sideArm, sideArmWarning } = this.props;
 
     const diff = maxChars - length(countText || '');
     const computedClass = classNames('composer--publisher', {
@@ -53,9 +60,20 @@ 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'
+            className={classNames('side_arm', {privacy_warning: sideArmWarning})}
             disabled={disabled || diff < 0}
             onClick={onSecondarySubmit}
             style={{ padding: null }}
@@ -75,7 +93,7 @@ class Publisher extends ImmutablePureComponent {
           />
         ) : null}
         <Button
-          className='primary'
+          className={classNames('primary', {privacy_warning: privacyWarning})}
           text={function () {
             switch (true) {
             case !!sideArm && sideArm !== 'none':
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..cf953ec3d 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,13 +12,14 @@ import {
   selectComposeSuggestion,
   submitCompose,
   uploadCompose,
+  resetCompose,
 } from 'flavours/glitch/actions/compose';
 import {
   openModal,
 } from 'flavours/glitch/actions/modal';
 import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
 
-import { privacyPreference } from 'flavours/glitch/util/privacy_preference';
+import { privacyPreference, order as privacyOrder } from 'flavours/glitch/util/privacy_preference';
 
 const messages = defineMessages({
   missingDescriptionMessage: {  id: 'confirmations.missing_media_description.message',
@@ -57,7 +58,9 @@ function mapStateToProps (state) {
     media: state.getIn(['compose', 'media_attachments']),
     preselectDate: state.getIn(['compose', 'preselectDate']),
     privacy: state.getIn(['compose', 'privacy']),
+    privacyWarning: replyPrivacy && privacyOrder.indexOf(state.getIn(['compose', 'privacy'])) < privacyOrder.indexOf(replyPrivacy),
     sideArm: sideArmPrivacy,
+    sideArmWarning: sideArmPrivacy && sideArmRestrictedPrivacy && privacyOrder.indexOf(sideArmPrivacy) < privacyOrder.indexOf(sideArmRestrictedPrivacy),
     sensitive: state.getIn(['compose', 'sensitive']),
     showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
     spoiler: spoilersAlwaysOn || state.getIn(['compose', 'spoiler']),
@@ -82,6 +85,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     dispatch(submitCompose(routerHistory));
   },
 
+  onClearAll() {
+    dispatch(resetCompose());
+  },
+
   onClearSuggestions() {
     dispatch(clearComposeSuggestions());
   },
diff --git a/app/javascript/flavours/glitch/features/getting_started/index.js b/app/javascript/flavours/glitch/features/getting_started/index.js
index b4549fdf8..43d535ac5 100644
--- a/app/javascript/flavours/glitch/features/getting_started/index.js
+++ b/app/javascript/flavours/glitch/features/getting_started/index.js
@@ -46,7 +46,10 @@ const makeMapStateToProps = () => {
       return lists;
     }
 
-    return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
+    return lists.toList().filter(item => !!item).sort((a, b) => {
+      const r = (b.get('reblogs') ? 1 : 0) - (a.get('reblogs') ? 1 : 0);
+      return r === 0 ? a.get('title').localeCompare(b.get('title')) : r;
+    });
   });
 
   const mapStateToProps = state => ({
diff --git a/app/javascript/flavours/glitch/features/list_adder/index.js b/app/javascript/flavours/glitch/features/list_adder/index.js
index cb8a15e8c..b7f3d1ef7 100644
--- a/app/javascript/flavours/glitch/features/list_adder/index.js
+++ b/app/javascript/flavours/glitch/features/list_adder/index.js
@@ -16,7 +16,10 @@ const getOrderedLists = createSelector([state => state.get('lists')], lists => {
     return lists;
   }
 
-  return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
+  return lists.toList().filter(item => !!item).sort((a, b) => {
+    const r = (b.get('reblogs') ? 1 : 0) - (a.get('reblogs') ? 1 : 0);
+    return r === 0 ? a.get('title').localeCompare(b.get('title')) : r;
+  });
 });
 
 const mapStateToProps = state => ({
diff --git a/app/javascript/flavours/glitch/features/lists/index.js b/app/javascript/flavours/glitch/features/lists/index.js
index e384f301b..3863b8e25 100644
--- a/app/javascript/flavours/glitch/features/lists/index.js
+++ b/app/javascript/flavours/glitch/features/lists/index.js
@@ -24,7 +24,10 @@ const getOrderedLists = createSelector([state => state.get('lists')], lists => {
     return lists;
   }
 
-  return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
+  return lists.toList().filter(item => !!item).sort((a, b) => {
+    const r = (b.get('reblogs') ? 1 : 0) - (a.get('reblogs') ? 1 : 0);
+    return r === 0 ? a.get('title').localeCompare(b.get('title')) : r;
+  });
 });
 
 const mapStateToProps = state => ({
diff --git a/app/javascript/flavours/glitch/features/notifications/components/follow.js b/app/javascript/flavours/glitch/features/notifications/components/follow.js
index 0d3162fc9..7b47f411b 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/follow.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/follow.js
@@ -84,11 +84,13 @@ export default class NotificationFollow extends ImmutablePureComponent {
               <Icon fixedWidth id='user-plus' />
             </div>
 
-            <FormattedMessage
-              id='notification.follow'
-              defaultMessage='{name} followed you'
-              values={{ name: link }}
-            />
+            <span title={notification.get('created_at')}>
+              <FormattedMessage
+                id='notification.follow'
+                defaultMessage='{name} followed you'
+                values={{ name: link }}
+              />
+            </span>
           </div>
 
           <AccountContainer hidden={hidden} id={account.get('id')} withNote={false} />
diff --git a/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js b/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js
index 73fc05dea..4cac53266 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js
@@ -20,6 +20,10 @@ class NotificationsPermissionBanner extends React.PureComponent {
     intl: PropTypes.object.isRequired,
   };
 
+  handleClickDisable = () => {
+    window.location = '/settings/preferences/notifications';
+  }
+
   handleClick = () => {
     this.props.dispatch(requestBrowserPermission());
   }
@@ -39,7 +43,8 @@ class NotificationsPermissionBanner extends React.PureComponent {
 
         <h2><FormattedMessage id='notifications_permission_banner.title' defaultMessage='Never miss a thing' /></h2>
         <p><FormattedMessage id='notifications_permission_banner.how_to_control' defaultMessage="To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled." values={{ icon: <Icon id='sliders' /> }} /></p>
-        <Button onClick={this.handleClick}><FormattedMessage id='notifications_permission_banner.enable' defaultMessage='Enable desktop notifications' /></Button>
+        <Button onClick={this.handleClick}><FormattedMessage id='notifications_permission_banner.enable' defaultMessage='Request' /></Button>
+        <Button onClick={this.handleClickDisable}><FormattedMessage id='notifications_permission_banner.disable' defaultMessage='Disable' /></Button>
       </div>
     );
   }
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 04d350bcb..d03a425eb 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';
 import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
 
 export default class DetailedStatus extends ImmutablePureComponent {
@@ -200,7 +200,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';
@@ -208,7 +208,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 = (
@@ -216,9 +216,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>
       );
@@ -228,37 +225,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} />
@@ -275,13 +278,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 b330adf3f..74d51f8f8 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' },
@@ -306,6 +308,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));
   }
@@ -591,6 +607,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/list_panel.js b/app/javascript/flavours/glitch/features/ui/components/list_panel.js
index 354e35027..f351e2a01 100644
--- a/app/javascript/flavours/glitch/features/ui/components/list_panel.js
+++ b/app/javascript/flavours/glitch/features/ui/components/list_panel.js
@@ -13,7 +13,10 @@ const getOrderedLists = createSelector([state => state.get('lists')], lists => {
     return lists;
   }
 
-  return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(4);
+  return lists.toList().filter(item => !!item).sort((a, b) => {
+    const r = (b.get('reblogs') ? 1 : 0) - (a.get('reblogs') ? 1 : 0);
+    return r === 0 ? a.get('title').localeCompare(b.get('title')) : r;
+  });
 });
 
 const mapStateToProps = state => ({
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 7d25db316..5970ceddb 100644
--- a/app/javascript/flavours/glitch/features/ui/components/mute_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/mute_modal.js
@@ -7,6 +7,7 @@ import Button from 'flavours/glitch/components/button';
 import { closeModal } from 'flavours/glitch/actions/modal';
 import { muteAccount } from 'flavours/glitch/actions/accounts';
 import { toggleHideNotifications, changeMuteDuration } from 'flavours/glitch/actions/mutes';
+import { toggleTimelinesOnly } from 'flavours/glitch/actions/mutes';
 
 const messages = defineMessages({
   minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
@@ -20,13 +21,14 @@ const mapStateToProps = state => {
     account: state.getIn(['mutes', 'new', 'account']),
     notifications: state.getIn(['mutes', 'new', 'notifications']),
     muteDuration: state.getIn(['mutes', 'new', 'duration']),
+    timelinesOnly: state.getIn(['mutes', 'new', 'timelines_only']),
   };
 };
 
 const mapDispatchToProps = dispatch => {
   return {
-    onConfirm(account, notifications, muteDuration) {
-      dispatch(muteAccount(account.get('id'), notifications, muteDuration));
+    onConfirm(account, notifications, timelinesOnly, muteDuration) {
+      dispatch(muteAccount(account.get('id'), notifications, timelinesOnly, muteDuration));
     },
 
     onClose() {
@@ -40,6 +42,10 @@ const mapDispatchToProps = dispatch => {
     onChangeMuteDuration(e) {
       dispatch(changeMuteDuration(e.target.value));
     },
+
+    onToggleTimelinesOnly() {
+      dispatch(toggleTimelinesOnly());
+    },
   };
 };
 
@@ -50,9 +56,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,
     muteDuration: PropTypes.number.isRequired,
     onChangeMuteDuration: PropTypes.func.isRequired,
@@ -64,7 +72,7 @@ class MuteModal extends React.PureComponent {
 
   handleClick = () => {
     this.props.onClose();
-    this.props.onConfirm(this.props.account, this.props.notifications, this.props.muteDuration);
+    this.props.onConfirm(this.props.account, this.props.notifications, this.props.timelinesOnly, this.props.muteDuration);
   }
 
   handleCancel = () => {
@@ -83,8 +91,12 @@ class MuteModal extends React.PureComponent {
     this.props.onChangeMuteDuration(e);
   }
 
+  toggleTimelinesOnly = () => {
+    this.props.onToggleTimelinesOnly();
+  }
+
   render () {
-    const { account, notifications, muteDuration, intl } = this.props;
+    const { account, notifications, timelinesOnly, muteDuration, intl } = this.props;
 
     return (
       <div className='modal-root__modal mute-modal'>
@@ -109,6 +121,13 @@ class MuteModal extends React.PureComponent {
             </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>
             <span><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </span>
 
             {/* eslint-disable-next-line jsx-a11y/no-onchange */}
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 61a34fd2b..fa18f84b3 100644
--- a/app/javascript/flavours/glitch/features/ui/index.js
+++ b/app/javascript/flavours/glitch/features/ui/index.js
@@ -215,8 +215,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 ea37ae4aa..53360b8f0 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',
@@ -53,7 +53,7 @@ const initialState = ImmutableMap({
     pop_in_position  : 'right',
   }),
   notifications : ImmutableMap({
-    favicon_badge : false,
+    favicon_badge : true,
     tab_badge     : true,
     show_unread   : true,
   }),
diff --git a/app/javascript/flavours/glitch/reducers/mutes.js b/app/javascript/flavours/glitch/reducers/mutes.js
index d346d9a78..f116e106a 100644
--- a/app/javascript/flavours/glitch/reducers/mutes.js
+++ b/app/javascript/flavours/glitch/reducers/mutes.js
@@ -4,6 +4,7 @@ import {
   MUTES_INIT_MODAL,
   MUTES_TOGGLE_HIDE_NOTIFICATIONS,
   MUTES_CHANGE_DURATION,
+  MUTES_TOGGLE_TIMELINES_ONLY,
 } from 'flavours/glitch/actions/mutes';
 
 const initialState = Immutable.Map({
@@ -11,6 +12,7 @@ const initialState = Immutable.Map({
     account: null,
     notifications: true,
     duration: 0,
+    timelinesOnly: false,
   }),
 });
 
@@ -20,11 +22,14 @@ 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_CHANGE_DURATION:
     return state.setIn(['new', 'duration'], Number(action.duration));
+  case MUTES_TOGGLE_TIMELINES_ONLY:
+    return state.updateIn(['new', 'timelines_only'], (old) => !old);
   default:
     return state;
   }
diff --git a/app/javascript/flavours/glitch/reducers/settings.js b/app/javascript/flavours/glitch/reducers/settings.js
index bf0545c48..64d13e18c 100644
--- a/app/javascript/flavours/glitch/reducers/settings.js
+++ b/app/javascript/flavours/glitch/reducers/settings.js
@@ -91,8 +91,9 @@ const initialState = ImmutableMap({
 
 const defaultColumns = fromJS([
   { id: 'COMPOSE', uuid: uuid(), params: {} },
-  { id: 'HOME', uuid: uuid(), params: {} },
   { id: 'NOTIFICATIONS', uuid: uuid(), params: {} },
+  { id: 'HOME', uuid: uuid(), params: {} },
+  { id: 'COMMUNITY', uuid: uuid(), params: {} },
 ]);
 
 const hydrate = (state, settings) => state.mergeDeep(settings).update('columns', (val = defaultColumns) => val);
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/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss
index 2550f50f4..d90327211 100644
--- a/app/javascript/flavours/glitch/styles/components/columns.scss
+++ b/app/javascript/flavours/glitch/styles/components/columns.scss
@@ -696,12 +696,9 @@
 }
 
 .notifications-permission-banner {
-  padding: 30px;
+  padding: 14px;
   border-bottom: 1px solid lighten($ui-base-color, 8%);
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
+  text-align: center;
   position: relative;
 
   &__close {
@@ -711,7 +708,7 @@
   }
 
   h2 {
-    font-size: 16px;
+    font-size: 14px;
     font-weight: 500;
     margin-bottom: 15px;
     text-align: center;
@@ -722,4 +719,9 @@
     margin-bottom: 15px;
     text-align: center;
   }
+
+  .button {
+    margin-left: 7px;
+    margin-right: 7px;
+  }
 }
diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss
index 460f75c1f..7db2dd2aa 100644
--- a/app/javascript/flavours/glitch/styles/components/composer.scss
+++ b/app/javascript/flavours/glitch/styles/components/composer.scss
@@ -654,6 +654,19 @@
     text-align: center;
   }
 
+  & > .privacy_warning {
+    background-color: $error-value-color;
+
+    &:hover {
+      background-color: lighten($error-value-color, 5%);
+    }
+
+    &:active,
+    &:focus {
+      background-color: darken($error-value-color, 5%);
+    }
+  }
+
   &.over {
     & > .count { color: $warning-red }
   }
diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss
index bc0965864..d9d962e34 100644
--- a/app/javascript/flavours/glitch/styles/components/modal.scss
+++ b/app/javascript/flavours/glitch/styles/components/modal.scss
@@ -876,7 +876,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 6e242281b..f4e971996 100644
--- a/app/javascript/flavours/glitch/styles/variables.scss
+++ b/app/javascript/flavours/glitch/styles/variables.scss
@@ -51,11 +51,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 b397844a2..d2f4450fc 100644
--- a/app/javascript/flavours/glitch/styles/widgets.scss
+++ b/app/javascript/flavours/glitch/styles/widgets.scss
@@ -576,7 +576,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/flavours/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js
index 911468e6f..05868970c 100644
--- a/app/javascript/flavours/glitch/util/initial_state.js
+++ b/app/javascript/flavours/glitch/util/initial_state.js
@@ -11,6 +11,8 @@ const initialState = element && function () {
 
 const getMeta = (prop) => initialState && initialState.meta && initialState.meta[prop];
 
+export const webPushEnabled = getMeta('web_push') === true;
+
 export const reduceMotion = getMeta('reduce_motion');
 export const autoPlayGif = getMeta('auto_play_gif');
 export const displaySensitiveMedia = getMeta('display_sensitive_media');