about summary refs log tree commit diff
path: root/app/javascript
diff options
context:
space:
mode:
authorkibigo! <marrus-sh@users.noreply.github.com>2017-11-20 22:13:37 -0800
committerkibigo! <marrus-sh@users.noreply.github.com>2017-11-20 22:13:37 -0800
commitbdbbd06dad298dc3e1a5f568f4a3ff3635b48f22 (patch)
tree1a475460389053b41f293d27f7c915f0d545d4ad /app/javascript
parent321fa41930f8a11356939a1684ff153f2f46443b (diff)
Finalized theme loading and stuff
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/core/about.js1
-rw-r--r--app/javascript/core/embed.js2
-rw-r--r--app/javascript/core/home.js1
-rw-r--r--app/javascript/core/public.js24
-rw-r--r--app/javascript/core/settings.js80
-rw-r--r--app/javascript/core/share.js1
-rw-r--r--app/javascript/core/theme.yml14
-rw-r--r--app/javascript/locales/index.js9
-rw-r--r--app/javascript/mastodon/locales/index.js10
-rw-r--r--app/javascript/packs/about.js22
-rw-r--r--app/javascript/packs/application.js10
-rw-r--r--app/javascript/packs/common.js3
-rw-r--r--app/javascript/packs/public.js75
-rw-r--r--app/javascript/packs/share.js22
-rw-r--r--app/javascript/styles/common.scss5
-rw-r--r--app/javascript/styles/win95.scss113
-rw-r--r--app/javascript/themes/glitch/containers/mastodon.js2
-rw-r--r--app/javascript/themes/glitch/packs/common.js4
-rw-r--r--app/javascript/themes/glitch/packs/home.js4
-rw-r--r--app/javascript/themes/glitch/packs/public.js83
-rw-r--r--app/javascript/themes/glitch/theme.yml39
-rw-r--r--app/javascript/themes/vanilla/theme.yml35
-rw-r--r--app/javascript/themes/win95/index.js10
-rw-r--r--app/javascript/themes/win95/theme.yml23
24 files changed, 344 insertions, 248 deletions
diff --git a/app/javascript/core/about.js b/app/javascript/core/about.js
deleted file mode 100644
index 6ed0e4ad3..000000000
--- a/app/javascript/core/about.js
+++ /dev/null
@@ -1 +0,0 @@
-//  This file will be loaded on about pages, regardless of theme.
diff --git a/app/javascript/core/embed.js b/app/javascript/core/embed.js
index 8167706a3..6146e6592 100644
--- a/app/javascript/core/embed.js
+++ b/app/javascript/core/embed.js
@@ -13,7 +13,7 @@ window.addEventListener('message', e => {
       id: data.id,
       height: document.getElementsByTagName('html')[0].scrollHeight,
     }, '*');
-  });
+  };
 
   if (['interactive', 'complete'].includes(document.readyState)) {
     setEmbedHeight();
diff --git a/app/javascript/core/home.js b/app/javascript/core/home.js
deleted file mode 100644
index 3c2e01590..000000000
--- a/app/javascript/core/home.js
+++ /dev/null
@@ -1 +0,0 @@
-//  This file will be loaded on home pages, regardless of theme.
diff --git a/app/javascript/core/public.js b/app/javascript/core/public.js
index 1a36b7a5f..47c34a259 100644
--- a/app/javascript/core/public.js
+++ b/app/javascript/core/public.js
@@ -1 +1,25 @@
 //  This file will be loaded on public pages, regardless of theme.
+
+const { delegate } = require('rails-ujs');
+
+delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
+  if (button !== 0) {
+    return true;
+  }
+  window.location.href = target.href;
+  return false;
+});
+
+delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => {
+  const contentEl = target.parentNode.parentNode.querySelector('.e-content');
+
+  if (contentEl.style.display === 'block') {
+    contentEl.style.display = 'none';
+    target.parentNode.style.marginBottom = 0;
+  } else {
+    contentEl.style.display = 'block';
+    target.parentNode.style.marginBottom = null;
+  }
+
+  return false;
+});
diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js
index 91332ed5a..7fb1d8e77 100644
--- a/app/javascript/core/settings.js
+++ b/app/javascript/core/settings.js
@@ -1,65 +1,37 @@
 //  This file will be loaded on settings pages, regardless of theme.
 
-function main() {
-  const { length } = require('stringz');
-  const { delegate } = require('rails-ujs');
+const { length } = require('stringz');
+const { delegate } = require('rails-ujs');
 
-  delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
-    if (button !== 0) {
-      return true;
-    }
-    window.location.href = target.href;
-    return false;
-  });
+delegate(document, '.account_display_name', 'input', ({ target }) => {
+  const nameCounter = document.querySelector('.name-counter');
 
-  delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => {
-    const contentEl = target.parentNode.parentNode.querySelector('.e-content');
-
-    if (contentEl.style.display === 'block') {
-      contentEl.style.display = 'none';
-      target.parentNode.style.marginBottom = 0;
-    } else {
-      contentEl.style.display = 'block';
-      target.parentNode.style.marginBottom = null;
-    }
-
-    return false;
-  });
-
-  delegate(document, '.account_display_name', 'input', ({ target }) => {
-    const nameCounter = document.querySelector('.name-counter');
-
-    if (nameCounter) {
-      nameCounter.textContent = 30 - length(target.value);
-    }
-  });
-
-  delegate(document, '.account_note', 'input', ({ target }) => {
-    const noteCounter = document.querySelector('.note-counter');
+  if (nameCounter) {
+    nameCounter.textContent = 30 - length(target.value);
+  }
+});
 
-    if (noteCounter) {
-      const noteWithoutMetadata = processBio(target.value).text;
-      noteCounter.textContent = 500 - length(noteWithoutMetadata);
-    }
-  });
+delegate(document, '.account_note', 'input', ({ target }) => {
+  const noteCounter = document.querySelector('.note-counter');
 
-  delegate(document, '#account_avatar', 'change', ({ target }) => {
-    const avatar = document.querySelector('.card.compact .avatar img');
-    const [file] = target.files || [];
-    const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
+  if (noteCounter) {
+    const noteWithoutMetadata = processBio(target.value).text;
+    noteCounter.textContent = 500 - length(noteWithoutMetadata);
+  }
+});
 
-    avatar.src = url;
-  });
+delegate(document, '#account_avatar', 'change', ({ target }) => {
+  const avatar = document.querySelector('.card.compact .avatar img');
+  const [file] = target.files || [];
+  const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
 
-  delegate(document, '#account_header', 'change', ({ target }) => {
-    const header = document.querySelector('.card.compact');
-    const [file] = target.files || [];
-    const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc;
+  avatar.src = url;
+});
 
-    header.style.backgroundImage = `url(${url})`;
-  });
-}
+delegate(document, '#account_header', 'change', ({ target }) => {
+  const header = document.querySelector('.card.compact');
+  const [file] = target.files || [];
+  const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc;
 
-loadPolyfills().then(main).catch(error => {
-  console.error(error);
+  header.style.backgroundImage = `url(${url})`;
 });
diff --git a/app/javascript/core/share.js b/app/javascript/core/share.js
deleted file mode 100644
index 98a413632..000000000
--- a/app/javascript/core/share.js
+++ /dev/null
@@ -1 +0,0 @@
-//  This file will be loaded on share pages, regardless of theme.
diff --git a/app/javascript/core/theme.yml b/app/javascript/core/theme.yml
new file mode 100644
index 000000000..17e8e66b3
--- /dev/null
+++ b/app/javascript/core/theme.yml
@@ -0,0 +1,14 @@
+#  These packs will be loaded on every appropriate page, regardless of
+#  theme.
+pack:
+  about:
+  admin: admin.js
+  auth:
+  common: common.js
+  embed: embed.js
+  error:
+  home:
+  modal:
+  public: public.js
+  settings: settings.js
+  share:
diff --git a/app/javascript/locales/index.js b/app/javascript/locales/index.js
new file mode 100644
index 000000000..421cb7fab
--- /dev/null
+++ b/app/javascript/locales/index.js
@@ -0,0 +1,9 @@
+let theLocale;
+
+export function setLocale(locale) {
+  theLocale = locale;
+}
+
+export function getLocale() {
+  return theLocale;
+}
diff --git a/app/javascript/mastodon/locales/index.js b/app/javascript/mastodon/locales/index.js
index 421cb7fab..7e7297561 100644
--- a/app/javascript/mastodon/locales/index.js
+++ b/app/javascript/mastodon/locales/index.js
@@ -1,9 +1 @@
-let theLocale;
-
-export function setLocale(locale) {
-  theLocale = locale;
-}
-
-export function getLocale() {
-  return theLocale;
-}
+export * from 'locales';
diff --git a/app/javascript/packs/about.js b/app/javascript/packs/about.js
new file mode 100644
index 000000000..63e12da42
--- /dev/null
+++ b/app/javascript/packs/about.js
@@ -0,0 +1,22 @@
+import loadPolyfills from '../mastodon/load_polyfills';
+
+function loaded() {
+  const TimelineContainer = require('../mastodon/containers/timeline_container').default;
+  const React             = require('react');
+  const ReactDOM          = require('react-dom');
+  const mountNode         = document.getElementById('mastodon-timeline');
+
+  if (mountNode !== null) {
+    const props = JSON.parse(mountNode.getAttribute('data-props'));
+    ReactDOM.render(<TimelineContainer {...props} />, mountNode);
+  }
+}
+
+function main() {
+  const ready = require('../mastodon/ready').default;
+  ready(loaded);
+}
+
+loadPolyfills().then(main).catch(error => {
+  console.error(error);
+});
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
index ee5bf244c..116632dea 100644
--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -1,15 +1,5 @@
-//  THIS IS THE `vanilla` THEME PACK FILE!!
-//  IT'S HERE FOR UPSTREAM COMPATIBILITY!!
-//  THE `glitch` PACK FILE IS IN `themes/glitch/index.js`!!
-
 import loadPolyfills from '../mastodon/load_polyfills';
 
-// import default stylesheet with variables
-import 'font-awesome/css/font-awesome.css';
-import '../styles/application.scss';
-
-require.context('../images/', true);
-
 loadPolyfills().then(() => {
   require('../mastodon/main').default();
 }).catch(e => {
diff --git a/app/javascript/packs/common.js b/app/javascript/packs/common.js
new file mode 100644
index 000000000..f3156c1c6
--- /dev/null
+++ b/app/javascript/packs/common.js
@@ -0,0 +1,3 @@
+import 'font-awesome/css/font-awesome.css';
+import 'styles/application.scss'
+require.context('../images/', true);
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
new file mode 100644
index 000000000..3472af6c1
--- /dev/null
+++ b/app/javascript/packs/public.js
@@ -0,0 +1,75 @@
+import loadPolyfills from '../mastodon/load_polyfills';
+import ready from '../mastodon/ready';
+
+function main() {
+  const IntlRelativeFormat = require('intl-relativeformat').default;
+  const emojify = require('../mastodon/features/emoji/emoji').default;
+  const { getLocale } = require('../mastodon/locales');
+  const { localeData } = getLocale();
+  const VideoContainer = require('../mastodon/containers/video_container').default;
+  const MediaGalleryContainer = require('../mastodon/containers/media_gallery_container').default;
+  const CardContainer = require('../mastodon/containers/card_container').default;
+  const React = require('react');
+  const ReactDOM = require('react-dom');
+
+  localeData.forEach(IntlRelativeFormat.__addLocaleData);
+
+  ready(() => {
+    const locale = document.documentElement.lang;
+
+    const dateTimeFormat = new Intl.DateTimeFormat(locale, {
+      year: 'numeric',
+      month: 'long',
+      day: 'numeric',
+      hour: 'numeric',
+      minute: 'numeric',
+    });
+
+    const relativeFormat = new IntlRelativeFormat(locale);
+
+    [].forEach.call(document.querySelectorAll('.emojify'), (content) => {
+      content.innerHTML = emojify(content.innerHTML);
+    });
+
+    [].forEach.call(document.querySelectorAll('time.formatted'), (content) => {
+      const datetime = new Date(content.getAttribute('datetime'));
+      const formattedDate = dateTimeFormat.format(datetime);
+
+      content.title = formattedDate;
+      content.textContent = formattedDate;
+    });
+
+    [].forEach.call(document.querySelectorAll('time.time-ago'), (content) => {
+      const datetime = new Date(content.getAttribute('datetime'));
+
+      content.title = dateTimeFormat.format(datetime);
+      content.textContent = relativeFormat.format(datetime);
+    });
+
+    [].forEach.call(document.querySelectorAll('.logo-button'), (content) => {
+      content.addEventListener('click', (e) => {
+        e.preventDefault();
+        window.open(e.target.href, 'mastodon-intent', 'width=400,height=400,resizable=no,menubar=no,status=no,scrollbars=yes');
+      });
+    });
+
+    [].forEach.call(document.querySelectorAll('[data-component="Video"]'), (content) => {
+      const props = JSON.parse(content.getAttribute('data-props'));
+      ReactDOM.render(<VideoContainer locale={locale} {...props} />, content);
+    });
+
+    [].forEach.call(document.querySelectorAll('[data-component="MediaGallery"]'), (content) => {
+      const props = JSON.parse(content.getAttribute('data-props'));
+      ReactDOM.render(<MediaGalleryContainer locale={locale} {...props} />, content);
+    });
+
+    [].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => {
+      const props = JSON.parse(content.getAttribute('data-props'));
+      ReactDOM.render(<CardContainer locale={locale} {...props} />, content);
+    });
+  });
+}
+
+loadPolyfills().then(main).catch(error => {
+  console.error(error);
+});
diff --git a/app/javascript/packs/share.js b/app/javascript/packs/share.js
new file mode 100644
index 000000000..e9580f648
--- /dev/null
+++ b/app/javascript/packs/share.js
@@ -0,0 +1,22 @@
+import loadPolyfills from '../mastodon/load_polyfills';
+
+function loaded() {
+  const ComposeContainer = require('../mastodon/containers/compose_container').default;
+  const React = require('react');
+  const ReactDOM = require('react-dom');
+  const mountNode = document.getElementById('mastodon-compose');
+
+  if (mountNode !== null) {
+    const props = JSON.parse(mountNode.getAttribute('data-props'));
+    ReactDOM.render(<ComposeContainer {...props} />, mountNode);
+  }
+}
+
+function main() {
+  const ready = require('../mastodon/ready').default;
+  ready(loaded);
+}
+
+loadPolyfills().then(main).catch(error => {
+  console.error(error);
+});
diff --git a/app/javascript/styles/common.scss b/app/javascript/styles/common.scss
deleted file mode 100644
index c1772e7ae..000000000
--- a/app/javascript/styles/common.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-// This makes our fonts available everywhere.
-
-@import 'fonts/roboto';
-@import 'fonts/roboto-mono';
-@import 'fonts/montserrat';
diff --git a/app/javascript/styles/win95.scss b/app/javascript/styles/win95.scss
index 885837b53..6c89fc5bf 100644
--- a/app/javascript/styles/win95.scss
+++ b/app/javascript/styles/win95.scss
@@ -1,3 +1,8 @@
+//  win95 theme from cybrespace.
+
+//  Modified to inherit glitch styles (themes/glitch/styles/index.scss)
+//  instead of vanilla ones (./application.scss)
+
 $win95-bg: #bfbfbf;
 $win95-dark-grey: #404040;
 $win95-mid-grey: #808080;
@@ -17,7 +22,7 @@ $ui-highlight-color: $win95-window-header;
 }
 
 @mixin win95-outset() {
-  box-shadow: inset -1px -1px 0px #000000, 
+  box-shadow: inset -1px -1px 0px #000000,
               inset 1px 1px 0px #ffffff,
               inset -2px -2px 0px #808080,
               inset 2px 2px 0px #dfdfdf;
@@ -41,7 +46,7 @@ $ui-highlight-color: $win95-window-header;
 }
 
 @mixin win95-inset() {
-  box-shadow: inset 1px 1px 0px #000000, 
+  box-shadow: inset 1px 1px 0px #000000,
               inset -1px -1px 0px #ffffff,
               inset 2px 2px 0px #808080,
               inset -2px -2px 0px #dfdfdf;
@@ -51,7 +56,7 @@ $ui-highlight-color: $win95-window-header;
 
 
 @mixin win95-tab() {
-  box-shadow: inset -1px 0px 0px #000000, 
+  box-shadow: inset -1px 0px 0px #000000,
               inset 1px 0px 0px #ffffff,
               inset 0px 1px 0px #ffffff,
               inset 0px 2px 0px #dfdfdf,
@@ -71,7 +76,7 @@ $ui-highlight-color: $win95-window-header;
   src: url('../fonts/premillenium/MSSansSerif.ttf') format('truetype');
 }
 
-@import 'application';
+@import '../themes/glitch/styles/index';  //  Imports glitch themes
 
 /* borrowed from cybrespace style: wider columns and full column width images */
 
@@ -174,7 +179,7 @@ body.admin {
   font-size:0px;
   color:$win95-bg;
 
-  background-image: url("../images/start.png"); 
+  background-image: url("../images/start.png");
   background-repeat:no-repeat;
   background-position:8%;
   background-clip:padding-box;
@@ -336,7 +341,7 @@ body.admin {
   border-radius:0px;
   background-color:white;
   @include win95-border-inset();
-  
+
   width:12px;
   height:12px;
 }
@@ -515,9 +520,9 @@ body.admin {
   color:black;
   font-weight:bold;
 }
-.account__avatar, 
-.account__avatar-overlay-base, 
-.account__header__avatar, 
+.account__avatar,
+.account__avatar-overlay-base,
+.account__header__avatar,
 .account__avatar-overlay-overlay {
   @include win95-border-slight-inset();
   clip-path:none;
@@ -627,7 +632,7 @@ body.admin {
 }
 
 .status-card__description {
- color:black; 
+ color:black;
 }
 
 .account__display-name strong, .status__display-name strong {
@@ -710,8 +715,8 @@ body.admin {
   width:40px;
   font-size:0px;
   color:$win95-bg;
-  
-  background-image: url("../images/start.png"); 
+
+  background-image: url("../images/start.png");
   background-repeat:no-repeat;
   background-position:8%;
   background-clip:padding-box;
@@ -723,7 +728,7 @@ body.admin {
 }
 
 .drawer__header a:first-child:hover {
-  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAIAAACpTQvdAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAF3pUWHRBdXRob3IAAAiZS84oys9LzAEAC5oC6A7BY/IAAACWSURBVCiRhVJJDsQgDEuqOfRZ7a1P5gbP4uaJaEjTADMWQhHYjlk4p0wLnNdptdF4KvBUDyGzVwc2xO+uKtH+1o0ytEEmqFpuxlvFCGCxKbNIT56QCi2MzaA/2Mz+mERSOeqzJG2RUxkjdTabgPtFoZ1bZxcKvgPcLZVufAyR9Ni8v5dWDzfFx0giC1RvZFv6l35QQ/Mvv39XXgGzQpoAAAAASUVORK5CYII="); 
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAIAAACpTQvdAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAF3pUWHRBdXRob3IAAAiZS84oys9LzAEAC5oC6A7BY/IAAACWSURBVCiRhVJJDsQgDEuqOfRZ7a1P5gbP4uaJaEjTADMWQhHYjlk4p0wLnNdptdF4KvBUDyGzVwc2xO+uKtH+1o0ytEEmqFpuxlvFCGCxKbNIT56QCi2MzaA/2Mz+mERSOeqzJG2RUxkjdTabgPtFoZ1bZxcKvgPcLZVufAyR9Ni8v5dWDzfFx0giC1RvZFv6l35QQ/Mvv39XXgGzQpoAAAAASUVORK5CYII=");
   background-repeat:no-repeat;
   background-position:8%;
   background-clip:padding-box;
@@ -732,7 +737,7 @@ body.admin {
 }
 
 .drawer__tab:first-child {
-  
+
 }
 
 .search {
@@ -844,7 +849,7 @@ body.admin {
   padding:4px 8px;
 }
 
-.privacy-dropdown.active 
+.privacy-dropdown.active
 .privacy-dropdown__value {
   background: $win95-bg;
   box-shadow:unset;
@@ -935,7 +940,7 @@ body.admin {
   background-color:$win95-bg;
   border:1px solid black;
   box-sizing:content-box;
-  
+
 }
 
 .emoji-dialog .emoji-search {
@@ -1010,8 +1015,8 @@ body.admin {
   width:60px;
   font-size:0px;
   color:$win95-bg;
-  
-  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAIAAACpTQvdAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAF3pUWHRBdXRob3IAAAiZS84oys9LzAEAC5oC6A7BY/IAAACWSURBVCiRhVJJDsQgDEuqOfRZ7a1P5gbP4uaJaEjTADMWQhHYjlk4p0wLnNdptdF4KvBUDyGzVwc2xO+uKtH+1o0ytEEmqFpuxlvFCGCxKbNIT56QCi2MzaA/2Mz+mERSOeqzJG2RUxkjdTabgPtFoZ1bZxcKvgPcLZVufAyR9Ni8v5dWDzfFx0giC1RvZFv6l35QQ/Mvv39XXgGzQpoAAAAASUVORK5CYII="); 
+
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAIAAACpTQvdAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAF3pUWHRBdXRob3IAAAiZS84oys9LzAEAC5oC6A7BY/IAAACWSURBVCiRhVJJDsQgDEuqOfRZ7a1P5gbP4uaJaEjTADMWQhHYjlk4p0wLnNdptdF4KvBUDyGzVwc2xO+uKtH+1o0ytEEmqFpuxlvFCGCxKbNIT56QCi2MzaA/2Mz+mERSOeqzJG2RUxkjdTabgPtFoZ1bZxcKvgPcLZVufAyR9Ni8v5dWDzfFx0giC1RvZFv6l35QQ/Mvv39XXgGzQpoAAAAASUVORK5CYII=");
   background-repeat:no-repeat;
   background-position:8%;
   background-clip:padding-box;
@@ -1049,40 +1054,40 @@ body.admin {
   }
 }
 
-.column-link[href="/web/timelines/public"] { 
-  background-image: url("../images/icon_public.png"); 
+.column-link[href="/web/timelines/public"] {
+  background-image: url("../images/icon_public.png");
   &:hover { background-image: url("../images/icon_public.png"); }
 }
-.column-link[href="/web/timelines/public/local"] { 
-  background-image: url("../images/icon_local.png"); 
+.column-link[href="/web/timelines/public/local"] {
+  background-image: url("../images/icon_local.png");
   &:hover { background-image: url("../images/icon_local.png"); }
 }
-.column-link[href="/web/pinned"] { 
-  background-image: url("../images/icon_pin.png"); 
+.column-link[href="/web/pinned"] {
+  background-image: url("../images/icon_pin.png");
   &:hover { background-image: url("../images/icon_pin.png"); }
 }
-.column-link[href="/web/favourites"] { 
-  background-image: url("../images/icon_likes.png"); 
+.column-link[href="/web/favourites"] {
+  background-image: url("../images/icon_likes.png");
   &:hover { background-image: url("../images/icon_likes.png"); }
 }
-.column-link[href="/web/blocks"] { 
-  background-image: url("../images/icon_blocks.png"); 
+.column-link[href="/web/blocks"] {
+  background-image: url("../images/icon_blocks.png");
   &:hover { background-image: url("../images/icon_blocks.png"); }
 }
-.column-link[href="/web/mutes"] { 
-  background-image: url("../images/icon_mutes.png"); 
+.column-link[href="/web/mutes"] {
+  background-image: url("../images/icon_mutes.png");
   &:hover { background-image: url("../images/icon_mutes.png"); }
 }
-.column-link[href="/settings/preferences"] { 
-  background-image: url("../images/icon_settings.png"); 
+.column-link[href="/settings/preferences"] {
+  background-image: url("../images/icon_settings.png");
   &:hover { background-image: url("../images/icon_settings.png"); }
 }
-.column-link[href="/about/more"] { 
-  background-image: url("../images/icon_about.png"); 
+.column-link[href="/about/more"] {
+  background-image: url("../images/icon_about.png");
   &:hover { background-image: url("../images/icon_about.png"); }
 }
-.column-link[href="/auth/sign_out"] { 
-  background-image: url("../images/icon_logout.png"); 
+.column-link[href="/auth/sign_out"] {
+  background-image: url("../images/icon_logout.png");
   &:hover { background-image: url("../images/icon_logout.png"); }
 }
 
@@ -1098,7 +1103,7 @@ body.admin {
   line-height:30px;
   padding-left:20px;
   padding-right:40px;
-  
+
   left:0px;
   bottom:-30px;
   display:block;
@@ -1106,9 +1111,9 @@ body.admin {
   background-color:#7f7f7f;
   width:200%;
   height:30px;
-  
+
   -ms-transform: rotate(-90deg);
-  
+
   -webkit-transform: rotate(-90deg);
   transform: rotate(-90deg);
   transform-origin:top left;
@@ -1189,7 +1194,7 @@ body.admin {
   left:unset;
 }
 
-.dropdown > .icon-button, .detailed-status__button > .icon-button, 
+.dropdown > .icon-button, .detailed-status__button > .icon-button,
 .status__action-bar > .icon-button, .star-icon i {
     /* i don't know what's going on with the inline
        styles someone should look at the react code */
@@ -1239,8 +1244,8 @@ body.admin {
   background:$win95-bg;
 }
 
-.actions-modal::before, 
-.boost-modal::before, 
+.actions-modal::before,
+.boost-modal::before,
 .confirmation-modal::before,
 .report-modal::before {
   content: "Confirmation";
@@ -1278,8 +1283,8 @@ body.admin {
   .confirmation-modal__cancel-button {
     color:black;
 
-    &:active, 
-    &:focus, 
+    &:active,
+    &:focus,
     &:hover {
       color:black;
     }
@@ -1566,10 +1571,10 @@ a.table-action-link:hover,
   background-color:white;
 }
 
-.simple_form input[type=text], 
-.simple_form input[type=number], 
-.simple_form input[type=email], 
-.simple_form input[type=password], 
+.simple_form input[type=text],
+.simple_form input[type=number],
+.simple_form input[type=email],
+.simple_form input[type=password],
 .simple_form textarea {
   color:black;
   background-color:white;
@@ -1580,8 +1585,8 @@ a.table-action-link:hover,
   }
 }
 
-.simple_form button, 
-.simple_form .button, 
+.simple_form button,
+.simple_form .button,
 .simple_form .block-button
 {
   background: $win95-bg;
@@ -1608,8 +1613,8 @@ a.table-action-link:hover,
   }
 }
 
-.simple_form button.negative, 
-.simple_form .button.negative, 
+.simple_form button.negative,
+.simple_form .button.negative,
 .simple_form .block-button.negative
 {
   background: $win95-bg;
@@ -1631,8 +1636,8 @@ a.table-action-link:hover,
   border-right-color:#f5f5f5;
   width:12px;
   height:12px;
-  display:inline-block; 
-  vertical-align:middle; 
+  display:inline-block;
+  vertical-align:middle;
   margin-right:2px;
 }
 
diff --git a/app/javascript/themes/glitch/containers/mastodon.js b/app/javascript/themes/glitch/containers/mastodon.js
index 348470637..755b5564a 100644
--- a/app/javascript/themes/glitch/containers/mastodon.js
+++ b/app/javascript/themes/glitch/containers/mastodon.js
@@ -9,7 +9,7 @@ import UI from 'themes/glitch/features/ui';
 import { hydrateStore } from 'themes/glitch/actions/store';
 import { connectUserStream } from 'themes/glitch/actions/streaming';
 import { IntlProvider, addLocaleData } from 'react-intl';
-import { getLocale } from 'mastodon/locales';
+import { getLocale } from 'locales';
 import initialState from 'themes/glitch/util/initial_state';
 
 const { localeData, messages } = getLocale();
diff --git a/app/javascript/themes/glitch/packs/common.js b/app/javascript/themes/glitch/packs/common.js
index 3a62700bd..f4fa129e1 100644
--- a/app/javascript/themes/glitch/packs/common.js
+++ b/app/javascript/themes/glitch/packs/common.js
@@ -1,3 +1,3 @@
 import 'font-awesome/css/font-awesome.css';
-require.context('../../images/', true);
-import './styles/index.scss';
+require.context('images/', true);
+import 'themes/glitch/styles/index.scss';
diff --git a/app/javascript/themes/glitch/packs/home.js b/app/javascript/themes/glitch/packs/home.js
index dada28317..69dddf51c 100644
--- a/app/javascript/themes/glitch/packs/home.js
+++ b/app/javascript/themes/glitch/packs/home.js
@@ -1,7 +1,7 @@
-import loadPolyfills from './util/load_polyfills';
+import loadPolyfills from 'themes/glitch/util/load_polyfills';
 
 loadPolyfills().then(() => {
-  require('./util/main').default();
+  require('themes/glitch/util/main').default();
 }).catch(e => {
   console.error(e);
 });
diff --git a/app/javascript/themes/glitch/packs/public.js b/app/javascript/themes/glitch/packs/public.js
index 6adacad98..d9a1b9655 100644
--- a/app/javascript/themes/glitch/packs/public.js
+++ b/app/javascript/themes/glitch/packs/public.js
@@ -2,32 +2,14 @@ import loadPolyfills from 'themes/glitch/util/load_polyfills';
 import { processBio } from 'themes/glitch/util/bio_metadata';
 import ready from 'themes/glitch/util/ready';
 
-window.addEventListener('message', e => {
-  const data = e.data || {};
-
-  if (!window.parent || data.type !== 'setHeight') {
-    return;
-  }
-
-  ready(() => {
-    window.parent.postMessage({
-      type: 'setHeight',
-      id: data.id,
-      height: document.getElementsByTagName('html')[0].scrollHeight,
-    }, '*');
-  });
-});
-
 function main() {
-  const { length } = require('stringz');
   const IntlRelativeFormat = require('intl-relativeformat').default;
-  const { delegate } = require('rails-ujs');
-  const emojify = require('../themes/glitch/util/emoji').default;
-  const { getLocale } = require('mastodon/locales');
+  const emojify = require('themes/glitch/util/emoji').default;
+  const { getLocale } = require('locales');
   const { localeData } = getLocale();
-  const VideoContainer = require('../themes/glitch/containers/video_container').default;
-  const MediaGalleryContainer = require('../themes/glitch/containers/media_gallery_container').default;
-  const CardContainer = require('../themes/glitch/containers/card_container').default;
+  const VideoContainer = require('themes/glitch/containers/video_container').default;
+  const MediaGalleryContainer = require('themes/glitch/containers/media_gallery_container').default;
+  const CardContainer = require('themes/glitch/containers/card_container').default;
   const React = require('react');
   const ReactDOM = require('react-dom');
 
@@ -87,61 +69,6 @@ function main() {
       ReactDOM.render(<CardContainer locale={locale} {...props} />, content);
     });
   });
-
-  delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
-    if (button !== 0) {
-      return true;
-    }
-    window.location.href = target.href;
-    return false;
-  });
-
-  delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => {
-    const contentEl = target.parentNode.parentNode.querySelector('.e-content');
-
-    if (contentEl.style.display === 'block') {
-      contentEl.style.display = 'none';
-      target.parentNode.style.marginBottom = 0;
-    } else {
-      contentEl.style.display = 'block';
-      target.parentNode.style.marginBottom = null;
-    }
-
-    return false;
-  });
-
-  delegate(document, '.account_display_name', 'input', ({ target }) => {
-    const nameCounter = document.querySelector('.name-counter');
-
-    if (nameCounter) {
-      nameCounter.textContent = 30 - length(target.value);
-    }
-  });
-
-  delegate(document, '.account_note', 'input', ({ target }) => {
-    const noteCounter = document.querySelector('.note-counter');
-
-    if (noteCounter) {
-      const noteWithoutMetadata = processBio(target.value).text;
-      noteCounter.textContent = 500 - length(noteWithoutMetadata);
-    }
-  });
-
-  delegate(document, '#account_avatar', 'change', ({ target }) => {
-    const avatar = document.querySelector('.card.compact .avatar img');
-    const [file] = target.files || [];
-    const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
-
-    avatar.src = url;
-  });
-
-  delegate(document, '#account_header', 'change', ({ target }) => {
-    const header = document.querySelector('.card.compact');
-    const [file] = target.files || [];
-    const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc;
-
-    header.style.backgroundImage = `url(${url})`;
-  });
 }
 
 loadPolyfills().then(main).catch(error => {
diff --git a/app/javascript/themes/glitch/theme.yml b/app/javascript/themes/glitch/theme.yml
index cf3fa32c2..ac6f57546 100644
--- a/app/javascript/themes/glitch/theme.yml
+++ b/app/javascript/themes/glitch/theme.yml
@@ -1,25 +1,34 @@
 #  (REQUIRED) The location of the pack files.
 pack:
   about: packs/about.js
-  admin: null
-  common: packs/common.js
-  embed: null
-  home: packs/home.js
+  admin:
+  auth:
+  common:
+    filename: packs/common.js
+    stylesheet: true
+  embed: packs/public.js
+  error:
+  home:
+    filename: packs/home.js
+    preload:
+    - themes/glitch/async/getting_started
+    - themes/glitch/async/compose
+    - themes/glitch/async/home_timeline
+    - themes/glitch/async/notifications
+    stylesheet: true
+  modal:
   public: packs/public.js
-  settings: null
+  settings:
   share: packs/share.js
 
 #  (OPTIONAL) The directory which contains the pack files.
 #  Defaults to the theme directory (`app/javascript/themes/[theme]`),
 #  which should be sufficient for like 99% of use-cases lol.
-#    pack_directory: app/javascript/packs
 
-#  (OPTIONAL) Additional javascript resources to preload, for use with
-#  lazy-loaded components. It is **STRONGLY RECOMMENDED** that you
-#  derive these pathnames from `themes/[your-theme]` to ensure that
-#  they stay unique.
-preload:
-- themes/glitch/async/getting_started
-- themes/glitch/async/compose
-- themes/glitch/async/home_timeline
-- themes/glitch/async/notifications
+#      pack_directory: app/javascript/packs
+
+#  (OPTIONAL) By default the theme will fallback to the default theme
+#  if a particular pack is not provided. You can specify different
+#  fallbacks here, or disable fallback behaviours altogether by
+#  specifying a `null` value.
+fallback:
diff --git a/app/javascript/themes/vanilla/theme.yml b/app/javascript/themes/vanilla/theme.yml
index b4a1598fc..67fd9723e 100644
--- a/app/javascript/themes/vanilla/theme.yml
+++ b/app/javascript/themes/vanilla/theme.yml
@@ -1,12 +1,23 @@
 #  (REQUIRED) The location of the pack files inside `pack_directory`.
 pack:
   about: about.js
-  admin: null
-  common: common.js
-  embed: null
-  home: application.js
+  admin:
+  auth:
+  common:
+    filename: common.js
+    stylesheet: true
+  embed: public.js
+  error:
+  home:
+    filename: application.js
+    preload:
+    - features/getting_started
+    - features/compose
+    - features/home_timeline
+    - features/notifications
+  modal:
   public: public.js
-  settings: null
+  settings:
   share: share.js
 
 #  (OPTIONAL) The directory which contains the pack files.
@@ -15,12 +26,8 @@ pack:
 #  somewhere else.
 pack_directory: app/javascript/packs
 
-#  (OPTIONAL) Additional javascript resources to preload, for use with
-#  lazy-loaded components. It is **STRONGLY RECOMMENDED** that you
-#  derive these pathnames from `themes/[your-theme]` to ensure that
-#  they stay unique. (Of course, vanilla doesn't do this ^^;;)
-preload:
-- features/getting_started
-- features/compose
-- features/home_timeline
-- features/notifications
+#  (OPTIONAL) By default the theme will fallback to the default theme
+#  if a particular pack is not provided. You can specify different
+#  fallbacks here, or disable fallback behaviours altogether by
+#  specifying a `null` value.
+fallback:
diff --git a/app/javascript/themes/win95/index.js b/app/javascript/themes/win95/index.js
new file mode 100644
index 000000000..bed6a1ef3
--- /dev/null
+++ b/app/javascript/themes/win95/index.js
@@ -0,0 +1,10 @@
+//  These lines are the same as in glitch:
+import 'font-awesome/css/font-awesome.css';
+require.context('../../images/', true);
+
+//  …But we want to use our own styles instead.
+import 'styles/win95.scss';
+
+//  Be sure to make this style file import from
+//  `themes/glitch/styles/index.scss` (the glitch styling), and not
+//  `application.scss` (which are the vanilla styles).
diff --git a/app/javascript/themes/win95/theme.yml b/app/javascript/themes/win95/theme.yml
new file mode 100644
index 000000000..43af38198
--- /dev/null
+++ b/app/javascript/themes/win95/theme.yml
@@ -0,0 +1,23 @@
+#  win95 theme.
+
+#  Ported over from `cybrespace:mastodon/theme_win95`.
+#  <https://github.com/cybrespace/mastodon/tree/theme_win95>
+
+#  You can use this theme file as inspiration for porting over
+#  a preëxisting Mastodon theme.
+
+#  We only modify the `common` pack, which contains our styling.
+pack:
+  common:
+    filename: index.js
+    stylesheet: true
+  #  All unspecified packs will inherit from glitch.
+
+#  By default, the glitch preloads will also be used here. You can
+#  disable them by setting `preload` to `null`.
+
+#      preload:
+
+#  The `fallback` parameter tells us to use glitch files for everything
+#  we haven't specified.
+fallback: glitch