about summary refs log tree commit diff
path: root/app/javascript/themes/glitch/components
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/themes/glitch/components')
-rw-r--r--app/javascript/themes/glitch/components/account.js116
-rw-r--r--app/javascript/themes/glitch/components/attachment_list.js33
-rw-r--r--app/javascript/themes/glitch/components/autosuggest_emoji.js42
-rw-r--r--app/javascript/themes/glitch/components/autosuggest_textarea.js222
-rw-r--r--app/javascript/themes/glitch/components/avatar.js72
-rw-r--r--app/javascript/themes/glitch/components/avatar_overlay.js30
-rw-r--r--app/javascript/themes/glitch/components/button.js64
-rw-r--r--app/javascript/themes/glitch/components/collapsable.js22
-rw-r--r--app/javascript/themes/glitch/components/column.js54
-rw-r--r--app/javascript/themes/glitch/components/column_back_button.js29
-rw-r--r--app/javascript/themes/glitch/components/column_back_button_slim.js31
-rw-r--r--app/javascript/themes/glitch/components/column_header.js214
-rw-r--r--app/javascript/themes/glitch/components/display_name.js20
-rw-r--r--app/javascript/themes/glitch/components/dropdown_menu.js211
-rw-r--r--app/javascript/themes/glitch/components/extended_video_player.js54
-rw-r--r--app/javascript/themes/glitch/components/icon_button.js137
-rw-r--r--app/javascript/themes/glitch/components/intersection_observer_article.js130
-rw-r--r--app/javascript/themes/glitch/components/load_more.js26
-rw-r--r--app/javascript/themes/glitch/components/loading_indicator.js11
-rw-r--r--app/javascript/themes/glitch/components/media_gallery.js255
-rw-r--r--app/javascript/themes/glitch/components/missing_indicator.js12
-rw-r--r--app/javascript/themes/glitch/components/notification_purge_buttons.js58
-rw-r--r--app/javascript/themes/glitch/components/permalink.js34
-rw-r--r--app/javascript/themes/glitch/components/relative_timestamp.js147
-rw-r--r--app/javascript/themes/glitch/components/scrollable_list.js198
-rw-r--r--app/javascript/themes/glitch/components/setting_text.js34
-rw-r--r--app/javascript/themes/glitch/components/status.js442
-rw-r--r--app/javascript/themes/glitch/components/status_action_bar.js188
-rw-r--r--app/javascript/themes/glitch/components/status_content.js245
-rw-r--r--app/javascript/themes/glitch/components/status_header.js120
-rw-r--r--app/javascript/themes/glitch/components/status_list.js72
-rw-r--r--app/javascript/themes/glitch/components/status_prepend.js83
-rw-r--r--app/javascript/themes/glitch/components/status_visibility_icon.js48
33 files changed, 0 insertions, 3454 deletions
diff --git a/app/javascript/themes/glitch/components/account.js b/app/javascript/themes/glitch/components/account.js
deleted file mode 100644
index d0ff77050..000000000
--- a/app/javascript/themes/glitch/components/account.js
+++ /dev/null
@@ -1,116 +0,0 @@
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
-import Avatar from './avatar';
-import DisplayName from './display_name';
-import Permalink from './permalink';
-import IconButton from './icon_button';
-import { defineMessages, injectIntl } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { me } from 'themes/glitch/util/initial_state';
-
-const messages = defineMessages({
-  follow: { id: 'account.follow', defaultMessage: 'Follow' },
-  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
-  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
-  unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
-  unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
-  mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'You are not currently muting notifications from @{name}. Click to mute notifications' },
-  unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'You are currently muting notifications from @{name}. Click to unmute notifications' },
-});
-
-@injectIntl
-export default class Account extends ImmutablePureComponent {
-
-  static propTypes = {
-    account: ImmutablePropTypes.map.isRequired,
-    onFollow: PropTypes.func.isRequired,
-    onBlock: PropTypes.func.isRequired,
-    onMute: PropTypes.func.isRequired,
-    intl: PropTypes.object.isRequired,
-    hidden: PropTypes.bool,
-  };
-
-  handleFollow = () => {
-    this.props.onFollow(this.props.account);
-  }
-
-  handleBlock = () => {
-    this.props.onBlock(this.props.account);
-  }
-
-  handleMute = () => {
-    this.props.onMute(this.props.account);
-  }
-
-  handleMuteNotifications = () => {
-    this.props.onMuteNotifications(this.props.account, true);
-  }
-
-  handleUnmuteNotifications = () => {
-    this.props.onMuteNotifications(this.props.account, false);
-  }
-
-  render () {
-    const { account, intl, hidden } = this.props;
-
-    if (!account) {
-      return <div />;
-    }
-
-    if (hidden) {
-      return (
-        <div>
-          {account.get('display_name')}
-          {account.get('username')}
-        </div>
-      );
-    }
-
-    let buttons;
-
-    if (account.get('id') !== me && account.get('relationship', null) !== null) {
-      const following = account.getIn(['relationship', 'following']);
-      const requested = account.getIn(['relationship', 'requested']);
-      const blocking  = account.getIn(['relationship', 'blocking']);
-      const muting  = account.getIn(['relationship', 'muting']);
-
-      if (requested) {
-        buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
-      } else if (blocking) {
-        buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
-      } else if (muting) {
-        let hidingNotificationsButton;
-        if (muting.get('notifications')) {
-          hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />;
-        } else {
-          hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username')  })} onClick={this.handleMuteNotifications} />;
-        }
-        buttons = (
-          <div>
-            <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />
-            {hidingNotificationsButton}
-          </div>
-        );
-      } else {
-        buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following ? true : false} />;
-      }
-    }
-
-    return (
-      <div className='account'>
-        <div className='account__wrapper'>
-          <Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
-            <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
-            <DisplayName account={account} />
-          </Permalink>
-
-          <div className='account__relationship'>
-            {buttons}
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/attachment_list.js b/app/javascript/themes/glitch/components/attachment_list.js
deleted file mode 100644
index b3d00b335..000000000
--- a/app/javascript/themes/glitch/components/attachment_list.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-
-const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
-
-export default class AttachmentList extends ImmutablePureComponent {
-
-  static propTypes = {
-    media: ImmutablePropTypes.list.isRequired,
-  };
-
-  render () {
-    const { media } = this.props;
-
-    return (
-      <div className='attachment-list'>
-        <div className='attachment-list__icon'>
-          <i className='fa fa-link' />
-        </div>
-
-        <ul className='attachment-list__list'>
-          {media.map(attachment =>
-            <li key={attachment.get('id')}>
-              <a href={attachment.get('remote_url')} target='_blank' rel='noopener'>{filename(attachment.get('remote_url'))}</a>
-            </li>
-          )}
-        </ul>
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/autosuggest_emoji.js b/app/javascript/themes/glitch/components/autosuggest_emoji.js
deleted file mode 100644
index 3c6f915e4..000000000
--- a/app/javascript/themes/glitch/components/autosuggest_emoji.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import unicodeMapping from 'themes/glitch/util/emoji/emoji_unicode_mapping_light';
-
-const assetHost = process.env.CDN_HOST || '';
-
-export default class AutosuggestEmoji extends React.PureComponent {
-
-  static propTypes = {
-    emoji: PropTypes.object.isRequired,
-  };
-
-  render () {
-    const { emoji } = this.props;
-    let url;
-
-    if (emoji.custom) {
-      url = emoji.imageUrl;
-    } else {
-      const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
-
-      if (!mapping) {
-        return null;
-      }
-
-      url = `${assetHost}/emoji/${mapping.filename}.svg`;
-    }
-
-    return (
-      <div className='autosuggest-emoji'>
-        <img
-          className='emojione'
-          src={url}
-          alt={emoji.native || emoji.colons}
-        />
-
-        {emoji.colons}
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/autosuggest_textarea.js b/app/javascript/themes/glitch/components/autosuggest_textarea.js
deleted file mode 100644
index fa93847a2..000000000
--- a/app/javascript/themes/glitch/components/autosuggest_textarea.js
+++ /dev/null
@@ -1,222 +0,0 @@
-import React from 'react';
-import AutosuggestAccountContainer from 'themes/glitch/features/compose/containers/autosuggest_account_container';
-import AutosuggestEmoji from './autosuggest_emoji';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
-import { isRtl } from 'themes/glitch/util/rtl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import Textarea from 'react-textarea-autosize';
-import classNames from 'classnames';
-
-const textAtCursorMatchesToken = (str, caretPosition) => {
-  let word;
-
-  let left  = str.slice(0, caretPosition).search(/[^\s\u200B]+$/);
-  let right = str.slice(caretPosition).search(/[\s\u200B]/);
-
-  if (right < 0) {
-    word = str.slice(left);
-  } else {
-    word = str.slice(left, right + caretPosition);
-  }
-
-  if (!word || word.trim().length < 3 || ['@', ':'].indexOf(word[0]) === -1) {
-    return [null, null];
-  }
-
-  word = word.trim().toLowerCase();
-
-  if (word.length > 0) {
-    return [left + 1, word];
-  } else {
-    return [null, null];
-  }
-};
-
-export default class AutosuggestTextarea extends ImmutablePureComponent {
-
-  static propTypes = {
-    value: PropTypes.string,
-    suggestions: ImmutablePropTypes.list,
-    disabled: PropTypes.bool,
-    placeholder: PropTypes.string,
-    onSuggestionSelected: PropTypes.func.isRequired,
-    onSuggestionsClearRequested: PropTypes.func.isRequired,
-    onSuggestionsFetchRequested: PropTypes.func.isRequired,
-    onChange: PropTypes.func.isRequired,
-    onKeyUp: PropTypes.func,
-    onKeyDown: PropTypes.func,
-    onPaste: PropTypes.func.isRequired,
-    autoFocus: PropTypes.bool,
-  };
-
-  static defaultProps = {
-    autoFocus: true,
-  };
-
-  state = {
-    suggestionsHidden: false,
-    selectedSuggestion: 0,
-    lastToken: null,
-    tokenStart: 0,
-  };
-
-  onChange = (e) => {
-    const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
-
-    if (token !== null && this.state.lastToken !== token) {
-      this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
-      this.props.onSuggestionsFetchRequested(token);
-    } else if (token === null) {
-      this.setState({ lastToken: null });
-      this.props.onSuggestionsClearRequested();
-    }
-
-    this.props.onChange(e);
-  }
-
-  onKeyDown = (e) => {
-    const { suggestions, disabled } = this.props;
-    const { selectedSuggestion, suggestionsHidden } = this.state;
-
-    if (disabled) {
-      e.preventDefault();
-      return;
-    }
-
-    switch(e.key) {
-    case 'Escape':
-      if (!suggestionsHidden) {
-        e.preventDefault();
-        this.setState({ suggestionsHidden: true });
-      }
-
-      break;
-    case 'ArrowDown':
-      if (suggestions.size > 0 && !suggestionsHidden) {
-        e.preventDefault();
-        this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
-      }
-
-      break;
-    case 'ArrowUp':
-      if (suggestions.size > 0 && !suggestionsHidden) {
-        e.preventDefault();
-        this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
-      }
-
-      break;
-    case 'Enter':
-    case 'Tab':
-      // Select suggestion
-      if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
-        e.preventDefault();
-        e.stopPropagation();
-        this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
-      }
-
-      break;
-    }
-
-    if (e.defaultPrevented || !this.props.onKeyDown) {
-      return;
-    }
-
-    this.props.onKeyDown(e);
-  }
-
-  onKeyUp = e => {
-    if (e.key === 'Escape' && this.state.suggestionsHidden) {
-      document.querySelector('.ui').parentElement.focus();
-    }
-
-    if (this.props.onKeyUp) {
-      this.props.onKeyUp(e);
-    }
-  }
-
-  onBlur = () => {
-    this.setState({ suggestionsHidden: true });
-  }
-
-  onSuggestionClick = (e) => {
-    const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
-    e.preventDefault();
-    this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
-    this.textarea.focus();
-  }
-
-  componentWillReceiveProps (nextProps) {
-    if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) {
-      this.setState({ suggestionsHidden: false });
-    }
-  }
-
-  setTextarea = (c) => {
-    this.textarea = c;
-  }
-
-  onPaste = (e) => {
-    if (e.clipboardData && e.clipboardData.files.length === 1) {
-      this.props.onPaste(e.clipboardData.files);
-      e.preventDefault();
-    }
-  }
-
-  renderSuggestion = (suggestion, i) => {
-    const { selectedSuggestion } = this.state;
-    let inner, key;
-
-    if (typeof suggestion === 'object') {
-      inner = <AutosuggestEmoji emoji={suggestion} />;
-      key   = suggestion.id;
-    } else {
-      inner = <AutosuggestAccountContainer id={suggestion} />;
-      key   = suggestion;
-    }
-
-    return (
-      <div role='button' tabIndex='0' key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
-        {inner}
-      </div>
-    );
-  }
-
-  render () {
-    const { value, suggestions, disabled, placeholder, autoFocus } = this.props;
-    const { suggestionsHidden } = this.state;
-    const style = { direction: 'ltr' };
-
-    if (isRtl(value)) {
-      style.direction = 'rtl';
-    }
-
-    return (
-      <div className='autosuggest-textarea'>
-        <label>
-          <span style={{ display: 'none' }}>{placeholder}</span>
-
-          <Textarea
-            inputRef={this.setTextarea}
-            className='autosuggest-textarea__textarea'
-            disabled={disabled}
-            placeholder={placeholder}
-            autoFocus={autoFocus}
-            value={value}
-            onChange={this.onChange}
-            onKeyDown={this.onKeyDown}
-            onKeyUp={this.onKeyUp}
-            onBlur={this.onBlur}
-            onPaste={this.onPaste}
-            style={style}
-          />
-        </label>
-
-        <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
-          {suggestions.map(this.renderSuggestion)}
-        </div>
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/avatar.js b/app/javascript/themes/glitch/components/avatar.js
deleted file mode 100644
index dd155f059..000000000
--- a/app/javascript/themes/glitch/components/avatar.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-
-export default class Avatar extends React.PureComponent {
-
-  static propTypes = {
-    account: ImmutablePropTypes.map.isRequired,
-    size: PropTypes.number.isRequired,
-    style: PropTypes.object,
-    animate: PropTypes.bool,
-    inline: PropTypes.bool,
-  };
-
-  static defaultProps = {
-    animate: false,
-    size: 20,
-    inline: false,
-  };
-
-  state = {
-    hovering: false,
-  };
-
-  handleMouseEnter = () => {
-    if (this.props.animate) return;
-    this.setState({ hovering: true });
-  }
-
-  handleMouseLeave = () => {
-    if (this.props.animate) return;
-    this.setState({ hovering: false });
-  }
-
-  render () {
-    const { account, size, animate, inline } = this.props;
-    const { hovering } = this.state;
-
-    const src = account.get('avatar');
-    const staticSrc = account.get('avatar_static');
-
-    let className = 'account__avatar';
-
-    if (inline) {
-      className = className + ' account__avatar-inline';
-    }
-
-    const style = {
-      ...this.props.style,
-      width: `${size}px`,
-      height: `${size}px`,
-      backgroundSize: `${size}px ${size}px`,
-    };
-
-    if (hovering || animate) {
-      style.backgroundImage = `url(${src})`;
-    } else {
-      style.backgroundImage = `url(${staticSrc})`;
-    }
-
-    return (
-      <div
-        className={className}
-        onMouseEnter={this.handleMouseEnter}
-        onMouseLeave={this.handleMouseLeave}
-        style={style}
-        data-avatar-of={`@${account.get('acct')}`}
-      />
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/avatar_overlay.js b/app/javascript/themes/glitch/components/avatar_overlay.js
deleted file mode 100644
index 2ecf9fa44..000000000
--- a/app/javascript/themes/glitch/components/avatar_overlay.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-
-export default class AvatarOverlay extends React.PureComponent {
-
-  static propTypes = {
-    account: ImmutablePropTypes.map.isRequired,
-    friend: ImmutablePropTypes.map.isRequired,
-  };
-
-  render() {
-    const { account, friend } = this.props;
-
-    const baseStyle = {
-      backgroundImage: `url(${account.get('avatar_static')})`,
-    };
-
-    const overlayStyle = {
-      backgroundImage: `url(${friend.get('avatar_static')})`,
-    };
-
-    return (
-      <div className='account__avatar-overlay'>
-        <div className='account__avatar-overlay-base' style={baseStyle} data-avatar-of={`@${account.get('acct')}`} />
-        <div className='account__avatar-overlay-overlay' style={overlayStyle} data-avatar-of={`@${friend.get('acct')}`} />
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/button.js b/app/javascript/themes/glitch/components/button.js
deleted file mode 100644
index 16868010c..000000000
--- a/app/javascript/themes/glitch/components/button.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-
-export default class Button extends React.PureComponent {
-
-  static propTypes = {
-    text: PropTypes.node,
-    onClick: PropTypes.func,
-    disabled: PropTypes.bool,
-    block: PropTypes.bool,
-    secondary: PropTypes.bool,
-    size: PropTypes.number,
-    className: PropTypes.string,
-    style: PropTypes.object,
-    children: PropTypes.node,
-    title: PropTypes.string,
-  };
-
-  static defaultProps = {
-    size: 36,
-  };
-
-  handleClick = (e) => {
-    if (!this.props.disabled) {
-      this.props.onClick(e);
-    }
-  }
-
-  setRef = (c) => {
-    this.node = c;
-  }
-
-  focus() {
-    this.node.focus();
-  }
-
-  render () {
-    let attrs = {
-      className: classNames('button', this.props.className, {
-        'button-secondary': this.props.secondary,
-        'button--block': this.props.block,
-      }),
-      disabled: this.props.disabled,
-      onClick: this.handleClick,
-      ref: this.setRef,
-      style: {
-        padding: `0 ${this.props.size / 2.25}px`,
-        height: `${this.props.size}px`,
-        lineHeight: `${this.props.size}px`,
-        ...this.props.style,
-      },
-    };
-
-    if (this.props.title) attrs.title = this.props.title;
-
-    return (
-      <button {...attrs}>
-        {this.props.text || this.props.children}
-      </button>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/collapsable.js b/app/javascript/themes/glitch/components/collapsable.js
deleted file mode 100644
index 8bc0a54f4..000000000
--- a/app/javascript/themes/glitch/components/collapsable.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from 'react';
-import Motion from 'themes/glitch/util/optional_motion';
-import spring from 'react-motion/lib/spring';
-import PropTypes from 'prop-types';
-
-const Collapsable = ({ fullHeight, isVisible, children }) => (
-  <Motion defaultStyle={{ opacity: !isVisible ? 0 : 100, height: isVisible ? fullHeight : 0 }} style={{ opacity: spring(!isVisible ? 0 : 100), height: spring(!isVisible ? 0 : fullHeight) }}>
-    {({ opacity, height }) =>
-      <div style={{ height: `${height}px`, overflow: 'hidden', opacity: opacity / 100, display: Math.floor(opacity) === 0 ? 'none' : 'block' }}>
-        {children}
-      </div>
-    }
-  </Motion>
-);
-
-Collapsable.propTypes = {
-  fullHeight: PropTypes.number.isRequired,
-  isVisible: PropTypes.bool.isRequired,
-  children: PropTypes.node.isRequired,
-};
-
-export default Collapsable;
diff --git a/app/javascript/themes/glitch/components/column.js b/app/javascript/themes/glitch/components/column.js
deleted file mode 100644
index adeba9cc1..000000000
--- a/app/javascript/themes/glitch/components/column.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import detectPassiveEvents from 'detect-passive-events';
-import { scrollTop } from 'themes/glitch/util/scroll';
-
-export default class Column extends React.PureComponent {
-
-  static propTypes = {
-    children: PropTypes.node,
-    extraClasses: PropTypes.string,
-    name: PropTypes.string,
-  };
-
-  scrollTop () {
-    const scrollable = this.node.querySelector('.scrollable');
-
-    if (!scrollable) {
-      return;
-    }
-
-    this._interruptScrollAnimation = scrollTop(scrollable);
-  }
-
-  handleWheel = () => {
-    if (typeof this._interruptScrollAnimation !== 'function') {
-      return;
-    }
-
-    this._interruptScrollAnimation();
-  }
-
-  setRef = c => {
-    this.node = c;
-  }
-
-  componentDidMount () {
-    this.node.addEventListener('wheel', this.handleWheel,  detectPassiveEvents.hasSupport ? { passive: true } : false);
-  }
-
-  componentWillUnmount () {
-    this.node.removeEventListener('wheel', this.handleWheel);
-  }
-
-  render () {
-    const { children, extraClasses, name } = this.props;
-
-    return (
-      <div role='region' data-column={name} className={`column ${extraClasses || ''}`} ref={this.setRef}>
-        {children}
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/column_back_button.js b/app/javascript/themes/glitch/components/column_back_button.js
deleted file mode 100644
index 50c3bf11f..000000000
--- a/app/javascript/themes/glitch/components/column_back_button.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-import PropTypes from 'prop-types';
-
-export default class ColumnBackButton extends React.PureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
-  handleClick = () => {
-    // if history is exhausted, or we would leave mastodon, just go to root.
-    if (window.history && (window.history.length === 1 || window.history.length === window._mastoInitialHistoryLen)) {
-      this.context.router.history.push('/');
-    } else {
-      this.context.router.history.goBack();
-    }
-  }
-
-  render () {
-    return (
-      <button onClick={this.handleClick} className='column-back-button'>
-        <i className='fa fa-fw fa-chevron-left column-back-button__icon' />
-        <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
-      </button>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/column_back_button_slim.js b/app/javascript/themes/glitch/components/column_back_button_slim.js
deleted file mode 100644
index 2cdf1b25b..000000000
--- a/app/javascript/themes/glitch/components/column_back_button_slim.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-import PropTypes from 'prop-types';
-
-export default class ColumnBackButtonSlim extends React.PureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
-  handleClick = () => {
-    // if history is exhausted, or we would leave mastodon, just go to root.
-    if (window.history && (window.history.length === 1 || window.history.length === window._mastoInitialHistoryLen)) {
-      this.context.router.history.push('/');
-    } else {
-      this.context.router.history.goBack();
-    }
-  }
-
-  render () {
-    return (
-      <div className='column-back-button--slim'>
-        <div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'>
-          <i className='fa fa-fw fa-chevron-left column-back-button__icon' />
-          <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
-        </div>
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/column_header.js b/app/javascript/themes/glitch/components/column_header.js
deleted file mode 100644
index e601082c8..000000000
--- a/app/javascript/themes/glitch/components/column_header.js
+++ /dev/null
@@ -1,214 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-
-// Glitch imports
-import NotificationPurgeButtonsContainer from 'themes/glitch/containers/notification_purge_buttons_container';
-
-const messages = defineMessages({
-  show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
-  hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
-  moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
-  moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
-  enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' },
-});
-
-@injectIntl
-export default class ColumnHeader extends React.PureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
-  static propTypes = {
-    intl: PropTypes.object.isRequired,
-    title: PropTypes.node.isRequired,
-    icon: PropTypes.string.isRequired,
-    active: PropTypes.bool,
-    localSettings : ImmutablePropTypes.map,
-    multiColumn: PropTypes.bool,
-    focusable: PropTypes.bool,
-    showBackButton: PropTypes.bool,
-    notifCleaning: PropTypes.bool, // true only for the notification column
-    notifCleaningActive: PropTypes.bool,
-    onEnterCleaningMode: PropTypes.func,
-    children: PropTypes.node,
-    pinned: PropTypes.bool,
-    onPin: PropTypes.func,
-    onMove: PropTypes.func,
-    onClick: PropTypes.func,
-    intl: PropTypes.object.isRequired,
-  };
-
-  static defaultProps = {
-    focusable: true,
-  }
-
-  state = {
-    collapsed: true,
-    animating: false,
-    animatingNCD: false,
-  };
-
-  handleToggleClick = (e) => {
-    e.stopPropagation();
-    this.setState({ collapsed: !this.state.collapsed, animating: true });
-  }
-
-  handleTitleClick = () => {
-    this.props.onClick();
-  }
-
-  handleMoveLeft = () => {
-    this.props.onMove(-1);
-  }
-
-  handleMoveRight = () => {
-    this.props.onMove(1);
-  }
-
-  handleBackClick = () => {
-    // if history is exhausted, or we would leave mastodon, just go to root.
-    if (window.history && (window.history.length === 1 || window.history.length === window._mastoInitialHistoryLen)) {
-      this.context.router.history.push('/');
-    } else {
-      this.context.router.history.goBack();
-    }
-  }
-
-  handleTransitionEnd = () => {
-    this.setState({ animating: false });
-  }
-
-  handleTransitionEndNCD = () => {
-    this.setState({ animatingNCD: false });
-  }
-
-  onEnterCleaningMode = () => {
-    this.setState({ animatingNCD: true });
-    this.props.onEnterCleaningMode(!this.props.notifCleaningActive);
-  }
-
-  render () {
-    const { intl, icon, active, children, pinned, onPin, multiColumn, focusable, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive } = this.props;
-    const { collapsed, animating, animatingNCD } = this.state;
-
-    let title = this.props.title;
-
-    const wrapperClassName = classNames('column-header__wrapper', {
-      'active': active,
-    });
-
-    const buttonClassName = classNames('column-header', {
-      'active': active,
-    });
-
-    const collapsibleClassName = classNames('column-header__collapsible', {
-      'collapsed': collapsed,
-      'animating': animating,
-    });
-
-    const collapsibleButtonClassName = classNames('column-header__button', {
-      'active': !collapsed,
-    });
-
-    const notifCleaningButtonClassName = classNames('column-header__button', {
-      'active': notifCleaningActive,
-    });
-
-    const notifCleaningDrawerClassName = classNames('ncd column-header__collapsible', {
-      'collapsed': !notifCleaningActive,
-      'animating': animatingNCD,
-    });
-
-    let extraContent, pinButton, moveButtons, backButton, collapseButton;
-
-    //*glitch
-    const msgEnterNotifCleaning = intl.formatMessage(messages.enterNotifCleaning);
-
-    if (children) {
-      extraContent = (
-        <div key='extra-content' className='column-header__collapsible__extra'>
-          {children}
-        </div>
-      );
-    }
-
-    if (multiColumn && pinned) {
-      pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={onPin}><i className='fa fa fa-times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
-
-      moveButtons = (
-        <div key='move-buttons' className='column-header__setting-arrows'>
-          <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button>
-          <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button>
-        </div>
-      );
-    } else if (multiColumn) {
-      pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={onPin}><i className='fa fa fa-plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
-    }
-
-    if (!pinned && (multiColumn || showBackButton)) {
-      backButton = (
-        <button onClick={this.handleBackClick} className='column-header__back-button'>
-          <i className='fa fa-fw fa-chevron-left column-back-button__icon' />
-          <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
-        </button>
-      );
-    }
-
-    const collapsedContent = [
-      extraContent,
-    ];
-
-    if (multiColumn) {
-      collapsedContent.push(moveButtons);
-      collapsedContent.push(pinButton);
-    }
-
-    if (children || multiColumn) {
-      collapseButton = <button className={collapsibleButtonClassName} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>;
-    }
-
-    return (
-      <div className={wrapperClassName}>
-        <h1 tabIndex={focusable ? 0 : null} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
-          <i className={`fa fa-fw fa-${icon} column-header__icon`} />
-          <span className='column-header__title'>
-            {title}
-          </span>
-          <div className='column-header__buttons'>
-            {backButton}
-            { notifCleaning ? (
-              <button
-                aria-label={msgEnterNotifCleaning}
-                title={msgEnterNotifCleaning}
-                onClick={this.onEnterCleaningMode}
-                className={notifCleaningButtonClassName}
-              >
-                <i className='fa fa-eraser' />
-              </button>
-            ) : null}
-            {collapseButton}
-          </div>
-        </h1>
-
-        { notifCleaning ? (
-          <div className={notifCleaningDrawerClassName} onTransitionEnd={this.handleTransitionEndNCD}>
-            <div className='column-header__collapsible-inner nopad-drawer'>
-              {(notifCleaningActive || animatingNCD) ? (<NotificationPurgeButtonsContainer />) : null }
-            </div>
-          </div>
-        ) : null}
-
-        <div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}>
-          <div className='column-header__collapsible-inner'>
-            {(!collapsed || animating) && collapsedContent}
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/display_name.js b/app/javascript/themes/glitch/components/display_name.js
deleted file mode 100644
index 2cf84f8f4..000000000
--- a/app/javascript/themes/glitch/components/display_name.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-
-export default class DisplayName extends React.PureComponent {
-
-  static propTypes = {
-    account: ImmutablePropTypes.map.isRequired,
-  };
-
-  render () {
-    const displayNameHtml = { __html: this.props.account.get('display_name_html') };
-
-    return (
-      <span className='display-name'>
-        <strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span>
-      </span>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/dropdown_menu.js b/app/javascript/themes/glitch/components/dropdown_menu.js
deleted file mode 100644
index d30dc2aaf..000000000
--- a/app/javascript/themes/glitch/components/dropdown_menu.js
+++ /dev/null
@@ -1,211 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import IconButton from './icon_button';
-import Overlay from 'react-overlays/lib/Overlay';
-import Motion from 'themes/glitch/util/optional_motion';
-import spring from 'react-motion/lib/spring';
-import detectPassiveEvents from 'detect-passive-events';
-
-const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
-
-class DropdownMenu extends React.PureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
-  static propTypes = {
-    items: PropTypes.array.isRequired,
-    onClose: PropTypes.func.isRequired,
-    style: PropTypes.object,
-    placement: PropTypes.string,
-    arrowOffsetLeft: PropTypes.string,
-    arrowOffsetTop: PropTypes.string,
-  };
-
-  static defaultProps = {
-    style: {},
-    placement: 'bottom',
-  };
-
-  handleDocumentClick = e => {
-    if (this.node && !this.node.contains(e.target)) {
-      this.props.onClose();
-    }
-  }
-
-  componentDidMount () {
-    document.addEventListener('click', this.handleDocumentClick, false);
-    document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
-  }
-
-  componentWillUnmount () {
-    document.removeEventListener('click', this.handleDocumentClick, false);
-    document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
-  }
-
-  setRef = c => {
-    this.node = c;
-  }
-
-  handleClick = e => {
-    const i = Number(e.currentTarget.getAttribute('data-index'));
-    const { action, to } = this.props.items[i];
-
-    this.props.onClose();
-
-    if (typeof action === 'function') {
-      e.preventDefault();
-      action();
-    } else if (to) {
-      e.preventDefault();
-      this.context.router.history.push(to);
-    }
-  }
-
-  renderItem (option, i) {
-    if (option === null) {
-      return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
-    }
-
-    const { text, href = '#' } = option;
-
-    return (
-      <li className='dropdown-menu__item' key={`${text}-${i}`}>
-        <a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' autoFocus={i === 0} onClick={this.handleClick} data-index={i}>
-          {text}
-        </a>
-      </li>
-    );
-  }
-
-  render () {
-    const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
-
-    return (
-      <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
-        {({ opacity, scaleX, scaleY }) => (
-          <div className='dropdown-menu' style={{ ...style, opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }} ref={this.setRef}>
-            <div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
-
-            <ul>
-              {items.map((option, i) => this.renderItem(option, i))}
-            </ul>
-          </div>
-        )}
-      </Motion>
-    );
-  }
-
-}
-
-export default class Dropdown extends React.PureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
-  static propTypes = {
-    icon: PropTypes.string.isRequired,
-    items: PropTypes.array.isRequired,
-    size: PropTypes.number.isRequired,
-    ariaLabel: PropTypes.string,
-    disabled: PropTypes.bool,
-    status: ImmutablePropTypes.map,
-    isUserTouching: PropTypes.func,
-    isModalOpen: PropTypes.bool.isRequired,
-    onModalOpen: PropTypes.func,
-    onModalClose: PropTypes.func,
-  };
-
-  static defaultProps = {
-    ariaLabel: 'Menu',
-  };
-
-  state = {
-    expanded: false,
-  };
-
-  handleClick = () => {
-    if (!this.state.expanded && this.props.isUserTouching() && this.props.onModalOpen) {
-      const { status, items } = this.props;
-
-      this.props.onModalOpen({
-        status,
-        actions: items,
-        onClick: this.handleItemClick,
-      });
-
-      return;
-    }
-
-    this.setState({ expanded: !this.state.expanded });
-  }
-
-  handleClose = () => {
-    if (this.props.onModalClose) {
-      this.props.onModalClose();
-    }
-
-    this.setState({ expanded: false });
-  }
-
-  handleKeyDown = e => {
-    switch(e.key) {
-    case 'Enter':
-      this.handleClick();
-      break;
-    case 'Escape':
-      this.handleClose();
-      break;
-    }
-  }
-
-  handleItemClick = e => {
-    const i = Number(e.currentTarget.getAttribute('data-index'));
-    const { action, to } = this.props.items[i];
-
-    this.handleClose();
-
-    if (typeof action === 'function') {
-      e.preventDefault();
-      action();
-    } else if (to) {
-      e.preventDefault();
-      this.context.router.history.push(to);
-    }
-  }
-
-  setTargetRef = c => {
-    this.target = c;
-  }
-
-  findTarget = () => {
-    return this.target;
-  }
-
-  render () {
-    const { icon, items, size, ariaLabel, disabled } = this.props;
-    const { expanded } = this.state;
-
-    return (
-      <div onKeyDown={this.handleKeyDown}>
-        <IconButton
-          icon={icon}
-          title={ariaLabel}
-          active={expanded}
-          disabled={disabled}
-          size={size}
-          ref={this.setTargetRef}
-          onClick={this.handleClick}
-        />
-
-        <Overlay show={expanded} placement='bottom' target={this.findTarget}>
-          <DropdownMenu items={items} onClose={this.handleClose} />
-        </Overlay>
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/extended_video_player.js b/app/javascript/themes/glitch/components/extended_video_player.js
deleted file mode 100644
index f8bd067e8..000000000
--- a/app/javascript/themes/glitch/components/extended_video_player.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-export default class ExtendedVideoPlayer extends React.PureComponent {
-
-  static propTypes = {
-    src: PropTypes.string.isRequired,
-    alt: PropTypes.string,
-    width: PropTypes.number,
-    height: PropTypes.number,
-    time: PropTypes.number,
-    controls: PropTypes.bool.isRequired,
-    muted: PropTypes.bool.isRequired,
-  };
-
-  handleLoadedData = () => {
-    if (this.props.time) {
-      this.video.currentTime = this.props.time;
-    }
-  }
-
-  componentDidMount () {
-    this.video.addEventListener('loadeddata', this.handleLoadedData);
-  }
-
-  componentWillUnmount () {
-    this.video.removeEventListener('loadeddata', this.handleLoadedData);
-  }
-
-  setRef = (c) => {
-    this.video = c;
-  }
-
-  render () {
-    const { src, muted, controls, alt } = this.props;
-
-    return (
-      <div className='extended-video-player'>
-        <video
-          ref={this.setRef}
-          src={src}
-          autoPlay
-          role='button'
-          tabIndex='0'
-          aria-label={alt}
-          muted={muted}
-          controls={controls}
-          loop={!controls}
-        />
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/icon_button.js b/app/javascript/themes/glitch/components/icon_button.js
deleted file mode 100644
index 31cdf4703..000000000
--- a/app/javascript/themes/glitch/components/icon_button.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import React from 'react';
-import Motion from 'themes/glitch/util/optional_motion';
-import spring from 'react-motion/lib/spring';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-
-export default class IconButton extends React.PureComponent {
-
-  static propTypes = {
-    className: PropTypes.string,
-    title: PropTypes.string.isRequired,
-    icon: PropTypes.string.isRequired,
-    onClick: PropTypes.func,
-    size: PropTypes.number,
-    active: PropTypes.bool,
-    pressed: PropTypes.bool,
-    expanded: PropTypes.bool,
-    style: PropTypes.object,
-    activeStyle: PropTypes.object,
-    disabled: PropTypes.bool,
-    inverted: PropTypes.bool,
-    animate: PropTypes.bool,
-    flip: PropTypes.bool,
-    overlay: PropTypes.bool,
-    tabIndex: PropTypes.string,
-    label: PropTypes.string,
-  };
-
-  static defaultProps = {
-    size: 18,
-    active: false,
-    disabled: false,
-    animate: false,
-    overlay: false,
-    tabIndex: '0',
-  };
-
-  handleClick = (e) =>  {
-    e.preventDefault();
-
-    if (!this.props.disabled) {
-      this.props.onClick(e);
-    }
-  }
-
-  render () {
-    let style = {
-      fontSize: `${this.props.size}px`,
-      height: `${this.props.size * 1.28571429}px`,
-      lineHeight: `${this.props.size}px`,
-      ...this.props.style,
-      ...(this.props.active ? this.props.activeStyle : {}),
-    };
-    if (!this.props.label) {
-      style.width = `${this.props.size * 1.28571429}px`;
-    } else {
-      style.textAlign = 'left';
-    }
-
-    const {
-      active,
-      animate,
-      className,
-      disabled,
-      expanded,
-      icon,
-      inverted,
-      flip,
-      overlay,
-      pressed,
-      tabIndex,
-      title,
-    } = this.props;
-
-    const classes = classNames(className, 'icon-button', {
-      active,
-      disabled,
-      inverted,
-      overlayed: overlay,
-    });
-
-    const flipDeg = flip ? -180 : -360;
-    const rotateDeg = active ? flipDeg : 0;
-
-    const motionDefaultStyle = {
-      rotate: rotateDeg,
-    };
-
-    const springOpts = {
-      stiffness: this.props.flip ? 60 : 120,
-      damping: 7,
-    };
-    const motionStyle = {
-      rotate: animate ? spring(rotateDeg, springOpts) : 0,
-    };
-
-    if (!animate) {
-      // Perf optimization: avoid unnecessary <Motion> components unless
-      // we actually need to animate.
-      return (
-        <button
-          aria-label={title}
-          aria-pressed={pressed}
-          aria-expanded={expanded}
-          title={title}
-          className={classes}
-          onClick={this.handleClick}
-          style={style}
-          tabIndex={tabIndex}
-        >
-          <i className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
-        </button>
-      );
-    }
-
-    return (
-      <Motion defaultStyle={motionDefaultStyle} style={motionStyle}>
-        {({ rotate }) =>
-          <button
-            aria-label={title}
-            aria-pressed={pressed}
-            aria-expanded={expanded}
-            title={title}
-            className={classes}
-            onClick={this.handleClick}
-            style={style}
-            tabIndex={tabIndex}
-          >
-            <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
-            {this.props.label}
-          </button>
-        }
-      </Motion>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/intersection_observer_article.js b/app/javascript/themes/glitch/components/intersection_observer_article.js
deleted file mode 100644
index f0139ac75..000000000
--- a/app/javascript/themes/glitch/components/intersection_observer_article.js
+++ /dev/null
@@ -1,130 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import scheduleIdleTask from 'themes/glitch/util/schedule_idle_task';
-import getRectFromEntry from 'themes/glitch/util/get_rect_from_entry';
-import { is } from 'immutable';
-
-// Diff these props in the "rendered" state
-const updateOnPropsForRendered = ['id', 'index', 'listLength'];
-// Diff these props in the "unrendered" state
-const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
-
-export default class IntersectionObserverArticle extends React.Component {
-
-  static propTypes = {
-    intersectionObserverWrapper: PropTypes.object.isRequired,
-    id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
-    index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
-    listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
-    saveHeightKey: PropTypes.string,
-    cachedHeight: PropTypes.number,
-    onHeightChange: PropTypes.func,
-    children: PropTypes.node,
-  };
-
-  state = {
-    isHidden: false, // set to true in requestIdleCallback to trigger un-render
-  }
-
-  shouldComponentUpdate (nextProps, nextState) {
-    const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight);
-    const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight);
-    if (!!isUnrendered !== !!willBeUnrendered) {
-      // If we're going from rendered to unrendered (or vice versa) then update
-      return true;
-    }
-    // Otherwise, diff based on props
-    const propsToDiff = isUnrendered ? updateOnPropsForUnrendered : updateOnPropsForRendered;
-    return !propsToDiff.every(prop => is(nextProps[prop], this.props[prop]));
-  }
-
-  componentDidMount () {
-    const { intersectionObserverWrapper, id } = this.props;
-
-    intersectionObserverWrapper.observe(
-      id,
-      this.node,
-      this.handleIntersection
-    );
-
-    this.componentMounted = true;
-  }
-
-  componentWillUnmount () {
-    const { intersectionObserverWrapper, id } = this.props;
-    intersectionObserverWrapper.unobserve(id, this.node);
-
-    this.componentMounted = false;
-  }
-
-  handleIntersection = (entry) => {
-    this.entry = entry;
-
-    scheduleIdleTask(this.calculateHeight);
-    this.setState(this.updateStateAfterIntersection);
-  }
-
-  updateStateAfterIntersection = (prevState) => {
-    if (prevState.isIntersecting && !this.entry.isIntersecting) {
-      scheduleIdleTask(this.hideIfNotIntersecting);
-    }
-    return {
-      isIntersecting: this.entry.isIntersecting,
-      isHidden: false,
-    };
-  }
-
-  calculateHeight = () => {
-    const { onHeightChange, saveHeightKey, id } = this.props;
-    // save the height of the fully-rendered element (this is expensive
-    // on Chrome, where we need to fall back to getBoundingClientRect)
-    this.height = getRectFromEntry(this.entry).height;
-
-    if (onHeightChange && saveHeightKey) {
-      onHeightChange(saveHeightKey, id, this.height);
-    }
-  }
-
-  hideIfNotIntersecting = () => {
-    if (!this.componentMounted) {
-      return;
-    }
-
-    // When the browser gets a chance, test if we're still not intersecting,
-    // and if so, set our isHidden to true to trigger an unrender. The point of
-    // this is to save DOM nodes and avoid using up too much memory.
-    // See: https://github.com/tootsuite/mastodon/issues/2900
-    this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
-  }
-
-  handleRef = (node) => {
-    this.node = node;
-  }
-
-  render () {
-    const { children, id, index, listLength, cachedHeight } = this.props;
-    const { isIntersecting, isHidden } = this.state;
-
-    if (!isIntersecting && (isHidden || cachedHeight)) {
-      return (
-        <article
-          ref={this.handleRef}
-          aria-posinset={index}
-          aria-setsize={listLength}
-          style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
-          data-id={id}
-          tabIndex='0'
-        >
-          {children && React.cloneElement(children, { hidden: true })}
-        </article>
-      );
-    }
-
-    return (
-      <article ref={this.handleRef} aria-posinset={index} aria-setsize={listLength} data-id={id} tabIndex='0'>
-        {children && React.cloneElement(children, { hidden: false })}
-      </article>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/load_more.js b/app/javascript/themes/glitch/components/load_more.js
deleted file mode 100644
index c4c8c94a2..000000000
--- a/app/javascript/themes/glitch/components/load_more.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-import PropTypes from 'prop-types';
-
-export default class LoadMore extends React.PureComponent {
-
-  static propTypes = {
-    onClick: PropTypes.func,
-    visible: PropTypes.bool,
-  }
-
-  static defaultProps = {
-    visible: true,
-  }
-
-  render() {
-    const { visible } = this.props;
-
-    return (
-      <button className='load-more' disabled={!visible} style={{ visibility: visible ? 'visible' : 'hidden' }} onClick={this.props.onClick}>
-        <FormattedMessage id='status.load_more' defaultMessage='Load more' />
-      </button>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/loading_indicator.js b/app/javascript/themes/glitch/components/loading_indicator.js
deleted file mode 100644
index d6a5adb6f..000000000
--- a/app/javascript/themes/glitch/components/loading_indicator.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-
-const LoadingIndicator = () => (
-  <div className='loading-indicator'>
-    <div className='loading-indicator__figure' />
-    <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />
-  </div>
-);
-
-export default LoadingIndicator;
diff --git a/app/javascript/themes/glitch/components/media_gallery.js b/app/javascript/themes/glitch/components/media_gallery.js
deleted file mode 100644
index b6b40c585..000000000
--- a/app/javascript/themes/glitch/components/media_gallery.js
+++ /dev/null
@@ -1,255 +0,0 @@
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
-import { is } from 'immutable';
-import IconButton from './icon_button';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { isIOS } from 'themes/glitch/util/is_mobile';
-import classNames from 'classnames';
-import { autoPlayGif } from 'themes/glitch/util/initial_state';
-
-const messages = defineMessages({
-  toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
-});
-
-class Item extends React.PureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
-  static propTypes = {
-    attachment: ImmutablePropTypes.map.isRequired,
-    standalone: PropTypes.bool,
-    index: PropTypes.number.isRequired,
-    size: PropTypes.number.isRequired,
-    letterbox: PropTypes.bool,
-    onClick: PropTypes.func.isRequired,
-  };
-
-  static defaultProps = {
-    standalone: false,
-    index: 0,
-    size: 1,
-  };
-
-  handleMouseEnter = (e) => {
-    if (this.hoverToPlay()) {
-      e.target.play();
-    }
-  }
-
-  handleMouseLeave = (e) => {
-    if (this.hoverToPlay()) {
-      e.target.pause();
-      e.target.currentTime = 0;
-    }
-  }
-
-  hoverToPlay () {
-    const { attachment } = this.props;
-    return !autoPlayGif && attachment.get('type') === 'gifv';
-  }
-
-  handleClick = (e) => {
-    const { index, onClick } = this.props;
-
-    if (this.context.router && e.button === 0) {
-      e.preventDefault();
-      onClick(index);
-    }
-
-    e.stopPropagation();
-  }
-
-  render () {
-    const { attachment, index, size, standalone, letterbox } = this.props;
-
-    let width  = 50;
-    let height = 100;
-    let top    = 'auto';
-    let left   = 'auto';
-    let bottom = 'auto';
-    let right  = 'auto';
-
-    if (size === 1) {
-      width = 100;
-    }
-
-    if (size === 4 || (size === 3 && index > 0)) {
-      height = 50;
-    }
-
-    if (size === 2) {
-      if (index === 0) {
-        right = '2px';
-      } else {
-        left = '2px';
-      }
-    } else if (size === 3) {
-      if (index === 0) {
-        right = '2px';
-      } else if (index > 0) {
-        left = '2px';
-      }
-
-      if (index === 1) {
-        bottom = '2px';
-      } else if (index > 1) {
-        top = '2px';
-      }
-    } else if (size === 4) {
-      if (index === 0 || index === 2) {
-        right = '2px';
-      }
-
-      if (index === 1 || index === 3) {
-        left = '2px';
-      }
-
-      if (index < 2) {
-        bottom = '2px';
-      } else {
-        top = '2px';
-      }
-    }
-
-    let thumbnail = '';
-
-    if (attachment.get('type') === 'image') {
-      const previewUrl = attachment.get('preview_url');
-      const previewWidth = attachment.getIn(['meta', 'small', 'width']);
-
-      const originalUrl = attachment.get('url');
-      const originalWidth = attachment.getIn(['meta', 'original', 'width']);
-
-      const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
-
-      const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null;
-      const sizes = hasSize ? `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw` : null;
-
-      thumbnail = (
-        <a
-          className='media-gallery__item-thumbnail'
-          href={attachment.get('remote_url') || originalUrl}
-          onClick={this.handleClick}
-          target='_blank'
-        >
-          <img className={letterbox ? 'letterbox' : null} src={previewUrl} srcSet={srcSet} sizes={sizes} alt={attachment.get('description')} title={attachment.get('description')} />
-        </a>
-      );
-    } else if (attachment.get('type') === 'gifv') {
-      const autoPlay = !isIOS() && autoPlayGif;
-
-      thumbnail = (
-        <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
-          <video
-            className={`media-gallery__item-gifv-thumbnail${letterbox ? ' letterbox' : ''}`}
-            aria-label={attachment.get('description')}
-            role='application'
-            src={attachment.get('url')}
-            onClick={this.handleClick}
-            onMouseEnter={this.handleMouseEnter}
-            onMouseLeave={this.handleMouseLeave}
-            autoPlay={autoPlay}
-            loop
-            muted
-          />
-
-          <span className='media-gallery__gifv__label'>GIF</span>
-        </div>
-      );
-    }
-
-    return (
-      <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
-        {thumbnail}
-      </div>
-    );
-  }
-
-}
-
-@injectIntl
-export default class MediaGallery extends React.PureComponent {
-
-  static propTypes = {
-    sensitive: PropTypes.bool,
-    standalone: PropTypes.bool,
-    letterbox: PropTypes.bool,
-    fullwidth: PropTypes.bool,
-    media: ImmutablePropTypes.list.isRequired,
-    size: PropTypes.object,
-    onOpenMedia: PropTypes.func.isRequired,
-    intl: PropTypes.object.isRequired,
-  };
-
-  static defaultProps = {
-    standalone: false,
-  };
-
-  state = {
-    visible: !this.props.sensitive,
-  };
-
-  componentWillReceiveProps (nextProps) {
-    if (!is(nextProps.media, this.props.media)) {
-      this.setState({ visible: !nextProps.sensitive });
-    }
-  }
-
-  handleOpen = () => {
-    this.setState({ visible: !this.state.visible });
-  }
-
-  handleClick = (index) => {
-    this.props.onOpenMedia(this.props.media, index);
-  }
-
-  isStandaloneEligible() {
-    const { media, standalone } = this.props;
-    return standalone && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
-  }
-
-  render () {
-    const { media, intl, sensitive, letterbox, fullwidth } = this.props;
-    const { visible } = this.state;
-    const size = media.take(4).size;
-
-    let children;
-
-    if (!visible) {
-      let warning;
-
-      if (sensitive) {
-        warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
-      } else {
-        warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
-      }
-
-      children = (
-        <button className='media-spoiler' onClick={this.handleOpen}>
-          <span className='media-spoiler__warning'>{warning}</span>
-          <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
-        </button>
-      );
-    } else {
-      if (this.isStandaloneEligible()) {
-        children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} />;
-      } else {
-        children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} />);
-      }
-    }
-
-    return (
-      <div className={`media-gallery size-${size} ${fullwidth ? 'full-width' : ''}`}>
-        <div className={classNames('spoiler-button', { 'spoiler-button--visible': visible })}>
-          <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
-        </div>
-
-        {children}
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/missing_indicator.js b/app/javascript/themes/glitch/components/missing_indicator.js
deleted file mode 100644
index 87df7f61c..000000000
--- a/app/javascript/themes/glitch/components/missing_indicator.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-
-const MissingIndicator = () => (
-  <div className='missing-indicator'>
-    <div>
-      <FormattedMessage id='missing_indicator.label' defaultMessage='Not found' />
-    </div>
-  </div>
-);
-
-export default MissingIndicator;
diff --git a/app/javascript/themes/glitch/components/notification_purge_buttons.js b/app/javascript/themes/glitch/components/notification_purge_buttons.js
deleted file mode 100644
index e0c1543b0..000000000
--- a/app/javascript/themes/glitch/components/notification_purge_buttons.js
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * Buttons widget for controlling the notification clearing mode.
- * In idle state, the cleaning mode button is shown. When the mode is active,
- * a Confirm and Abort buttons are shown in its place.
- */
-
-
-//  Package imports  //
-import React from 'react';
-import PropTypes from 'prop-types';
-import { defineMessages, injectIntl } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-
-const messages = defineMessages({
-  btnAll : { id: 'notification_purge.btn_all', defaultMessage: 'Select\nall' },
-  btnNone : { id: 'notification_purge.btn_none', defaultMessage: 'Select\nnone' },
-  btnInvert : { id: 'notification_purge.btn_invert', defaultMessage: 'Invert\nselection' },
-  btnApply : { id: 'notification_purge.btn_apply', defaultMessage: 'Clear\nselected' },
-});
-
-@injectIntl
-export default class NotificationPurgeButtons extends ImmutablePureComponent {
-
-  static propTypes = {
-    onDeleteMarked : PropTypes.func.isRequired,
-    onMarkAll : PropTypes.func.isRequired,
-    onMarkNone : PropTypes.func.isRequired,
-    onInvert : PropTypes.func.isRequired,
-    intl: PropTypes.object.isRequired,
-    markNewForDelete: PropTypes.bool,
-  };
-
-  render () {
-    const { intl, markNewForDelete } = this.props;
-
-    //className='active'
-    return (
-      <div className='column-header__notif-cleaning-buttons'>
-        <button onClick={this.props.onMarkAll} className={markNewForDelete ? 'active' : ''}>
-          <b>∀</b><br />{intl.formatMessage(messages.btnAll)}
-        </button>
-
-        <button onClick={this.props.onMarkNone} className={!markNewForDelete ? 'active' : ''}>
-          <b>∅</b><br />{intl.formatMessage(messages.btnNone)}
-        </button>
-
-        <button onClick={this.props.onInvert}>
-          <b>¬</b><br />{intl.formatMessage(messages.btnInvert)}
-        </button>
-
-        <button onClick={this.props.onDeleteMarked}>
-          <i className='fa fa-trash' /><br />{intl.formatMessage(messages.btnApply)}
-        </button>
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/permalink.js b/app/javascript/themes/glitch/components/permalink.js
deleted file mode 100644
index d726d37a2..000000000
--- a/app/javascript/themes/glitch/components/permalink.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-export default class Permalink extends React.PureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
-  static propTypes = {
-    className: PropTypes.string,
-    href: PropTypes.string.isRequired,
-    to: PropTypes.string.isRequired,
-    children: PropTypes.node,
-  };
-
-  handleClick = (e) => {
-    if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
-      e.preventDefault();
-      this.context.router.history.push(this.props.to);
-    }
-  }
-
-  render () {
-    const { href, children, className, ...other } = this.props;
-
-    return (
-      <a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
-        {children}
-      </a>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/relative_timestamp.js b/app/javascript/themes/glitch/components/relative_timestamp.js
deleted file mode 100644
index 51588e78c..000000000
--- a/app/javascript/themes/glitch/components/relative_timestamp.js
+++ /dev/null
@@ -1,147 +0,0 @@
-import React from 'react';
-import { injectIntl, defineMessages } from 'react-intl';
-import PropTypes from 'prop-types';
-
-const messages = defineMessages({
-  just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
-  seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' },
-  minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
-  hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
-  days: { id: 'relative_time.days', defaultMessage: '{number}d' },
-});
-
-const dateFormatOptions = {
-  hour12: false,
-  year: 'numeric',
-  month: 'short',
-  day: '2-digit',
-  hour: '2-digit',
-  minute: '2-digit',
-};
-
-const shortDateFormatOptions = {
-  month: 'numeric',
-  day: 'numeric',
-};
-
-const SECOND = 1000;
-const MINUTE = 1000 * 60;
-const HOUR   = 1000 * 60 * 60;
-const DAY    = 1000 * 60 * 60 * 24;
-
-const MAX_DELAY = 2147483647;
-
-const selectUnits = delta => {
-  const absDelta = Math.abs(delta);
-
-  if (absDelta < MINUTE) {
-    return 'second';
-  } else if (absDelta < HOUR) {
-    return 'minute';
-  } else if (absDelta < DAY) {
-    return 'hour';
-  }
-
-  return 'day';
-};
-
-const getUnitDelay = units => {
-  switch (units) {
-  case 'second':
-    return SECOND;
-  case 'minute':
-    return MINUTE;
-  case 'hour':
-    return HOUR;
-  case 'day':
-    return DAY;
-  default:
-    return MAX_DELAY;
-  }
-};
-
-@injectIntl
-export default class RelativeTimestamp extends React.Component {
-
-  static propTypes = {
-    intl: PropTypes.object.isRequired,
-    timestamp: PropTypes.string.isRequired,
-  };
-
-  state = {
-    now: this.props.intl.now(),
-  };
-
-  shouldComponentUpdate (nextProps, nextState) {
-    // As of right now the locale doesn't change without a new page load,
-    // but we might as well check in case that ever changes.
-    return this.props.timestamp !== nextProps.timestamp ||
-      this.props.intl.locale !== nextProps.intl.locale ||
-      this.state.now !== nextState.now;
-  }
-
-  componentWillReceiveProps (nextProps) {
-    if (this.props.timestamp !== nextProps.timestamp) {
-      this.setState({ now: this.props.intl.now() });
-    }
-  }
-
-  componentDidMount () {
-    this._scheduleNextUpdate(this.props, this.state);
-  }
-
-  componentWillUpdate (nextProps, nextState) {
-    this._scheduleNextUpdate(nextProps, nextState);
-  }
-
-  componentWillUnmount () {
-    clearTimeout(this._timer);
-  }
-
-  _scheduleNextUpdate (props, state) {
-    clearTimeout(this._timer);
-
-    const { timestamp }  = props;
-    const delta          = (new Date(timestamp)).getTime() - state.now;
-    const unitDelay      = getUnitDelay(selectUnits(delta));
-    const unitRemainder  = Math.abs(delta % unitDelay);
-    const updateInterval = 1000 * 10;
-    const delay          = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder);
-
-    this._timer = setTimeout(() => {
-      this.setState({ now: this.props.intl.now() });
-    }, delay);
-  }
-
-  render () {
-    const { timestamp, intl } = this.props;
-
-    const date  = new Date(timestamp);
-    const delta = this.state.now - date.getTime();
-
-    let relativeTime;
-
-    if (delta < 10 * SECOND) {
-      relativeTime = intl.formatMessage(messages.just_now);
-    } else if (delta < 3 * DAY) {
-      if (delta < MINUTE) {
-        relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) });
-      } else if (delta < HOUR) {
-        relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) });
-      } else if (delta < DAY) {
-        relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) });
-      } else {
-        relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) });
-      }
-    } else {
-      relativeTime = intl.formatDate(date, shortDateFormatOptions);
-    }
-
-    return (
-      <time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
-        {relativeTime}
-      </time>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/scrollable_list.js b/app/javascript/themes/glitch/components/scrollable_list.js
deleted file mode 100644
index ccdcd7c85..000000000
--- a/app/javascript/themes/glitch/components/scrollable_list.js
+++ /dev/null
@@ -1,198 +0,0 @@
-import React, { PureComponent } from 'react';
-import { ScrollContainer } from 'react-router-scroll-4';
-import PropTypes from 'prop-types';
-import IntersectionObserverArticleContainer from 'themes/glitch/containers/intersection_observer_article_container';
-import LoadMore from './load_more';
-import IntersectionObserverWrapper from 'themes/glitch/util/intersection_observer_wrapper';
-import { throttle } from 'lodash';
-import { List as ImmutableList } from 'immutable';
-import classNames from 'classnames';
-import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'themes/glitch/util/fullscreen';
-
-export default class ScrollableList extends PureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
-  static propTypes = {
-    scrollKey: PropTypes.string.isRequired,
-    onScrollToBottom: PropTypes.func,
-    onScrollToTop: PropTypes.func,
-    onScroll: PropTypes.func,
-    trackScroll: PropTypes.bool,
-    shouldUpdateScroll: PropTypes.func,
-    isLoading: PropTypes.bool,
-    hasMore: PropTypes.bool,
-    prepend: PropTypes.node,
-    emptyMessage: PropTypes.node,
-    children: PropTypes.node,
-  };
-
-  static defaultProps = {
-    trackScroll: true,
-  };
-
-  state = {
-    lastMouseMove: null,
-  };
-
-  intersectionObserverWrapper = new IntersectionObserverWrapper();
-
-  handleScroll = throttle(() => {
-    if (this.node) {
-      const { scrollTop, scrollHeight, clientHeight } = this.node;
-      const offset = scrollHeight - scrollTop - clientHeight;
-      this._oldScrollPosition = scrollHeight - scrollTop;
-
-      if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
-        this.props.onScrollToBottom();
-      } else if (scrollTop < 100 && this.props.onScrollToTop) {
-        this.props.onScrollToTop();
-      } else if (this.props.onScroll) {
-        this.props.onScroll();
-      }
-    }
-  }, 150, {
-    trailing: true,
-  });
-
-  handleMouseMove = throttle(() => {
-    this._lastMouseMove = new Date();
-  }, 300);
-
-  handleMouseLeave = () => {
-    this._lastMouseMove = null;
-  }
-
-  componentDidMount () {
-    this.attachScrollListener();
-    this.attachIntersectionObserver();
-    attachFullscreenListener(this.onFullScreenChange);
-
-    // Handle initial scroll posiiton
-    this.handleScroll();
-  }
-
-  componentDidUpdate (prevProps) {
-    const someItemInserted = React.Children.count(prevProps.children) > 0 &&
-      React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
-      this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
-
-    // Reset the scroll position when a new child comes in in order not to
-    // jerk the scrollbar around if you're already scrolled down the page.
-    if (someItemInserted && this._oldScrollPosition && this.node.scrollTop > 0) {
-      const newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
-
-      if (this.node.scrollTop !== newScrollTop) {
-        this.node.scrollTop = newScrollTop;
-      }
-    } else {
-      this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
-    }
-  }
-
-  componentWillUnmount () {
-    this.detachScrollListener();
-    this.detachIntersectionObserver();
-    detachFullscreenListener(this.onFullScreenChange);
-  }
-
-  onFullScreenChange = () => {
-    this.setState({ fullscreen: isFullscreen() });
-  }
-
-  attachIntersectionObserver () {
-    this.intersectionObserverWrapper.connect({
-      root: this.node,
-      rootMargin: '300% 0px',
-    });
-  }
-
-  detachIntersectionObserver () {
-    this.intersectionObserverWrapper.disconnect();
-  }
-
-  attachScrollListener () {
-    this.node.addEventListener('scroll', this.handleScroll);
-  }
-
-  detachScrollListener () {
-    this.node.removeEventListener('scroll', this.handleScroll);
-  }
-
-  getFirstChildKey (props) {
-    const { children } = props;
-    let firstChild = children;
-    if (children instanceof ImmutableList) {
-      firstChild = children.get(0);
-    } else if (Array.isArray(children)) {
-      firstChild = children[0];
-    }
-    return firstChild && firstChild.key;
-  }
-
-  setRef = (c) => {
-    this.node = c;
-  }
-
-  handleLoadMore = (e) => {
-    e.preventDefault();
-    this.props.onScrollToBottom();
-  }
-
-  _recentlyMoved () {
-    return this._lastMouseMove !== null && ((new Date()) - this._lastMouseMove < 600);
-  }
-
-  render () {
-    const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
-    const { fullscreen } = this.state;
-    const childrenCount = React.Children.count(children);
-
-    const loadMore     = (hasMore && childrenCount > 0) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
-    let scrollableArea = null;
-
-    if (isLoading || childrenCount > 0 || !emptyMessage) {
-      scrollableArea = (
-        <div className={classNames('scrollable', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove} onMouseLeave={this.handleMouseLeave}>
-          <div role='feed' className='item-list'>
-            {prepend}
-
-            {React.Children.map(this.props.children, (child, index) => (
-              <IntersectionObserverArticleContainer
-                key={child.key}
-                id={child.key}
-                index={index}
-                listLength={childrenCount}
-                intersectionObserverWrapper={this.intersectionObserverWrapper}
-                saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
-              >
-                {child}
-              </IntersectionObserverArticleContainer>
-            ))}
-
-            {loadMore}
-          </div>
-        </div>
-      );
-    } else {
-      scrollableArea = (
-        <div className='empty-column-indicator' ref={this.setRef}>
-          {emptyMessage}
-        </div>
-      );
-    }
-
-    if (trackScroll) {
-      return (
-        <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
-          {scrollableArea}
-        </ScrollContainer>
-      );
-    } else {
-      return scrollableArea;
-    }
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/setting_text.js b/app/javascript/themes/glitch/components/setting_text.js
deleted file mode 100644
index a6dde4c0f..000000000
--- a/app/javascript/themes/glitch/components/setting_text.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-
-export default class SettingText extends React.PureComponent {
-
-  static propTypes = {
-    settings: ImmutablePropTypes.map.isRequired,
-    settingKey: PropTypes.array.isRequired,
-    label: PropTypes.string.isRequired,
-    onChange: PropTypes.func.isRequired,
-  };
-
-  handleChange = (e) => {
-    this.props.onChange(this.props.settingKey, e.target.value);
-  }
-
-  render () {
-    const { settings, settingKey, label } = this.props;
-
-    return (
-      <label>
-        <span style={{ display: 'none' }}>{label}</span>
-        <input
-          className='setting-text'
-          value={settings.getIn(settingKey)}
-          onChange={this.handleChange}
-          placeholder={label}
-        />
-      </label>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/status.js b/app/javascript/themes/glitch/components/status.js
deleted file mode 100644
index e2ef47f5f..000000000
--- a/app/javascript/themes/glitch/components/status.js
+++ /dev/null
@@ -1,442 +0,0 @@
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
-import StatusPrepend from './status_prepend';
-import StatusHeader from './status_header';
-import StatusContent from './status_content';
-import StatusActionBar from './status_action_bar';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { MediaGallery, Video } from 'themes/glitch/util/async-components';
-import { HotKeys } from 'react-hotkeys';
-import NotificationOverlayContainer from 'themes/glitch/features/notifications/containers/overlay_container';
-
-// We use the component (and not the container) since we do not want
-// to use the progress bar to show download progress
-import Bundle from '../features/ui/components/bundle';
-
-export default class Status extends ImmutablePureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
-  static propTypes = {
-    id: PropTypes.string,
-    status: ImmutablePropTypes.map,
-    account: ImmutablePropTypes.map,
-    onReply: PropTypes.func,
-    onFavourite: PropTypes.func,
-    onReblog: PropTypes.func,
-    onDelete: PropTypes.func,
-    onPin: PropTypes.func,
-    onOpenMedia: PropTypes.func,
-    onOpenVideo: PropTypes.func,
-    onBlock: PropTypes.func,
-    onEmbed: PropTypes.func,
-    onHeightChange: PropTypes.func,
-    muted: PropTypes.bool,
-    collapse: PropTypes.bool,
-    hidden: PropTypes.bool,
-    prepend: PropTypes.string,
-    withDismiss: PropTypes.bool,
-    onMoveUp: PropTypes.func,
-    onMoveDown: PropTypes.func,
-  };
-
-  state = {
-    isExpanded: null,
-    markedForDelete: false,
-  }
-
-  // Avoid checking props that are functions (and whose equality will always
-  // evaluate to false. See react-immutable-pure-component for usage.
-  updateOnProps = [
-    'status',
-    'account',
-    'settings',
-    'prepend',
-    'boostModal',
-    'muted',
-    'collapse',
-    'notification',
-  ]
-
-  updateOnStates = [
-    'isExpanded',
-    'markedForDelete',
-  ]
-
-  //  If our settings have changed to disable collapsed statuses, then we
-  //  need to make sure that we uncollapse every one. We do that by watching
-  //  for changes to `settings.collapsed.enabled` in
-  //  `componentWillReceiveProps()`.
-
-  //  We also need to watch for changes on the `collapse` prop---if this
-  //  changes to anything other than `undefined`, then we need to collapse or
-  //  uncollapse our status accordingly.
-  componentWillReceiveProps (nextProps) {
-    if (!nextProps.settings.getIn(['collapsed', 'enabled'])) {
-      if (this.state.isExpanded === false) {
-        this.setExpansion(null);
-      }
-    } else if (
-      nextProps.collapse !== this.props.collapse &&
-      nextProps.collapse !== undefined
-    ) this.setExpansion(nextProps.collapse ? false : null);
-  }
-
-  //  When mounting, we just check to see if our status should be collapsed,
-  //  and collapse it if so. We don't need to worry about whether collapsing
-  //  is enabled here, because `setExpansion()` already takes that into
-  //  account.
-
-  //  The cases where a status should be collapsed are:
-  //
-  //   -  The `collapse` prop has been set to `true`
-  //   -  The user has decided in local settings to collapse all statuses.
-  //   -  The user has decided to collapse all notifications ('muted'
-  //      statuses).
-  //   -  The user has decided to collapse long statuses and the status is
-  //      over 400px (without media, or 650px with).
-  //   -  The status is a reply and the user has decided to collapse all
-  //      replies.
-  //   -  The status contains media and the user has decided to collapse all
-  //      statuses with media.
-  //   -  The status is a reblog the user has decided to collapse all
-  //      statuses which are reblogs.
-  componentDidMount () {
-    const { node } = this;
-    const {
-      status,
-      settings,
-      collapse,
-      muted,
-      prepend,
-    } = this.props;
-    const autoCollapseSettings = settings.getIn(['collapsed', 'auto']);
-
-    if (function () {
-      switch (true) {
-      case collapse:
-      case autoCollapseSettings.get('all'):
-      case autoCollapseSettings.get('notifications') && muted:
-      case autoCollapseSettings.get('lengthy') && node.clientHeight > (
-        status.get('media_attachments').size && !muted ? 650 : 400
-      ):
-      case autoCollapseSettings.get('reblogs') && prepend === 'reblogged_by':
-      case autoCollapseSettings.get('replies') && status.get('in_reply_to_id', null) !== null:
-      case autoCollapseSettings.get('media') && !(status.get('spoiler_text').length) && status.get('media_attachments').size:
-        return true;
-      default:
-        return false;
-      }
-    }()) this.setExpansion(false);
-  }
-
-  //  `setExpansion()` sets the value of `isExpanded` in our state. It takes
-  //  one argument, `value`, which gives the desired value for `isExpanded`.
-  //  The default for this argument is `null`.
-
-  //  `setExpansion()` automatically checks for us whether toot collapsing
-  //  is enabled, so we don't have to.
-  setExpansion = (value) => {
-    switch (true) {
-    case value === undefined || value === null:
-      this.setState({ isExpanded: null });
-      break;
-    case !value && this.props.settings.getIn(['collapsed', 'enabled']):
-      this.setState({ isExpanded: false });
-      break;
-    case !!value:
-      this.setState({ isExpanded: true });
-      break;
-    }
-  }
-
-  //  `parseClick()` takes a click event and responds appropriately.
-  //  If our status is collapsed, then clicking on it should uncollapse it.
-  //  If `Shift` is held, then clicking on it should collapse it.
-  //  Otherwise, we open the url handed to us in `destination`, if
-  //  applicable.
-  parseClick = (e, destination) => {
-    const { router } = this.context;
-    const { status } = this.props;
-    const { isExpanded } = this.state;
-    if (!router) return;
-    if (destination === undefined) {
-      destination = `/statuses/${
-        status.getIn(['reblog', 'id'], status.get('id'))
-      }`;
-    }
-    if (e.button === 0) {
-      if (isExpanded === false) this.setExpansion(null);
-      else if (e.shiftKey) {
-        this.setExpansion(false);
-        document.getSelection().removeAllRanges();
-      } else router.history.push(destination);
-      e.preventDefault();
-    }
-  }
-
-  handleAccountClick = (e) => {
-    if (this.context.router && e.button === 0) {
-      const id = e.currentTarget.getAttribute('data-id');
-      e.preventDefault();
-      this.context.router.history.push(`/accounts/${id}`);
-    }
-  }
-
-  handleExpandedToggle = () => {
-    this.setExpansion(this.state.isExpanded || !this.props.status.get('spoiler') ? null : true);
-  };
-
-  handleOpenVideo = startTime => {
-    this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime);
-  }
-
-  handleHotkeyReply = e => {
-    e.preventDefault();
-    this.props.onReply(this.props.status, this.context.router.history);
-  }
-
-  handleHotkeyFavourite = () => {
-    this.props.onFavourite(this.props.status);
-  }
-
-  handleHotkeyBoost = e => {
-    this.props.onReblog(this.props.status, e);
-  }
-
-  handleHotkeyMention = e => {
-    e.preventDefault();
-    this.props.onMention(this.props.status.get('account'), this.context.router.history);
-  }
-
-  handleHotkeyOpen = () => {
-    this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
-  }
-
-  handleHotkeyOpenProfile = () => {
-    this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
-  }
-
-  handleHotkeyMoveUp = () => {
-    this.props.onMoveUp(this.props.status.get('id'));
-  }
-
-  handleHotkeyMoveDown = () => {
-    this.props.onMoveDown(this.props.status.get('id'));
-  }
-
-  handleRef = c => {
-    this.node = c;
-  }
-
-  renderLoadingMediaGallery () {
-    return <div className='media_gallery' style={{ height: '110px' }} />;
-  }
-
-  renderLoadingVideoPlayer () {
-    return <div className='media-spoiler-video' style={{ height: '110px' }} />;
-  }
-
-  render () {
-    const {
-      handleRef,
-      parseClick,
-      setExpansion,
-    } = this;
-    const { router } = this.context;
-    const {
-      status,
-      account,
-      settings,
-      collapsed,
-      muted,
-      prepend,
-      intersectionObserverWrapper,
-      onOpenVideo,
-      onOpenMedia,
-      notification,
-      hidden,
-      ...other
-    } = this.props;
-    const { isExpanded } = this.state;
-    let background = null;
-    let attachments = null;
-    let media = null;
-    let mediaIcon = null;
-
-    if (status === null) {
-      return null;
-    }
-
-    if (hidden) {
-      return (
-        <div
-          ref={this.handleRef}
-          data-id={status.get('id')}
-          style={{
-            height: `${this.height}px`,
-            opacity: 0,
-            overflow: 'hidden',
-          }}
-        >
-          {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
-          {' '}
-          {status.get('content')}
-        </div>
-      );
-    }
-
-    //  If user backgrounds for collapsed statuses are enabled, then we
-    //  initialize our background accordingly. This will only be rendered if
-    //  the status is collapsed.
-    if (settings.getIn(['collapsed', 'backgrounds', 'user_backgrounds'])) {
-      background = status.getIn(['account', 'header']);
-    }
-
-    //  This handles our media attachments. Note that we don't show media on
-    //  muted (notification) statuses. If the media type is unknown, then we
-    //  simply ignore it.
-
-    //  After we have generated our appropriate media element and stored it in
-    //  `media`, we snatch the thumbnail to use as our `background` if media
-    //  backgrounds for collapsed statuses are enabled.
-    attachments = status.get('media_attachments');
-    if (attachments.size > 0 && !muted) {
-      if (attachments.some(item => item.get('type') === 'unknown')) {  //  Media type is 'unknown'
-        /*  Do nothing  */
-      } else if (attachments.getIn([0, 'type']) === 'video') {  //  Media type is 'video'
-        const video = status.getIn(['media_attachments', 0]);
-
-        media = (
-          <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
-            {Component => <Component
-              preview={video.get('preview_url')}
-              src={video.get('url')}
-              sensitive={status.get('sensitive')}
-              letterbox={settings.getIn(['media', 'letterbox'])}
-              fullwidth={settings.getIn(['media', 'fullwidth'])}
-              onOpenVideo={this.handleOpenVideo}
-            />}
-          </Bundle>
-        );
-        mediaIcon = 'video-camera';
-      } else {  //  Media type is 'image' or 'gifv'
-        media = (
-          <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
-            {Component => (
-              <Component
-                media={attachments}
-                sensitive={status.get('sensitive')}
-                letterbox={settings.getIn(['media', 'letterbox'])}
-                fullwidth={settings.getIn(['media', 'fullwidth'])}
-                onOpenMedia={this.props.onOpenMedia}
-              />
-            )}
-          </Bundle>
-        );
-        mediaIcon = 'picture-o';
-      }
-
-      if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) {
-        background = attachments.getIn([0, 'preview_url']);
-      }
-    }
-
-    //  Here we prepare extra data-* attributes for CSS selectors.
-    //  Users can use those for theming, hiding avatars etc via UserStyle
-    const selectorAttribs = {
-      'data-status-by': `@${status.getIn(['account', 'acct'])}`,
-    };
-
-    if (prepend && account) {
-      const notifKind = {
-        favourite: 'favourited',
-        reblog: 'boosted',
-        reblogged_by: 'boosted',
-      }[prepend];
-
-      selectorAttribs[`data-${notifKind}-by`] = `@${account.get('acct')}`;
-    }
-
-    const handlers = {
-      reply: this.handleHotkeyReply,
-      favourite: this.handleHotkeyFavourite,
-      boost: this.handleHotkeyBoost,
-      mention: this.handleHotkeyMention,
-      open: this.handleHotkeyOpen,
-      openProfile: this.handleHotkeyOpenProfile,
-      moveUp: this.handleHotkeyMoveUp,
-      moveDown: this.handleHotkeyMoveDown,
-    };
-
-    return (
-      <HotKeys handlers={handlers}>
-        <div
-          className={
-            `status${
-              muted ? ' muted' : ''
-            } status-${status.get('visibility')}${
-              isExpanded === false ? ' collapsed' : ''
-            }${
-              isExpanded === false && background ? ' has-background' : ''
-            }${
-              this.state.markedForDelete ? ' marked-for-delete' : ''
-            }`
-          }
-          style={{
-            backgroundImage: (
-              isExpanded === false && background ?
-              `url(${background})` :
-              'none'
-            ),
-          }}
-          {...selectorAttribs}
-          ref={handleRef}
-        >
-          {prepend && account ? (
-            <StatusPrepend
-              type={prepend}
-              account={account}
-              parseClick={parseClick}
-              notificationId={this.props.notificationId}
-            />
-          ) : null}
-          <StatusHeader
-            status={status}
-            friend={account}
-            mediaIcon={mediaIcon}
-            collapsible={settings.getIn(['collapsed', 'enabled'])}
-            collapsed={isExpanded === false}
-            parseClick={parseClick}
-            setExpansion={setExpansion}
-          />
-          <StatusContent
-            status={status}
-            media={media}
-            mediaIcon={mediaIcon}
-            expanded={isExpanded}
-            setExpansion={setExpansion}
-            parseClick={parseClick}
-            disabled={!router}
-          />
-          {isExpanded !== false ? (
-            <StatusActionBar
-              {...other}
-              status={status}
-              account={status.get('account')}
-            />
-          ) : null}
-          {notification ? (
-            <NotificationOverlayContainer
-              notification={notification}
-            />
-          ) : null}
-        </div>
-      </HotKeys>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/status_action_bar.js b/app/javascript/themes/glitch/components/status_action_bar.js
deleted file mode 100644
index 9d615ed7c..000000000
--- a/app/javascript/themes/glitch/components/status_action_bar.js
+++ /dev/null
@@ -1,188 +0,0 @@
-//  THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !!
-//  SEE INSTEAD : glitch/components/status/action_bar
-
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
-import IconButton from './icon_button';
-import DropdownMenuContainer from 'themes/glitch/containers/dropdown_menu_container';
-import { defineMessages, injectIntl } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { me } from 'themes/glitch/util/initial_state';
-import RelativeTimestamp from './relative_timestamp';
-
-const messages = defineMessages({
-  delete: { id: 'status.delete', defaultMessage: 'Delete' },
-  mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
-  mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
-  block: { id: 'account.block', defaultMessage: 'Block @{name}' },
-  reply: { id: 'status.reply', defaultMessage: 'Reply' },
-  share: { id: 'status.share', defaultMessage: 'Share' },
-  more: { id: 'status.more', defaultMessage: 'More' },
-  replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
-  reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
-  cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
-  favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
-  open: { id: 'status.open', defaultMessage: 'Expand this status' },
-  report: { id: 'status.report', defaultMessage: 'Report @{name}' },
-  muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
-  unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
-  pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
-  unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
-  embed: { id: 'status.embed', defaultMessage: 'Embed' },
-});
-
-@injectIntl
-export default class StatusActionBar extends ImmutablePureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
-  static propTypes = {
-    status: ImmutablePropTypes.map.isRequired,
-    onReply: PropTypes.func,
-    onFavourite: PropTypes.func,
-    onReblog: PropTypes.func,
-    onDelete: PropTypes.func,
-    onMention: PropTypes.func,
-    onMute: PropTypes.func,
-    onBlock: PropTypes.func,
-    onReport: PropTypes.func,
-    onEmbed: PropTypes.func,
-    onMuteConversation: PropTypes.func,
-    onPin: PropTypes.func,
-    withDismiss: PropTypes.bool,
-    intl: PropTypes.object.isRequired,
-  };
-
-  // Avoid checking props that are functions (and whose equality will always
-  // evaluate to false. See react-immutable-pure-component for usage.
-  updateOnProps = [
-    'status',
-    'withDismiss',
-  ]
-
-  handleReplyClick = () => {
-    this.props.onReply(this.props.status, this.context.router.history);
-  }
-
-  handleShareClick = () => {
-    navigator.share({
-      text: this.props.status.get('search_index'),
-      url: this.props.status.get('url'),
-    });
-  }
-
-  handleFavouriteClick = () => {
-    this.props.onFavourite(this.props.status);
-  }
-
-  handleReblogClick = (e) => {
-    this.props.onReblog(this.props.status, e);
-  }
-
-  handleDeleteClick = () => {
-    this.props.onDelete(this.props.status);
-  }
-
-  handlePinClick = () => {
-    this.props.onPin(this.props.status);
-  }
-
-  handleMentionClick = () => {
-    this.props.onMention(this.props.status.get('account'), this.context.router.history);
-  }
-
-  handleMuteClick = () => {
-    this.props.onMute(this.props.status.get('account'));
-  }
-
-  handleBlockClick = () => {
-    this.props.onBlock(this.props.status.get('account'));
-  }
-
-  handleOpen = () => {
-    this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
-  }
-
-  handleEmbed = () => {
-    this.props.onEmbed(this.props.status);
-  }
-
-  handleReport = () => {
-    this.props.onReport(this.props.status);
-  }
-
-  handleConversationMuteClick = () => {
-    this.props.onMuteConversation(this.props.status);
-  }
-
-  render () {
-    const { status, intl, withDismiss } = this.props;
-
-    const mutingConversation = status.get('muted');
-    const anonymousAccess    = !me;
-    const publicStatus       = ['public', 'unlisted'].includes(status.get('visibility'));
-
-    let menu = [];
-    let reblogIcon = 'retweet';
-    let replyIcon;
-    let replyTitle;
-
-    menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
-
-    if (publicStatus) {
-      menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
-    }
-
-    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);
-    }
-
-    if (status.getIn(['account', 'id']) === me) {
-      if (publicStatus) {
-        menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
-      }
-
-      menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
-    } else {
-      menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
-      menu.push(null);
-      menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
-      menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
-      menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
-    }
-
-    if (status.get('in_reply_to_id', null) === null) {
-      replyIcon = 'reply';
-      replyTitle = intl.formatMessage(messages.reply);
-    } else {
-      replyIcon = 'reply-all';
-      replyTitle = intl.formatMessage(messages.replyAll);
-    }
-
-    const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && (
-      <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
-    );
-
-    return (
-      <div className='status__action-bar'>
-        <IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
-        <IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
-        <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
-        {shareButton}
-
-        <div className='status__action-bar-dropdown'>
-          <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} />
-        </div>
-
-        <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/status_content.js b/app/javascript/themes/glitch/components/status_content.js
deleted file mode 100644
index 3eba6eaa0..000000000
--- a/app/javascript/themes/glitch/components/status_content.js
+++ /dev/null
@@ -1,245 +0,0 @@
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
-import { isRtl } from 'themes/glitch/util/rtl';
-import { FormattedMessage } from 'react-intl';
-import Permalink from './permalink';
-import classnames from 'classnames';
-
-export default class StatusContent extends React.PureComponent {
-
-  static propTypes = {
-    status: ImmutablePropTypes.map.isRequired,
-    expanded: PropTypes.bool,
-    setExpansion: PropTypes.func,
-    media: PropTypes.element,
-    mediaIcon: PropTypes.string,
-    parseClick: PropTypes.func,
-    disabled: PropTypes.bool,
-  };
-
-  state = {
-    hidden: true,
-  };
-
-  _updateStatusLinks () {
-    const node  = this.node;
-    const links = node.querySelectorAll('a');
-
-    for (var i = 0; i < links.length; ++i) {
-      let link = links[i];
-      if (link.classList.contains('status-link')) {
-        continue;
-      }
-      link.classList.add('status-link');
-
-      let mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
-
-      if (mention) {
-        link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
-        link.setAttribute('title', mention.get('acct'));
-      } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
-        link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
-      } else {
-        link.addEventListener('click', this.onLinkClick.bind(this), false);
-        link.setAttribute('title', link.href);
-      }
-
-      link.setAttribute('target', '_blank');
-      link.setAttribute('rel', 'noopener');
-    }
-  }
-
-  componentDidMount () {
-    this._updateStatusLinks();
-  }
-
-  componentDidUpdate () {
-    this._updateStatusLinks();
-  }
-
-  onLinkClick = (e) => {
-    if (this.props.expanded === false) {
-      if (this.props.parseClick) this.props.parseClick(e);
-    }
-  }
-
-  onMentionClick = (mention, e) => {
-    if (this.props.parseClick) {
-      this.props.parseClick(e, `/accounts/${mention.get('id')}`);
-    }
-  }
-
-  onHashtagClick = (hashtag, e) => {
-    hashtag = hashtag.replace(/^#/, '').toLowerCase();
-
-    if (this.props.parseClick) {
-      this.props.parseClick(e, `/timelines/tag/${hashtag}`);
-    }
-  }
-
-  handleMouseDown = (e) => {
-    this.startXY = [e.clientX, e.clientY];
-  }
-
-  handleMouseUp = (e) => {
-    const { parseClick } = this.props;
-
-    if (!this.startXY) {
-      return;
-    }
-
-    const [ startX, startY ] = this.startXY;
-    const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
-
-    if (e.target.localName === 'button' || e.target.localName === 'a' || (e.target.parentNode && (e.target.parentNode.localName === 'button' || e.target.parentNode.localName === 'a'))) {
-      return;
-    }
-
-    if (deltaX + deltaY < 5 && e.button === 0 && parseClick) {
-      parseClick(e);
-    }
-
-    this.startXY = null;
-  }
-
-  handleSpoilerClick = (e) => {
-    e.preventDefault();
-
-    if (this.props.setExpansion) {
-      this.props.setExpansion(this.props.expanded ? null : true);
-    } else {
-      this.setState({ hidden: !this.state.hidden });
-    }
-  }
-
-  setRef = (c) => {
-    this.node = c;
-  }
-
-  render () {
-    const {
-      status,
-      media,
-      mediaIcon,
-      parseClick,
-      disabled,
-    } = this.props;
-
-    const hidden = this.props.setExpansion ? !this.props.expanded : this.state.hidden;
-
-    const content = { __html: status.get('contentHtml') };
-    const spoilerContent = { __html: status.get('spoilerHtml') };
-    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,
-    });
-
-    if (isRtl(status.get('search_index'))) {
-      directionStyle.direction = 'rtl';
-    }
-
-    if (status.get('spoiler_text').length > 0) {
-      let mentionsPlaceholder = '';
-
-      const mentionLinks = status.get('mentions').map(item => (
-        <Permalink
-          to={`/accounts/${item.get('id')}`}
-          href={item.get('url')}
-          key={item.get('id')}
-          className='mention'
-        >
-          @<span>{item.get('username')}</span>
-        </Permalink>
-      )).reduce((aggregate, item) => [...aggregate, item, ' '], []);
-
-      const toggleText = hidden ? [
-        <FormattedMessage
-          id='status.show_more'
-          defaultMessage='Show more'
-          key='0'
-        />,
-        mediaIcon ? (
-          <i
-            className={
-              `fa fa-fw fa-${mediaIcon} status__content__spoiler-icon`
-            }
-            aria-hidden='true'
-            key='1'
-          />
-        ) : null,
-      ] : [
-        <FormattedMessage
-          id='status.show_less'
-          defaultMessage='Show less'
-          key='0'
-        />,
-      ];
-
-      if (hidden) {
-        mentionsPlaceholder = <div>{mentionLinks}</div>;
-      }
-
-      return (
-        <div className={classNames} tabIndex='0'>
-          <p
-            style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}
-            onMouseDown={this.handleMouseDown}
-            onMouseUp={this.handleMouseUp}
-          >
-            <span dangerouslySetInnerHTML={spoilerContent} />
-            {' '}
-            <button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>
-              {toggleText}
-            </button>
-          </p>
-
-          {mentionsPlaceholder}
-
-          <div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}>
-            <div
-              ref={this.setRef}
-              style={directionStyle}
-              tabIndex={!hidden ? 0 : null}
-              onMouseDown={this.handleMouseDown}
-              onMouseUp={this.handleMouseUp}
-              dangerouslySetInnerHTML={content}
-            />
-            {media}
-          </div>
-
-        </div>
-      );
-    } else if (parseClick) {
-      return (
-        <div
-          className={classNames}
-          style={directionStyle}
-          tabIndex='0'
-        >
-          <div
-            ref={this.setRef}
-            onMouseDown={this.handleMouseDown}
-            onMouseUp={this.handleMouseUp}
-            dangerouslySetInnerHTML={content}
-            tabIndex='0'
-          />
-          {media}
-        </div>
-      );
-    } else {
-      return (
-        <div
-          className='status__content'
-          style={directionStyle}
-          tabIndex='0'
-        >
-          <div ref={this.setRef} dangerouslySetInnerHTML={content} tabIndex='0' />
-          {media}
-        </div>
-      );
-    }
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/status_header.js b/app/javascript/themes/glitch/components/status_header.js
deleted file mode 100644
index bfa996cd5..000000000
--- a/app/javascript/themes/glitch/components/status_header.js
+++ /dev/null
@@ -1,120 +0,0 @@
-//  Package imports.
-import React from 'react';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { defineMessages, injectIntl } from 'react-intl';
-
-//  Mastodon imports.
-import Avatar from './avatar';
-import AvatarOverlay from './avatar_overlay';
-import DisplayName from './display_name';
-import IconButton from './icon_button';
-import VisibilityIcon from './status_visibility_icon';
-
-//  Messages for use with internationalization stuff.
-const messages = defineMessages({
-  collapse: { id: 'status.collapse', defaultMessage: 'Collapse' },
-  uncollapse: { id: 'status.uncollapse', defaultMessage: 'Uncollapse' },
-  public: { id: 'privacy.public.short', defaultMessage: 'Public' },
-  unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
-  private: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
-  direct: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
-});
-
-@injectIntl
-export default class StatusHeader extends React.PureComponent {
-
-  static propTypes = {
-    status: ImmutablePropTypes.map.isRequired,
-    friend: ImmutablePropTypes.map,
-    mediaIcon: PropTypes.string,
-    collapsible: PropTypes.bool,
-    collapsed: PropTypes.bool,
-    parseClick: PropTypes.func.isRequired,
-    setExpansion: PropTypes.func.isRequired,
-    intl: PropTypes.object.isRequired,
-  };
-
-  //  Handles clicks on collapsed button
-  handleCollapsedClick = (e) => {
-    const { collapsed, setExpansion } = this.props;
-    if (e.button === 0) {
-      setExpansion(collapsed ? null : false);
-      e.preventDefault();
-    }
-  }
-
-  //  Handles clicks on account name/image
-  handleAccountClick = (e) => {
-    const { status, parseClick } = this.props;
-    parseClick(e, `/accounts/${+status.getIn(['account', 'id'])}`);
-  }
-
-  //  Rendering.
-  render () {
-    const {
-      status,
-      friend,
-      mediaIcon,
-      collapsible,
-      collapsed,
-      intl,
-    } = this.props;
-
-    const account = status.get('account');
-
-    return (
-      <header className='status__info'>
-        <a
-          href={account.get('url')}
-          target='_blank'
-          className='status__avatar'
-          onClick={this.handleAccountClick}
-        >
-          {
-            friend ? (
-              <AvatarOverlay account={account} friend={friend} />
-            ) : (
-              <Avatar account={account} size={48} />
-            )
-          }
-        </a>
-        <a
-          href={account.get('url')}
-          target='_blank'
-          className='status__display-name'
-          onClick={this.handleAccountClick}
-        >
-          <DisplayName account={account} />
-        </a>
-        <div className='status__info__icons'>
-          {mediaIcon ? (
-            <i
-              className={`fa fa-fw fa-${mediaIcon}`}
-              aria-hidden='true'
-            />
-          ) : null}
-          {(
-            <VisibilityIcon visibility={status.get('visibility')} />
-          )}
-          {collapsible ? (
-            <IconButton
-              className='status__collapse-button'
-              animate flip
-              active={collapsed}
-              title={
-                collapsed ?
-                intl.formatMessage(messages.uncollapse) :
-                intl.formatMessage(messages.collapse)
-              }
-              icon='angle-double-up'
-              onClick={this.handleCollapsedClick}
-            />
-          ) : null}
-        </div>
-
-      </header>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/status_list.js b/app/javascript/themes/glitch/components/status_list.js
deleted file mode 100644
index ddb1354c6..000000000
--- a/app/javascript/themes/glitch/components/status_list.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import PropTypes from 'prop-types';
-import StatusContainer from 'themes/glitch/containers/status_container';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import ScrollableList from './scrollable_list';
-
-export default class StatusList extends ImmutablePureComponent {
-
-  static propTypes = {
-    scrollKey: PropTypes.string.isRequired,
-    statusIds: ImmutablePropTypes.list.isRequired,
-    onScrollToBottom: PropTypes.func,
-    onScrollToTop: PropTypes.func,
-    onScroll: PropTypes.func,
-    trackScroll: PropTypes.bool,
-    shouldUpdateScroll: PropTypes.func,
-    isLoading: PropTypes.bool,
-    hasMore: PropTypes.bool,
-    prepend: PropTypes.node,
-    emptyMessage: PropTypes.node,
-  };
-
-  static defaultProps = {
-    trackScroll: true,
-  };
-
-  handleMoveUp = id => {
-    const elementIndex = this.props.statusIds.indexOf(id) - 1;
-    this._selectChild(elementIndex);
-  }
-
-  handleMoveDown = id => {
-    const elementIndex = this.props.statusIds.indexOf(id) + 1;
-    this._selectChild(elementIndex);
-  }
-
-  _selectChild (index) {
-    const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
-
-    if (element) {
-      element.focus();
-    }
-  }
-
-  setRef = c => {
-    this.node = c;
-  }
-
-  render () {
-    const { statusIds, ...other } = this.props;
-    const { isLoading } = other;
-
-    const scrollableContent = (isLoading || statusIds.size > 0) ? (
-      statusIds.map((statusId) => (
-        <StatusContainer
-          key={statusId}
-          id={statusId}
-          onMoveUp={this.handleMoveUp}
-          onMoveDown={this.handleMoveDown}
-        />
-      ))
-    ) : null;
-
-    return (
-      <ScrollableList {...other} ref={this.setRef}>
-        {scrollableContent}
-      </ScrollableList>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/status_prepend.js b/app/javascript/themes/glitch/components/status_prepend.js
deleted file mode 100644
index bd2559e46..000000000
--- a/app/javascript/themes/glitch/components/status_prepend.js
+++ /dev/null
@@ -1,83 +0,0 @@
-//  Package imports  //
-import React from 'react';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { FormattedMessage } from 'react-intl';
-
-export default class StatusPrepend extends React.PureComponent {
-
-  static propTypes = {
-    type: PropTypes.string.isRequired,
-    account: ImmutablePropTypes.map.isRequired,
-    parseClick: PropTypes.func.isRequired,
-    notificationId: PropTypes.number,
-  };
-
-  handleClick = (e) => {
-    const { account, parseClick } = this.props;
-    parseClick(e, `/accounts/${+account.get('id')}`);
-  }
-
-  Message = () => {
-    const { type, account } = this.props;
-    let link = (
-      <a
-        onClick={this.handleClick}
-        href={account.get('url')}
-        className='status__display-name'
-      >
-        <b
-          dangerouslySetInnerHTML={{
-            __html : account.get('display_name_html') || account.get('username'),
-          }}
-        />
-      </a>
-    );
-    switch (type) {
-    case 'reblogged_by':
-      return (
-        <FormattedMessage
-          id='status.reblogged_by'
-          defaultMessage='{name} boosted'
-          values={{ name : link }}
-        />
-      );
-    case 'favourite':
-      return (
-        <FormattedMessage
-          id='notification.favourite'
-          defaultMessage='{name} favourited your status'
-          values={{ name : link }}
-        />
-      );
-    case 'reblog':
-      return (
-        <FormattedMessage
-          id='notification.reblog'
-          defaultMessage='{name} boosted your status'
-          values={{ name : link }}
-        />
-      );
-    }
-    return null;
-  }
-
-  render () {
-    const { Message } = this;
-    const { type } = this.props;
-
-    return !type ? null : (
-      <aside className={type === 'reblogged_by' ? 'status__prepend' : 'notification__message'}>
-        <div className={type === 'reblogged_by' ? 'status__prepend-icon-wrapper' : 'notification__favourite-icon-wrapper'}>
-          <i
-            className={`fa fa-fw fa-${
-              type === 'favourite' ? 'star star-icon' : 'retweet'
-            } status__prepend-icon`}
-          />
-        </div>
-        <Message />
-      </aside>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/components/status_visibility_icon.js b/app/javascript/themes/glitch/components/status_visibility_icon.js
deleted file mode 100644
index 017b69cbb..000000000
--- a/app/javascript/themes/glitch/components/status_visibility_icon.js
+++ /dev/null
@@ -1,48 +0,0 @@
-//  Package imports  //
-import React from 'react';
-import PropTypes from 'prop-types';
-import { defineMessages, injectIntl } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-
-const messages = defineMessages({
-  public: { id: 'privacy.public.short', defaultMessage: 'Public' },
-  unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
-  private: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
-  direct: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
-});
-
-@injectIntl
-export default class VisibilityIcon extends ImmutablePureComponent {
-
-  static propTypes = {
-    visibility: PropTypes.string,
-    intl: PropTypes.object.isRequired,
-    withLabel: PropTypes.bool,
-  };
-
-  render() {
-    const { withLabel, visibility, intl } = this.props;
-
-    const visibilityClass = {
-      public: 'globe',
-      unlisted: 'unlock-alt',
-      private: 'lock',
-      direct: 'envelope',
-    }[visibility];
-
-    const label = intl.formatMessage(messages[visibility]);
-
-    const icon = (<i
-      className={`status__visibility-icon fa fa-fw fa-${visibilityClass}`}
-      title={label}
-      aria-hidden='true'
-    />);
-
-    if (withLabel) {
-      return (<span style={{ whiteSpace: 'nowrap' }}>{icon} {label}</span>);
-    } else {
-      return icon;
-    }
-  }
-
-}