about summary refs log tree commit diff
diff options
context:
space:
mode:
authorkibigo! <marrus-sh@users.noreply.github.com>2017-06-28 22:00:54 -0700
committerkibigo! <marrus-sh@users.noreply.github.com>2017-06-28 22:00:54 -0700
commit595c6de32c052f2a5e21307a0e43a6bd5d4b5c88 (patch)
tree6d12eb42ae89bf859803c24f57248ea2ad30451e
parent6cbbdc805f68b0318c9b3a7c01f4bdaefee5a2fa (diff)
Added App Setttings Modal
-rw-r--r--app/javascript/mastodon/actions/local_settings.js2
-rw-r--r--app/javascript/mastodon/components/status.js40
-rw-r--r--app/javascript/mastodon/containers/mastodon.js4
-rw-r--r--app/javascript/mastodon/containers/status_container.js1
-rw-r--r--app/javascript/mastodon/features/compose/index.js11
-rw-r--r--app/javascript/mastodon/features/getting_started/index.js50
-rw-r--r--app/javascript/mastodon/features/notifications/components/notification.js15
-rw-r--r--app/javascript/mastodon/features/notifications/containers/notification_container.js1
-rw-r--r--app/javascript/mastodon/features/ui/components/column_link.js12
-rw-r--r--app/javascript/mastodon/features/ui/components/modal_root.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/settings_modal.js212
-rw-r--r--app/javascript/mastodon/features/ui/containers/settings_modal_container.js19
-rw-r--r--app/javascript/mastodon/features/ui/index.js8
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json95
-rw-r--r--app/javascript/mastodon/locales/en.json16
-rw-r--r--app/javascript/mastodon/reducers/index.js4
-rw-r--r--app/javascript/mastodon/reducers/local_settings.js19
-rw-r--r--app/javascript/styles/components.scss130
-rw-r--r--app/javascript/styles/custom.scss14
19 files changed, 561 insertions, 94 deletions
diff --git a/app/javascript/mastodon/actions/local_settings.js b/app/javascript/mastodon/actions/local_settings.js
index 742a1eec2..18e0c245c 100644
--- a/app/javascript/mastodon/actions/local_settings.js
+++ b/app/javascript/mastodon/actions/local_settings.js
@@ -14,7 +14,7 @@ export function changeLocalSetting(key, value) {
 
 export function saveLocalSettings() {
   return (_, getState) => {
-    const localSettings = getState().get('localSettings').toJS();
+    const localSettings = getState().get('local_settings').toJS();
     localStorage.setItem('mastodon-settings', JSON.stringify(localSettings));
   };
 };
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index 75e8c9640..33e4a25e4 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -25,6 +25,7 @@ export default class StatusOrReblog extends ImmutablePureComponent {
   static propTypes = {
     status: ImmutablePropTypes.map,
     account: ImmutablePropTypes.map,
+    settings: ImmutablePropTypes.map,
     wrapped: PropTypes.bool,
     onReply: PropTypes.func,
     onFavourite: PropTypes.func,
@@ -47,6 +48,7 @@ export default class StatusOrReblog extends ImmutablePureComponent {
   updateOnProps = [
     'status',
     'account',
+    'settings',
     'wrapped',
     'me',
     'boostModal',
@@ -97,6 +99,7 @@ class Status extends ImmutablePureComponent {
   static propTypes = {
     status: ImmutablePropTypes.map,
     account: ImmutablePropTypes.map,
+    settings: ImmutablePropTypes.map,
     wrapped: PropTypes.bool,
     onReply: PropTypes.func,
     onFavourite: PropTypes.func,
@@ -126,6 +129,7 @@ class Status extends ImmutablePureComponent {
   updateOnProps = [
     'status',
     'account',
+    'settings',
     'wrapped',
     'me',
     'boostModal',
@@ -140,7 +144,8 @@ class Status extends ImmutablePureComponent {
   ]
 
   componentWillReceiveProps (nextProps) {
-    if (nextProps.collapse !== this.props.collapse && nextProps.collapse !== undefined) this.setState({ isCollapsed: !!nextProps.collapse });
+    if (!nextProps.settings.getIn(['collapsed', 'enabled'])) this.collapse(false);
+    else if (nextProps.collapse !== this.props.collapse && nextProps.collapse !== undefined) this.collapse(this.props.collapse);
   }
 
   shouldComponentUpdate (nextProps, nextState) {
@@ -165,8 +170,13 @@ class Status extends ImmutablePureComponent {
   componentDidMount () {
     const node = this.node;
 
-    if (this.props.collapse !== undefined) this.setState({ isCollapsed: !!this.props.collapse });
-    else if (node.clientHeight > 400) this.setState({ isCollapsed: true });
+    const { collapse, settings, status } = this.props;
+
+    if (collapse !== undefined) this.collapse(collapse);
+    else if (settings.getIn(['collapsed', 'auto', 'all'])) this.collapse();
+    else if (settings.getIn(['collapsed', 'auto', 'lengthy']) && node.clientHeight > 400) this.collapse();
+    else if (settings.getIn(['collapsed', 'auto', 'replies']) && status.get('in_reply_to_id', null) !== null) this.collapse();
+    else if (settings.getIn(['collapsed', 'auto', 'media']) && status.get('media_attachments').size > 0) this.collapse();
 
     if (!this.props.intersectionObserverWrapper) {
       // TODO: enable IntersectionObserver optimization for notification statuses.
@@ -186,6 +196,11 @@ class Status extends ImmutablePureComponent {
     this.componentMounted = false;
   }
 
+  collapse = (collapsedOrNot) => {
+    if (collapsedOrNot === undefined) collapsedOrNot = true;
+    if (this.props.settings.getIn(['collapsed', 'enabled'])) this.setState({ isCollapsed: !!collapsedOrNot });
+  }
+
   handleIntersection = (entry) => {
     // Edge 15 doesn't support isIntersecting, but we can infer it
     // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12156111/
@@ -247,20 +262,23 @@ class Status extends ImmutablePureComponent {
   };
 
   handleCollapsedClick = () => {
-    this.setState({ isCollapsed: !this.state.isCollapsed, isExpanded: false });
+    this.collapse(!this.state.isCollapsed);
+    this.setState({ isExpanded: false });
   }
 
   render () {
     let media = null;
     let mediaType = null;
-    let thumb = null;
     let statusAvatar;
 
     // Exclude intersectionObserverWrapper from `other` variable
     // because intersection is managed in here.
-    const { status, account, intersectionObserverWrapper, intl, ...other } = this.props;
+    const { status, account, settings, intersectionObserverWrapper, intl, ...other } = this.props;
     const { isExpanded, isIntersecting, isHidden, isCollapsed } = this.state;
 
+
+    let background = settings.getIn(['collapsed', 'backgrounds', 'user_backgrounds']) ? status.getIn(['account', 'header']) : null;
+
     if (status === null) {
       return null;
     }
@@ -280,12 +298,12 @@ class Status extends ImmutablePureComponent {
       } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
         media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />;
         mediaType = <i className='fa fa-fw fa-video-camera' aria-hidden='true' />;
-        if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0)) thumb = status.getIn(['media_attachments', 0]).get('preview_url');
       } else {
         media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />;
         mediaType = status.get('media_attachments').size > 1 ? <i className='fa fa-fw fa-th-large' aria-hidden='true' /> : <i className='fa fa-fw fa-picture-o' aria-hidden='true' />;
-        if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0)) thumb = status.getIn(['media_attachments', 0]).get('preview_url');
       }
+
+      if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) background = status.getIn(['media_attachments', 0]).get('preview_url');
     }
 
     if (account === undefined || account === null) {
@@ -295,19 +313,19 @@ class Status extends ImmutablePureComponent {
     }
 
     return (
-      <div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')} ${isCollapsed ? 'status-collapsed' : ''}`} data-id={status.get('id')} ref={this.handleRef} style={{ backgroundImage: thumb && isCollapsed ? 'url(' + thumb + ')' : 'none' }}>
+      <div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')} ${isCollapsed ? 'status-collapsed' : ''}`} data-id={status.get('id')} ref={this.handleRef} style={{ backgroundImage: background && isCollapsed ? 'url(' + background + ')' : 'none' }}>
         <div className='status__info'>
 
           <div className='status__info__icons'>
             {mediaType}
-            <IconButton
+            {settings.getIn(['collapsed', 'enabled']) ? <IconButton
               className='status__collapse-button'
               animate flip
               active={isCollapsed}
               title={isCollapsed ? intl.formatMessage(messages.uncollapse) : intl.formatMessage(messages.collapse)}
               icon='angle-double-up'
               onClick={this.handleCollapsedClick}
-            />
+            /> : null}
           </div>
 
           <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name'>
diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js
index 3468a7944..398f7d243 100644
--- a/app/javascript/mastodon/containers/mastodon.js
+++ b/app/javascript/mastodon/containers/mastodon.js
@@ -25,9 +25,9 @@ addLocaleData(localeData);
 const store = configureStore();
 const initialState = JSON.parse(document.getElementById('initial-state').textContent);
 try {
-  initialState.localSettings = JSON.parse(localStorage.getItem('mastodon-settings'));
+  initialState.local_settings = JSON.parse(localStorage.getItem('mastodon-settings'));
 } catch (e) {
-  initialState.localSettings = {};
+  initialState.local_settings = {};
 }
 store.dispatch(hydrateStore(initialState));
 
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js
index 438ecfe43..bf4ef5532 100644
--- a/app/javascript/mastodon/containers/status_container.js
+++ b/app/javascript/mastodon/containers/status_container.js
@@ -34,6 +34,7 @@ const makeMapStateToProps = () => {
   const mapStateToProps = (state, props) => ({
     status: getStatus(state, props.id),
     me: state.getIn(['meta', 'me']),
+    settings: state.get('local_settings'),
     boostModal: state.getIn(['meta', 'boost_modal']),
     deleteModal: state.getIn(['meta', 'delete_modal']),
     autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js
index 512167193..843dcc96c 100644
--- a/app/javascript/mastodon/features/compose/index.js
+++ b/app/javascript/mastodon/features/compose/index.js
@@ -4,6 +4,7 @@ import NavigationContainer from './containers/navigation_container';
 import PropTypes from 'prop-types';
 import { connect } from 'react-redux';
 import { mountCompose, unmountCompose } from '../../actions/compose';
+import { openModal } from '../../actions/modal';
 import { changeLocalSetting } from '../../actions/local_settings';
 import Link from 'react-router-dom/Link';
 import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
@@ -16,13 +17,13 @@ const messages = defineMessages({
   start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
   public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
   community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
-  preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
+  settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
   logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
 });
 
 const mapStateToProps = state => ({
   showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
-  layout: state.getIn(['localSettings', 'layout']),
+  layout: state.getIn(['local_settings', 'layout']),
 });
 
 @connect(mapStateToProps)
@@ -51,6 +52,10 @@ export default class Compose extends React.PureComponent {
     e.preventDefault();
   }
 
+  openSettings = () => {
+    this.props.dispatch(openModal('SETTINGS', {}));
+  }
+
   render () {
     const { multiColumn, showSearch, intl, layout } = this.props;
 
@@ -62,7 +67,7 @@ export default class Compose extends React.PureComponent {
           <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)}><i role='img' aria-label={intl.formatMessage(messages.start)} className='fa fa-fw fa-asterisk' /></Link>
           <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role='img' aria-label={intl.formatMessage(messages.community)} className='fa fa-fw fa-users' /></Link>
           <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role='img' aria-label={intl.formatMessage(messages.public)} className='fa fa-fw fa-globe' /></Link>
-          <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)}><i role='img' aria-label={intl.formatMessage(messages.preferences)} className='fa fa-fw fa-cog' /></a>
+          <a onClick={this.openSettings} role='button' tabIndex='0' className='drawer__tab' title={intl.formatMessage(messages.settings)}><i role='img' aria-label={intl.formatMessage(messages.settings)} className='fa fa-fw fa-cogs' /></a>
           <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)}><i role='img' aria-label={intl.formatMessage(messages.logout)} className='fa fa-fw fa-sign-out' /></a>
         </div>
       );
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index ac93b3d47..684612b1c 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -4,6 +4,7 @@ import ColumnLink from '../ui/components/column_link';
 import ColumnSubheading from '../ui/components/column_subheading';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import { connect } from 'react-redux';
+import { openModal } from '../../actions/modal';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -17,6 +18,7 @@ const messages = defineMessages({
   settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
   community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
   preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
+  settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
   follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
   sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
   favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
@@ -39,8 +41,13 @@ export default class GettingStarted extends ImmutablePureComponent {
     me: ImmutablePropTypes.map.isRequired,
     columns: ImmutablePropTypes.list,
     multiColumn: PropTypes.bool,
+    dispatch: PropTypes.func.isRequired,
   };
 
+  openSettings = () => {
+    this.props.dispatch(openModal('SETTINGS', {}));
+  }
+
   render () {
     const { intl, me, columns, multiColumn } = this.props;
 
@@ -79,27 +86,30 @@ export default class GettingStarted extends ImmutablePureComponent {
 
     return (
       <Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile>
-        <div className='getting-started__wrapper'>
-          <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />
-          {navItems}
-          <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
-          <ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
-          <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
-          <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
-        </div>
+        <div className='scrollable optionally-scrollable'>
+          <div className='getting-started__wrapper'>
+            <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />
+            {navItems}
+            <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
+            <ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
+            <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
+            <ColumnLink icon='cogs' text={intl.formatMessage(messages.settings)} onClick={this.openSettings} />
+            <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
+          </div>
 
-        <div className='getting-started__footer scrollable optionally-scrollable'>
-          <div className='static-content getting-started'>
-            <p>
-              <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.faq' defaultMessage='FAQ' /></a> • <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.userguide' defaultMessage='User Guide' /></a> • <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.appsshort' defaultMessage='Apps' /></a>
-            </p>
-            <p>
-              <FormattedMessage
-                id='getting_started.open_source_notice'
-                defaultMessage='Glitchsoc is open source software, a friendly fork of {Mastodon}. You can contribute or report issues on GitHub at {github}.'
-                values={{ github: <a href='https://github.com/glitch-soc/mastodon' rel='noopener' target='_blank'>glitch-soc/mastodon</a>, Mastodon: <a href='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>Mastodon</a> }}
-              />
-            </p>
+          <div className='getting-started__footer'>
+            <div className='static-content getting-started'>
+              <p>
+                <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.faq' defaultMessage='FAQ' /></a> • <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.userguide' defaultMessage='User Guide' /></a> • <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' rel='noopener' target='_blank'><FormattedMessage id='getting_started.appsshort' defaultMessage='Apps' /></a>
+              </p>
+              <p>
+                <FormattedMessage
+                  id='getting_started.open_source_notice'
+                  defaultMessage='Glitchsoc is open source software, a friendly fork of {Mastodon}. You can contribute or report issues on GitHub at {github}.'
+                  values={{ github: <a href='https://github.com/glitch-soc/mastodon' rel='noopener' target='_blank'>glitch-soc/mastodon</a>, Mastodon: <a href='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>Mastodon</a> }}
+                />
+              </p>
+            </div>
           </div>
         </div>
       </Column>
diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js
index bf580794d..6c1985174 100644
--- a/app/javascript/mastodon/features/notifications/components/notification.js
+++ b/app/javascript/mastodon/features/notifications/components/notification.js
@@ -12,6 +12,7 @@ export default class Notification extends ImmutablePureComponent {
 
   static propTypes = {
     notification: ImmutablePropTypes.map.isRequired,
+    settings: ImmutablePropTypes.map.isRequired,
   };
 
   renderFollow (account, link) {
@@ -34,7 +35,7 @@ export default class Notification extends ImmutablePureComponent {
     return <StatusContainer id={notification.get('status')} withDismiss />;
   }
 
-  renderFavourite (notification, link) {
+  renderFavourite (notification, settings, link) {
     return (
       <div className='notification notification-favourite'>
         <div className='notification__message'>
@@ -44,12 +45,12 @@ export default class Notification extends ImmutablePureComponent {
           <FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} />
         </div>
 
-        <StatusContainer id={notification.get('status')} account={notification.get('account')} muted collapse withDismiss />
+        <StatusContainer id={notification.get('status')} account={notification.get('account')} muted collapse={settings.getIn(['collapsed', 'auto', 'notifications'])} withDismiss />
       </div>
     );
   }
 
-  renderReblog (notification, link) {
+  renderReblog (notification, settings, link) {
     return (
       <div className='notification notification-reblog'>
         <div className='notification__message'>
@@ -59,13 +60,13 @@ export default class Notification extends ImmutablePureComponent {
           <FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} />
         </div>
 
-        <StatusContainer id={notification.get('status')} account={notification.get('account')} muted collapse withDismiss />
+        <StatusContainer id={notification.get('status')} account={notification.get('account')} muted collapse={settings.getIn(['collapsed', 'auto', 'notifications'])} withDismiss />
       </div>
     );
   }
 
   render () {
-    const { notification } = this.props;
+    const { notification, settings } = this.props;
     const account          = notification.get('account');
     const displayName      = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
     const displayNameHTML  = { __html: emojify(escapeTextContentForBrowser(displayName)) };
@@ -77,9 +78,9 @@ export default class Notification extends ImmutablePureComponent {
     case 'mention':
       return this.renderMention(notification);
     case 'favourite':
-      return this.renderFavourite(notification, link);
+      return this.renderFavourite(notification, settings, link);
     case 'reblog':
-      return this.renderReblog(notification, link);
+      return this.renderReblog(notification, settings, link);
     }
 
     return null;
diff --git a/app/javascript/mastodon/features/notifications/containers/notification_container.js b/app/javascript/mastodon/features/notifications/containers/notification_container.js
index 786222967..66baf98a1 100644
--- a/app/javascript/mastodon/features/notifications/containers/notification_container.js
+++ b/app/javascript/mastodon/features/notifications/containers/notification_container.js
@@ -7,6 +7,7 @@ const makeMapStateToProps = () => {
 
   const mapStateToProps = (state, props) => ({
     notification: getNotification(state, props.notification, props.accountId),
+    settings: state.get('local_settings'),
   });
 
   return mapStateToProps;
diff --git a/app/javascript/mastodon/features/ui/components/column_link.js b/app/javascript/mastodon/features/ui/components/column_link.js
index cbdb6534f..cbc926581 100644
--- a/app/javascript/mastodon/features/ui/components/column_link.js
+++ b/app/javascript/mastodon/features/ui/components/column_link.js
@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import Link from 'react-router-dom/Link';
 
-const ColumnLink = ({ icon, text, to, href, method, hideOnMobile }) => {
+const ColumnLink = ({ icon, text, to, onClick, href, method, hideOnMobile }) => {
   if (href) {
     return (
       <a href={href} className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`} data-method={method}>
@@ -10,13 +10,20 @@ const ColumnLink = ({ icon, text, to, href, method, hideOnMobile }) => {
         {text}
       </a>
     );
-  } else {
+  } else if (to) {
     return (
       <Link to={to} className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`}>
         <i className={`fa fa-fw fa-${icon} column-link__icon`} />
         {text}
       </Link>
     );
+  } else {
+    return (
+      <a onClick={onClick} role='button' tabIndex='0' className={`column-link ${hideOnMobile ? 'hidden-on-mobile' : ''}`} data-method={method}>
+        <i className={`fa fa-fw fa-${icon} column-link__icon`} />
+        {text}
+      </a>
+    );
   }
 };
 
@@ -24,6 +31,7 @@ ColumnLink.propTypes = {
   icon: PropTypes.string.isRequired,
   text: PropTypes.string.isRequired,
   to: PropTypes.string,
+  onClick: PropTypes.func,
   href: PropTypes.string,
   method: PropTypes.string,
   hideOnMobile: PropTypes.bool,
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js
index 48b048eb7..3e3894441 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.js
+++ b/app/javascript/mastodon/features/ui/components/modal_root.js
@@ -6,6 +6,7 @@ import VideoModal from './video_modal';
 import BoostModal from './boost_modal';
 import ConfirmationModal from './confirmation_modal';
 import ReportModal from './report_modal';
+import SettingsModal from '../containers/settings_modal_container';
 import TransitionMotion from 'react-motion/lib/TransitionMotion';
 import spring from 'react-motion/lib/spring';
 
@@ -16,6 +17,7 @@ const MODAL_COMPONENTS = {
   'BOOST': BoostModal,
   'CONFIRM': ConfirmationModal,
   'REPORT': ReportModal,
+  'SETTINGS': SettingsModal,
 };
 
 export default class ModalRoot extends React.PureComponent {
diff --git a/app/javascript/mastodon/features/ui/components/settings_modal.js b/app/javascript/mastodon/features/ui/components/settings_modal.js
new file mode 100644
index 000000000..4fc059ac2
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/settings_modal.js
@@ -0,0 +1,212 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { FormattedMessage } from 'react-intl';
+
+class SettingsItem extends React.PureComponent {
+
+  static propTypes = {
+    settings: ImmutablePropTypes.map.isRequired,
+    item: PropTypes.array.isRequired,
+    id: PropTypes.string.isRequired,
+    dependsOn: PropTypes.array,
+    dependsOnNot: PropTypes.array,
+    children: PropTypes.element.isRequired,
+    onChange: PropTypes.func.isRequired,
+  };
+
+  handleChange = (e) => {
+    const { item, onChange } = this.props;
+    onChange(item, e);
+  }
+
+  render () {
+    const { settings, item, id, children, dependsOn, dependsOnNot } = this.props;
+    let enabled = true;
+
+    if (dependsOn) {
+      for (let i = 0; i < dependsOn.length; i++) {
+        enabled = enabled && settings.getIn(dependsOn[i]);
+      }
+    }
+    if (dependsOnNot) {
+      for (let i = 0; i < dependsOnNot.length; i++) {
+        enabled = enabled && !settings.getIn(dependsOnNot[i]);
+      }
+    }
+
+    return (
+      <label htmlFor={id}>
+        <input
+          id={id}
+          type='checkbox'
+          checked={settings.getIn(item)}
+          onChange={this.handleChange}
+          disabled={!enabled}
+        />
+        {children}
+      </label>
+    );
+  }
+
+}
+
+export default class SettingsModal extends React.PureComponent {
+
+  static propTypes = {
+    settings: ImmutablePropTypes.map.isRequired,
+    toggleSetting: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+  };
+
+  state = {
+    currentIndex: 0,
+  };
+
+  General = () => {
+    return (
+      <div>
+        <h1><FormattedMessage id='settings.general' defaultMessage='General' /></h1>
+        <SettingsItem
+          settings={this.props.settings}
+          item={['stretch']}
+          id='mastodon-settings--stretch'
+          onChange={this.props.toggleSetting}
+        >
+          <FormattedMessage id='settings.wide_view' defaultMessage='Wide view (Desktop mode only)' />
+        </SettingsItem>
+      </div>
+    );
+  }
+
+  CollapsedStatuses = () => {
+    return (
+      <div>
+        <h1><FormattedMessage id='settings.collapsed_statuses' defaultMessage='Collapsed toots' /></h1>
+        <SettingsItem
+          settings={this.props.settings}
+          item={['collapsed', 'enabled']}
+          id='mastodon-settings--collapsed-enabled'
+          onChange={this.props.toggleSetting}
+        >
+          <FormattedMessage id='settings.enable_collapsed' defaultMessage='Enable collapsed toots' />
+        </SettingsItem>
+        <section>
+          <h2><FormattedMessage id='settings.auto_collapse' defaultMessage='Automatic collapsing' /></h2>
+          <SettingsItem
+            settings={this.props.settings}
+            item={['collapsed', 'auto', 'all']}
+            id='mastodon-settings--collapsed-auto-all'
+            onChange={this.props.toggleSetting}
+            dependsOn={[['collapsed', 'enabled']]}
+          >
+            <FormattedMessage id='settings.auto_collapse_all' defaultMessage='Everything' />
+          </SettingsItem>
+          <SettingsItem
+            settings={this.props.settings}
+            item={['collapsed', 'auto', 'notifications']}
+            id='mastodon-settings--collapsed-auto-notifications'
+            onChange={this.props.toggleSetting}
+            dependsOn={[['collapsed', 'enabled']]}
+            dependsOnNot={[['collapsed', 'auto', 'all']]}
+          >
+            <FormattedMessage id='settings.auto_collapse_notifications' defaultMessage='Notifications' />
+          </SettingsItem>
+          <SettingsItem
+            settings={this.props.settings}
+            item={['collapsed', 'auto', 'lengthy']}
+            id='mastodon-settings--collapsed-auto-lengthy'
+            onChange={this.props.toggleSetting}
+            dependsOn={[['collapsed', 'enabled']]}
+            dependsOnNot={[['collapsed', 'auto', 'all']]}
+          >
+            <FormattedMessage id='settings.auto_collapse_lengthy' defaultMessage='Lengthy toots' />
+          </SettingsItem>
+          <SettingsItem
+            settings={this.props.settings}
+            item={['collapsed', 'auto', 'replies']}
+            id='mastodon-settings--collapsed-auto-replies'
+            onChange={this.props.toggleSetting}
+            dependsOn={[['collapsed', 'enabled']]}
+            dependsOnNot={[['collapsed', 'auto', 'all']]}
+          >
+            <FormattedMessage id='settings.auto_collapse_replies' defaultMessage='Replies' />
+          </SettingsItem>
+          <SettingsItem
+            settings={this.props.settings}
+            item={['collapsed', 'auto', 'media']}
+            id='mastodon-settings--collapsed-auto-media'
+            onChange={this.props.toggleSetting}
+            dependsOn={[['collapsed', 'enabled']]}
+            dependsOnNot={[['collapsed', 'auto', 'all']]}
+          >
+            <FormattedMessage id='settings.auto_collapse_media' defaultMessage='Toots with media' />
+          </SettingsItem>
+        </section>
+        <section>
+          <h2><FormattedMessage id='settings.image_backgrounds' defaultMessage='Image backgrounds' /></h2>
+          <SettingsItem
+            settings={this.props.settings}
+            item={['collapsed', 'backgrounds', 'user_backgrounds']}
+            id='mastodon-settings--collapsed-user-backgrouns'
+            onChange={this.props.toggleSetting}
+            dependsOn={[['collapsed', 'enabled']]}
+          >
+            <FormattedMessage id='settings.image_backgrounds_users' defaultMessage='Give collapsed toots an image background' />
+          </SettingsItem>
+          <SettingsItem
+            settings={this.props.settings}
+            item={['collapsed', 'backgrounds', 'preview_images']}
+            id='mastodon-settings--collapsed-preview-images'
+            onChange={this.props.toggleSetting}
+            dependsOn={[['collapsed', 'enabled']]}
+          >
+            <FormattedMessage id='settings.image_backgrounds_media' defaultMessage='Preview collapsed toot media' />
+          </SettingsItem>
+        </section>
+      </div>
+    );
+  }
+
+  navigateTo = (e) =>
+    this.setState({ currentIndex: +e.currentTarget.getAttribute('data-mastodon-navigation_index') });
+
+  render () {
+
+    const { General, CollapsedStatuses, navigateTo } = this;
+    const { onClose } = this.props;
+    const { currentIndex } = this.state;
+
+    return (
+      <div className='modal-root__modal settings-modal'>
+
+        <nav className='settings-modal__navigation'>
+          <a onClick={navigateTo} role='button' data-mastodon-navigation_index='0' tabIndex='0' className={`settings-modal__navigation-item${currentIndex === 0 ? ' active' : ''}`}>
+            <FormattedMessage id='settings.general' defaultMessage='General' />
+          </a>
+          <a onClick={navigateTo} role='button' data-mastodon-navigation_index='1' tabIndex='0' className={`settings-modal__navigation-item${currentIndex === 1 ? ' active' : ''}`}>
+            <FormattedMessage id='settings.collapsed_statuses' defaultMessage='Collapsed toots' />
+          </a>
+          <a href='/settings/preferences' className='settings-modal__navigation-item'>
+            <i className='fa fa-fw fa-cogs' /> <FormattedMessage id='settings.preferences' defaultMessage='User preferences' />
+          </a>
+          <a onClick={onClose} role='button' tabIndex='0' className='settings-modal__navigation-close'>
+            <FormattedMessage id='settings.close' defaultMessage='Close' />
+          </a>
+
+        </nav>
+
+        <div className='settings-modal__content'>
+          {
+            [
+              <General />,
+              <CollapsedStatuses />,
+            ][currentIndex] || <General />
+          }
+        </div>
+
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/ui/containers/settings_modal_container.js b/app/javascript/mastodon/features/ui/containers/settings_modal_container.js
new file mode 100644
index 000000000..83565c4b9
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/containers/settings_modal_container.js
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux';
+import { changeLocalSetting } from '../../../actions/local_settings';
+import { closeModal } from '../../../actions/modal';
+import SettingsModal from '../components/settings_modal';
+
+const mapStateToProps = state => ({
+  settings: state.get('local_settings'),
+});
+
+const mapDispatchToProps = dispatch => ({
+  toggleSetting (setting, e) {
+    dispatch(changeLocalSetting(setting, e.target.checked));
+  },
+  onClose () {
+    dispatch(closeModal());
+  },
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(SettingsModal);
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 4d38c2677..824963a53 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -73,7 +73,8 @@ class WrappedRoute extends React.Component {
 }
 
 const mapStateToProps = state => ({
-  layout: state.getIn(['localSettings', 'layout']),
+  layout: state.getIn(['local_settings', 'layout']),
+  isWide: state.getIn(['local_settings', 'stretch']),
 });
 
 @connect(mapStateToProps)
@@ -83,6 +84,7 @@ export default class UI extends React.PureComponent {
     dispatch: PropTypes.func.isRequired,
     children: PropTypes.node,
     layout: PropTypes.string,
+    isWide: PropTypes.bool,
   };
 
   state = {
@@ -179,7 +181,7 @@ export default class UI extends React.PureComponent {
 
   render () {
     const { width, draggingOver } = this.state;
-    const { children, layout } = this.props;
+    const { children, layout, isWide } = this.props;
 
     const columnsClass = layout => {
       switch (layout) {
@@ -193,7 +195,7 @@ export default class UI extends React.PureComponent {
     };
 
     return (
-      <div className={'ui ' + columnsClass(layout)} ref={this.setRef}>
+      <div className={'ui ' + columnsClass(layout) + (isWide ? ' wide' : '')} ref={this.setRef}>
         <TabsBar />
         <ColumnsAreaContainer singleColumn={isMobile(width, layout)}>
           <WrappedSwitch>
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 5ab914477..147f6a971 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -189,16 +189,16 @@
   {
     "descriptors": [
       {
-        "defaultMessage": "{name} boosted",
-        "id": "status.reblogged_by"
-      },
-      {
         "defaultMessage": "Collapse",
         "id": "status.collapse"
       },
       {
         "defaultMessage": "Uncollapse",
         "id": "status.uncollapse"
+      },
+      {
+        "defaultMessage": "{name} boosted",
+        "id": "status.reblogged_by"
       }
     ],
     "path": "app/javascript/mastodon/components/status.json"
@@ -652,8 +652,8 @@
         "id": "navigation_bar.community_timeline"
       },
       {
-        "defaultMessage": "Preferences",
-        "id": "navigation_bar.preferences"
+        "defaultMessage": "App settings",
+        "id": "navigation_bar.app_settings"
       },
       {
         "defaultMessage": "Logout",
@@ -668,12 +668,12 @@
         "id": "layout.mobile"
       },
       {
-        "defaultMessage": "Desktop",
-        "id": "layout.desktop"
-      },
-      {
         "defaultMessage": "Auto",
         "id": "layout.auto"
+      },
+      {
+        "defaultMessage": "Desktop",
+        "id": "layout.desktop"
       }
     ],
     "path": "app/javascript/mastodon/features/compose/index.json"
@@ -744,6 +744,10 @@
         "id": "navigation_bar.preferences"
       },
       {
+        "defaultMessage": "App settings",
+        "id": "navigation_bar.app_settings"
+      },
+      {
         "defaultMessage": "Follow requests",
         "id": "navigation_bar.follow_requests"
       },
@@ -1073,7 +1077,7 @@
         "id": "onboarding.page_one.welcome"
       },
       {
-        "defaultMessage": "{domain} is an 'instance' of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
+        "defaultMessage": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
         "id": "onboarding.page_one.federation"
       },
       {
@@ -1121,7 +1125,7 @@
         "id": "onboarding.page_six.almost_done"
       },
       {
-        "defaultMessage": "{domain} runs on Glitchsoc, a friendly fork of {Mastodon}. Glitchsoc is fully compatible with any Mastodon instance or app. You can report bugs, request features, or contribute to the code on {github}.",
+        "defaultMessage": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
         "id": "onboarding.page_six.github"
       },
       {
@@ -1171,6 +1175,71 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "General",
+        "id": "settings.general"
+      },
+      {
+        "defaultMessage": "Wide view (Desktop mode only)",
+        "id": "settings.wide_view"
+      },
+      {
+        "defaultMessage": "Collapsed toots",
+        "id": "settings.collapsed_statuses"
+      },
+      {
+        "defaultMessage": "Enable collapsed toots",
+        "id": "settings.enable_collapsed"
+      },
+      {
+        "defaultMessage": "Automatic collapsing",
+        "id": "settings.auto_collapse"
+      },
+      {
+        "defaultMessage": "Everything",
+        "id": "settings.auto_collapse_all"
+      },
+      {
+        "defaultMessage": "Notifications",
+        "id": "settings.auto_collapse_notifications"
+      },
+      {
+        "defaultMessage": "Lengthy toots",
+        "id": "settings.auto_collapse_lengthy"
+      },
+      {
+        "defaultMessage": "Replies",
+        "id": "settings.auto_collapse_replies"
+      },
+      {
+        "defaultMessage": "Toots with media",
+        "id": "settings.auto_collapse_media"
+      },
+      {
+        "defaultMessage": "Image backgrounds",
+        "id": "settings.image_backgrounds"
+      },
+      {
+        "defaultMessage": "Give collapsed toots an image background",
+        "id": "settings.image_backgrounds_users"
+      },
+      {
+        "defaultMessage": "Preview collapsed toot media",
+        "id": "settings.image_backgrounds_media"
+      },
+      {
+        "defaultMessage": "User preferences",
+        "id": "settings.global_settings"
+      },
+      {
+        "defaultMessage": "Close",
+        "id": "settings.close"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/ui/components/settings_modal.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "Compose",
         "id": "tabs_bar.compose"
       },
@@ -1211,4 +1280,4 @@
     ],
     "path": "app/javascript/mastodon/features/ui/components/video_modal.json"
   }
-]
+]
\ No newline at end of file
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index d0c0ca137..fb81aba4a 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -87,6 +87,7 @@
   "loading_indicator.label": "Loading...",
   "media_gallery.toggle_visible": "Toggle visibility",
   "missing_indicator.label": "Not found",
+  "navigation_bar.app_settings": "App settings",
   "navigation_bar.blocks": "Blocked users",
   "navigation_bar.community_timeline": "Local timeline",
   "navigation_bar.edit_profile": "Edit profile",
@@ -146,6 +147,21 @@
   "report.target": "Reporting {target}",
   "search.placeholder": "Search",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
+  "settings.auto_collapse": "Automatic collapsing",
+  "settings.auto_collapse_all": "Everything",
+  "settings.auto_collapse_lengthy": "Lengthy toots",
+  "settings.auto_collapse_media": "Toots with media",
+  "settings.auto_collapse_notifications": "Notifications",
+  "settings.auto_collapse_replies": "Replies",
+  "settings.close": "Close",
+  "settings.collapsed_statuses": "Collapsed toots",
+  "settings.enable_collapsed": "Enable collapsed toots",
+  "settings.general": "General",
+  "settings.global_settings": "User preferences",
+  "settings.image_backgrounds": "Image backgrounds",
+  "settings.image_backgrounds_media": "Preview collapsed toot media",
+  "settings.image_backgrounds_users": "Give collapsed toots an image background",
+  "settings.wide_view": "Wide view (Desktop mode only)",
   "status.cannot_reblog": "This post cannot be boosted",
   "status.collapse": "Collapse",
   "status.delete": "Delete",
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index 24f7f94a6..0749a4e4a 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -14,7 +14,7 @@ import relationships from './relationships';
 import search from './search';
 import notifications from './notifications';
 import settings from './settings';
-import localSettings from './local_settings';
+import local_settings from './local_settings';
 import status_lists from './status_lists';
 import cards from './cards';
 import reports from './reports';
@@ -37,7 +37,7 @@ export default combineReducers({
   search,
   notifications,
   settings,
-  localSettings,
+  local_settings,
   cards,
   reports,
   contexts,
diff --git a/app/javascript/mastodon/reducers/local_settings.js b/app/javascript/mastodon/reducers/local_settings.js
index 529d31ebb..c6581fe2e 100644
--- a/app/javascript/mastodon/reducers/local_settings.js
+++ b/app/javascript/mastodon/reducers/local_settings.js
@@ -3,7 +3,22 @@ import { STORE_HYDRATE } from '../actions/store';
 import Immutable from 'immutable';
 
 const initialState = Immutable.Map({
-  layout: 'auto',
+  layout    : 'auto',
+  stretch   : true,
+  collapsed : {
+    enabled     : true,
+    auto        : {
+      all              : false,
+      notifications    : true,
+      lengthy          : true,
+      replies          : false,
+      media            : false,
+    },
+    backgrounds : {
+      user_backgrounds : false,
+      preview_images   : false,
+    },
+  },
 });
 
 const hydrate = (state, localSettings) => state.mergeDeep(localSettings);
@@ -11,7 +26,7 @@ const hydrate = (state, localSettings) => state.mergeDeep(localSettings);
 export default function localSettings(state = initialState, action) {
   switch(action.type) {
   case STORE_HYDRATE:
-    return hydrate(state, action.state.get('localSettings'));
+    return hydrate(state, action.state.get('local_settings'));
   case LOCAL_SETTING_CHANGE:
     return state.setIn(action.key, action.value);
   default:
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss
index 9bf386c0c..2ed6d14a3 100644
--- a/app/javascript/styles/components.scss
+++ b/app/javascript/styles/components.scss
@@ -1351,6 +1351,10 @@
   overflow-x: auto;
   position: relative;
   padding: 10px;
+
+  .wide & {
+    justify-content: center;
+  }
 }
 
 @include limited-single-column('screen and (max-width: 360px)', $parent: null) {
@@ -1367,6 +1371,12 @@
   flex-direction: column;
   overflow: hidden;
 
+  .wide & {
+    flex: auto;
+    min-width: 330px;
+    max-width: 400px;
+  }
+
   > .scrollable {
     background: $ui-base-color;
   }
@@ -1387,6 +1397,12 @@
   display: flex;
   flex-direction: column;
   overflow-y: auto;
+
+  .wide & {
+    flex: 1 1 200px;
+    min-width: 300px;
+    max-width: 400px;
+  }
 }
 
 .drawer__tab {
@@ -1399,11 +1415,12 @@
   text-align: center;
   font-size: 16px;
   border-bottom: 2px solid transparent;
+  outline: none;
+  cursor: pointer;
 }
 
 .column,
 .drawer {
-  flex: 1 1 100%;
   @supports(display: grid) { // hack to fix Chrome <57
     contain: strict;
   }
@@ -1419,20 +1436,25 @@
   }
 }
 
-@include single-column('screen and (max-width: 1024px)', $parent: null) {
-  .column,
-  .drawer {
-    width: 100%;
-    padding: 0;
-  }
+:root {  //  Overrides .wide stylings for mobile view
+  @include single-column('screen and (max-width: 1024px)', $parent: null) {
+    .column,
+    .drawer {
+      flex: auto;
+      width: 100%;
+      min-width: 0;
+      max-width: none;
+      padding: 0;
+    }
 
-  .columns-area {
-    flex-direction: column;
-  }
+    .columns-area {
+      flex-direction: column;
+    }
 
-  .search__input,
-  .autosuggest-textarea__textarea {
-    font-size: 16px;
+    .search__input,
+    .autosuggest-textarea__textarea {
+      font-size: 16px;
+    }
   }
 }
 
@@ -1443,7 +1465,6 @@
 
   .column,
   .drawer {
-    flex: 0 0 auto;
     padding: 10px;
     padding-left: 5px;
     padding-right: 5px;
@@ -1771,6 +1792,8 @@
   font-size: 16px;
   padding: 15px;
   text-decoration: none;
+  cursor: pointer;
+  outline: none;
 
   &:hover {
     background: lighten($ui-base-color, 11%);
@@ -3312,6 +3335,85 @@ button.icon-button.active i.fa-retweet {
   margin-bottom: 20px;
 }
 
+.settings-modal {
+  position: relative;
+  display: flex;
+  flex-direction: row;
+  background: $ui-secondary-color;
+  color: $ui-base-color;
+  border-radius: 8px;
+  height: 80vh;
+  width: 80vw;
+  max-width: 740px;
+  max-height: 450px;
+  overflow: hidden;
+
+  label {
+    display: block;
+  }
+
+  h1 {
+    font-size: 18px;
+    font-weight: 500;
+    line-height: 24px;
+    margin-bottom: 20px;
+  }
+
+  h2 {
+    font-size: 15px;
+    font-weight: 500;
+    line-height: 20px;
+    margin-top: 20px;
+    margin-bottom: 10px;
+  }
+}
+
+.settings-modal__navigation {
+  background: $primary-text-color;
+  color: $ui-base-color;
+  width: 200px;
+  font-size: 15px;
+  line-height: 20px;
+  overflow-y: auto;
+
+  .settings-modal__navigation-item, .settings-modal__navigation-close {
+    display: block;
+    padding: 15px 20px;
+    cursor: pointer;
+    outline: none;
+    text-decoration: none;
+  }
+
+  .settings-modal__navigation-item {
+    background: $primary-text-color;
+    color: inherit;
+    border-bottom: 1px $ui-primary-color solid;
+    transition: background .3s;
+
+    &:hover {
+      background: $ui-secondary-color;
+    }
+
+    &.active {
+      background: $ui-highlight-color;
+      color: $primary-text-color;
+    }
+  }
+
+  .settings-modal__navigation-close {
+    background: $error-value-color;
+    color: $primary-text-color;
+  }
+}
+
+.settings-modal__content {
+  display: block;
+  flex: auto;
+  padding: 15px 20px 15px 20px;
+  width: 360px;
+  overflow-y: auto;
+}
+
 .onboard-sliders {
   display: inline-block;
   max-width: 30px;
diff --git a/app/javascript/styles/custom.scss b/app/javascript/styles/custom.scss
index 5144e4fb6..6a18fd628 100644
--- a/app/javascript/styles/custom.scss
+++ b/app/javascript/styles/custom.scss
@@ -1,19 +1,5 @@
 @import 'application';
 
-@include multi-columns('screen and (min-width: 1300px)', $parent: null) {
-  .column {
-    flex-grow: 1 !important;
-    max-width: 400px;
-  }
-
-  .drawer {
-    flex-grow: 1 !important;
-    flex-basis: 200px !important;
-    min-width: 268px;
-    max-width: 400px;
-  }
-}
-
 .muted {
   .status__content p, .status__content a {
     color: lighten($ui-base-color, 35%);