about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app/javascript/mastodon/actions/local_settings.js20
-rw-r--r--app/javascript/mastodon/containers/mastodon.js5
-rw-r--r--app/javascript/mastodon/features/compose/index.js57
-rw-r--r--app/javascript/mastodon/features/ui/index.js24
-rw-r--r--app/javascript/mastodon/is_mobile.js11
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json16
-rw-r--r--app/javascript/mastodon/locales/en.json4
-rw-r--r--app/javascript/mastodon/reducers/index.js2
-rw-r--r--app/javascript/mastodon/reducers/local_settings.js20
-rw-r--r--app/javascript/mastodon/reducers/settings.js1
-rw-r--r--app/javascript/styles/_mixins.scss30
-rw-r--r--app/javascript/styles/components.scss70
-rw-r--r--app/javascript/styles/custom.scss2
13 files changed, 228 insertions, 34 deletions
diff --git a/app/javascript/mastodon/actions/local_settings.js b/app/javascript/mastodon/actions/local_settings.js
new file mode 100644
index 000000000..742a1eec2
--- /dev/null
+++ b/app/javascript/mastodon/actions/local_settings.js
@@ -0,0 +1,20 @@
+export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE';
+
+export function changeLocalSetting(key, value) {
+  return dispatch => {
+    dispatch({
+      type: LOCAL_SETTING_CHANGE,
+      key,
+      value,
+    });
+
+    dispatch(saveLocalSettings());
+  };
+};
+
+export function saveLocalSettings() {
+  return (_, getState) => {
+    const localSettings = getState().get('localSettings').toJS();
+    localStorage.setItem('mastodon-settings', JSON.stringify(localSettings));
+  };
+};
diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js
index 3bd89902f..3468a7944 100644
--- a/app/javascript/mastodon/containers/mastodon.js
+++ b/app/javascript/mastodon/containers/mastodon.js
@@ -24,6 +24,11 @@ addLocaleData(localeData);
 
 const store = configureStore();
 const initialState = JSON.parse(document.getElementById('initial-state').textContent);
+try {
+  initialState.localSettings = JSON.parse(localStorage.getItem('mastodon-settings'));
+} catch (e) {
+  initialState.localSettings = {};
+}
 store.dispatch(hydrateStore(initialState));
 
 export default class Mastodon extends React.PureComponent {
diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js
index 747fe4216..512167193 100644
--- a/app/javascript/mastodon/features/compose/index.js
+++ b/app/javascript/mastodon/features/compose/index.js
@@ -4,8 +4,9 @@ import NavigationContainer from './containers/navigation_container';
 import PropTypes from 'prop-types';
 import { connect } from 'react-redux';
 import { mountCompose, unmountCompose } from '../../actions/compose';
+import { changeLocalSetting } from '../../actions/local_settings';
 import Link from 'react-router-dom/Link';
-import { injectIntl, defineMessages } from 'react-intl';
+import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
 import SearchContainer from './containers/search_container';
 import Motion from 'react-motion/lib/Motion';
 import spring from 'react-motion/lib/spring';
@@ -21,6 +22,7 @@ const messages = defineMessages({
 
 const mapStateToProps = state => ({
   showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
+  layout: state.getIn(['localSettings', 'layout']),
 });
 
 @connect(mapStateToProps)
@@ -32,6 +34,7 @@ export default class Compose extends React.PureComponent {
     multiColumn: PropTypes.bool,
     showSearch: PropTypes.bool,
     intl: PropTypes.object.isRequired,
+    layout: PropTypes.string,
   };
 
   componentDidMount () {
@@ -42,8 +45,14 @@ export default class Compose extends React.PureComponent {
     this.props.dispatch(unmountCompose());
   }
 
+  onLayoutClick = (e) => {
+    const layout = e.currentTarget.getAttribute('data-mastodon-layout');
+    this.props.dispatch(changeLocalSetting(['layout'], layout));
+    e.preventDefault();
+  }
+
   render () {
-    const { multiColumn, showSearch, intl } = this.props;
+    const { multiColumn, showSearch, intl, layout } = this.props;
 
     let header = '';
 
@@ -59,6 +68,47 @@ export default class Compose extends React.PureComponent {
       );
     }
 
+    let layoutContent = '';
+
+    switch (layout) {
+    case 'single':
+      layoutContent = (
+        <div className='layout__selector'>
+          <p>
+            <FormattedMessage id='layout.current_is' defaultMessage='Your current layout is:' /> <b><FormattedMessage id='layout.mobile' defaultMessage='Mobile' /></b>
+          </p>
+          <p>
+            <a onClick={this.onLayoutClick} role='button' tabIndex='0' data-mastodon-layout='auto'><FormattedMessage id='layout.auto' defaultMessage='Auto' /></a> • <a onClick={this.onLayoutClick} role='button' tabIndex='0' data-mastodon-layout='multiple'><FormattedMessage id='layout.desktop' defaultMessage='Desktop' /></a>
+          </p>
+        </div>
+      );
+      break;
+    case 'multiple':
+      layoutContent = (
+        <div className='layout__selector'>
+          <p>
+            <FormattedMessage id='layout.current_is' defaultMessage='Your current layout is:' /> <b><FormattedMessage id='layout.desktop' defaultMessage='Desktop' /></b>
+          </p>
+          <p>
+            <a onClick={this.onLayoutClick} role='button' tabIndex='0' data-mastodon-layout='auto'><FormattedMessage id='layout.auto' defaultMessage='Auto' /></a> • <a onClick={this.onLayoutClick} role='button' tabIndex='0' data-mastodon-layout='single'><FormattedMessage id='layout.mobile' defaultMessage='Mobile' /></a>
+          </p>
+        </div>
+      );
+      break;
+    default:
+      layoutContent = (
+        <div className='layout__selector'>
+          <p>
+            <FormattedMessage id='layout.current_is' defaultMessage='Your current layout is:' /> <b><FormattedMessage id='layout.auto' defaultMessage='Auto' /></b>
+          </p>
+          <p>
+            <a onClick={this.onLayoutClick} role='button' tabIndex='0' data-mastodon-layout='multiple'><FormattedMessage id='layout.desktop' defaultMessage='Desktop' /></a> • <a onClick={this.onLayoutClick} role='button' tabIndex='0' data-mastodon-layout='single'><FormattedMessage id='layout.mobile' defaultMessage='Mobile' /></a>
+          </p>
+        </div>
+      );
+      break;
+    }
+
     return (
       <div className='drawer'>
         {header}
@@ -79,6 +129,9 @@ export default class Compose extends React.PureComponent {
             }
           </Motion>
         </div>
+
+        {layoutContent}
+
       </div>
     );
   }
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 8453679b0..e5915ffe0 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -74,12 +74,17 @@ class WrappedRoute extends React.Component {
 
 }
 
-@connect()
+const mapStateToProps = state => ({
+  layout: state.getIn(['localSettings', 'layout']),
+});
+
+@connect(mapStateToProps)
 export default class UI extends React.PureComponent {
 
   static propTypes = {
     dispatch: PropTypes.func.isRequired,
     children: PropTypes.node,
+    layout: PropTypes.string,
   };
 
   state = {
@@ -176,12 +181,23 @@ export default class UI extends React.PureComponent {
 
   render () {
     const { width, draggingOver } = this.state;
-    const { children } = this.props;
+    const { children, layout } = this.props;
+
+    const columnsClass = layout => {
+      switch (layout) {
+      case 'single':
+        return 'single-column';
+      case 'multiple':
+        return 'multi-columns';
+      default:
+        return 'auto-columns';
+      }
+    };
 
     return (
-      <div className='ui' ref={this.setRef}>
+      <div className={'ui ' + columnsClass(layout)} ref={this.setRef}>
         <TabsBar />
-        <ColumnsAreaContainer singleColumn={isMobile(width)}>
+        <ColumnsAreaContainer singleColumn={isMobile(width, layout)}>
           <WrappedSwitch>
             <Redirect from='/' to='/getting-started' exact />
             <WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
diff --git a/app/javascript/mastodon/is_mobile.js b/app/javascript/mastodon/is_mobile.js
index 992e63727..014a9a8d5 100644
--- a/app/javascript/mastodon/is_mobile.js
+++ b/app/javascript/mastodon/is_mobile.js
@@ -1,7 +1,14 @@
 const LAYOUT_BREAKPOINT = 1024;
 
-export function isMobile(width) {
-  return width <= LAYOUT_BREAKPOINT;
+export function isMobile(width, columns) {
+  switch (columns) {
+  case 'multiple':
+    return false;
+  case 'single':
+    return true;
+  default:
+    return width <= LAYOUT_BREAKPOINT;
+  }
 };
 
 const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index dd790f659..803d9b292 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -658,6 +658,22 @@
       {
         "defaultMessage": "Logout",
         "id": "navigation_bar.logout"
+      },
+      {
+        "defaultMessage": "Your current layout is:",
+        "id": "layout.current_is"
+      },
+      {
+        "defaultMessage": "Mobile",
+        "id": "layout.mobile"
+      },
+      {
+        "defaultMessage": "Desktop",
+        "id": "layout.desktop"
+      },
+      {
+        "defaultMessage": "Auto",
+        "id": "layout.auto"
       }
     ],
     "path": "app/javascript/mastodon/features/compose/index.json"
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 8fb409618..c19d4aa02 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -79,6 +79,10 @@
   "home.column_settings.show_reblogs": "Show boosts",
   "home.column_settings.show_replies": "Show replies",
   "home.settings": "Column settings",
+  "layout.auto": "Auto",
+  "layout.current_is": "Your current layout is:",
+  "layout.desktop": "Desktop",
+  "layout.mobile": "Mobile",
   "lightbox.close": "Close",
   "loading_indicator.label": "Loading...",
   "media_gallery.toggle_visible": "Toggle visibility",
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index be402a16b..24f7f94a6 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -14,6 +14,7 @@ import relationships from './relationships';
 import search from './search';
 import notifications from './notifications';
 import settings from './settings';
+import localSettings from './local_settings';
 import status_lists from './status_lists';
 import cards from './cards';
 import reports from './reports';
@@ -36,6 +37,7 @@ export default combineReducers({
   search,
   notifications,
   settings,
+  localSettings,
   cards,
   reports,
   contexts,
diff --git a/app/javascript/mastodon/reducers/local_settings.js b/app/javascript/mastodon/reducers/local_settings.js
new file mode 100644
index 000000000..529d31ebb
--- /dev/null
+++ b/app/javascript/mastodon/reducers/local_settings.js
@@ -0,0 +1,20 @@
+import { LOCAL_SETTING_CHANGE } from '../actions/local_settings';
+import { STORE_HYDRATE } from '../actions/store';
+import Immutable from 'immutable';
+
+const initialState = Immutable.Map({
+  layout: 'auto',
+});
+
+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'));
+  case LOCAL_SETTING_CHANGE:
+    return state.setIn(action.key, action.value);
+  default:
+    return state;
+  }
+};
diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js
index ddad7a4fc..9a15a1fe3 100644
--- a/app/javascript/mastodon/reducers/settings.js
+++ b/app/javascript/mastodon/reducers/settings.js
@@ -6,6 +6,7 @@ import uuid from '../uuid';
 
 const initialState = Immutable.Map({
   onboarded: false,
+  layout: 'auto',
 
   home: Immutable.Map({
     shows: Immutable.Map({
diff --git a/app/javascript/styles/_mixins.scss b/app/javascript/styles/_mixins.scss
index 455062135..7412991b8 100644
--- a/app/javascript/styles/_mixins.scss
+++ b/app/javascript/styles/_mixins.scss
@@ -10,3 +10,33 @@
   height: $size;
   background-size: $size $size;
 }
+
+@mixin single-column($media, $parent: '&') {
+  .auto-columns #{$parent} {
+    @media #{$media} {
+      @content;
+    }
+  }
+  .single-column #{$parent} {
+    @content;
+  }
+}
+
+@mixin limited-single-column($media, $parent: '&') {
+  .auto-columns #{$parent}, .single-column #{$parent} {
+    @media #{$media} {
+      @content;
+    }
+  }
+}
+
+@mixin multi-columns($media, $parent: '&') {
+  .auto-columns #{$parent} {
+    @media #{$media} {
+      @content;
+    }
+  }
+  .multi-columns #{$parent} {
+    @content;
+  }
+}
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss
index 025ef2f64..af9da6c37 100644
--- a/app/javascript/styles/components.scss
+++ b/app/javascript/styles/components.scss
@@ -1328,11 +1328,12 @@
   justify-content: flex-start;
   overflow-x: auto;
   position: relative;
+  padding: 10px;
 }
 
-@media screen and (min-width: 360px) {
+@include limited-single-column('screen and (max-width: 360px)', $parent: null) {
   .columns-area {
-    padding: 10px;
+    padding: 0;
   }
 }
 
@@ -1386,18 +1387,17 @@
   }
 }
 
-@media screen and (min-width: 360px) {
+@include limited-single-column('screen and (max-width: 360px)', $parent: null) {
   .tabs-bar {
-    margin: 10px;
-    margin-bottom: 0;
+    margin: 0;
   }
 
   .search {
-    margin-bottom: 10px;
+    margin-bottom: 0;
   }
 }
 
-@media screen and (max-width: 1024px) {
+@include single-column('screen and (max-width: 1024px)', $parent: null) {
   .column,
   .drawer {
     width: 100%;
@@ -1414,7 +1414,7 @@
   }
 }
 
-@media screen and (min-width: 1025px) {
+@include multi-columns('screen and (min-width: 1025px)', $parent: null) {
   .columns-area {
     padding: 0;
   }
@@ -1447,28 +1447,26 @@
 .drawer__pager {
   box-sizing: border-box;
   padding: 0;
-  flex-grow: 1;
+  flex: 0 0 auto;
   position: relative;
   overflow: hidden;
-  display: flex;
 }
 
 .drawer__inner {
-  position: absolute;
-  top: 0;
-  left: 0;
   background: lighten($ui-base-color, 13%);
   box-sizing: border-box;
   padding: 0;
-  display: flex;
-  flex-direction: column;
   overflow: hidden;
   overflow-y: auto;
   width: 100%;
-  height: 100%;
 
   &.darker {
+    position: absolute;
+    top: 0;
+    left: 0;
     background: $ui-base-color;
+    width: 100%;
+    height: 100%;
   }
 }
 
@@ -1496,11 +1494,32 @@
   }
 }
 
+.layout__selector {
+  margin-top: 20px;
+
+  a {
+    text-decoration: underline;
+    cursor: pointer;
+    color: lighten($ui-base-color, 26%);
+  }
+
+  b {
+    font-weight: bold;
+  }
+
+  p {
+    font-size: 13px;
+    color: $ui-secondary-color;
+  }
+}
+
 .tabs-bar {
   display: flex;
   background: lighten($ui-base-color, 8%);
   flex: 0 0 auto;
   overflow-y: auto;
+  margin: 10px;
+  margin-bottom: 0;
 }
 
 .tabs-bar__link {
@@ -1528,7 +1547,7 @@
   &:hover,
   &:focus,
   &:active {
-    @media screen and (min-width: 1025px) {
+    @include multi-columns('screen and (min-width: 1025px)') {
       background: lighten($ui-base-color, 14%);
       transition: all 100ms linear;
     }
@@ -1540,7 +1559,7 @@
   }
 }
 
-@media screen and (min-width: 600px) {
+@include limited-single-column('screen and (max-width: 600px)', $parent: null) {
   .tabs-bar__link {
     span {
       display: inline;
@@ -1548,7 +1567,7 @@
   }
 }
 
-@media screen and (min-width: 1025px) {
+@include multi-columns('screen and (min-width: 1025px)', $parent: null) {
   .tabs-bar {
     display: none;
   }
@@ -1737,7 +1756,7 @@
   }
 
   &.hidden-on-mobile {
-    @media screen and (max-width: 1024px) {
+    @include single-column('screen and (max-width: 1024px)') {
       display: none;
     }
   }
@@ -1781,7 +1800,7 @@
     outline: 0;
   }
 
-  @media screen and (max-width: 600px) {
+  @include limited-single-column('screen and (max-width: 600px)') {
     font-size: 16px;
   }
 }
@@ -1798,7 +1817,7 @@
   padding-right: 10px + 22px;
   resize: none;
 
-  @media screen and (max-width: 600px) {
+  @include limited-single-column('screen and (max-width: 600px)') {
     height: 100px !important; // prevent auto-resize textarea
     resize: vertical;
   }
@@ -1911,7 +1930,7 @@
     border-bottom-color: $ui-highlight-color;
   }
 
-  @media screen and (max-width: 600px) {
+  @include limited-single-column('screen and (max-width: 600px)') {
     font-size: 16px;
   }
 }
@@ -2114,7 +2133,7 @@ button.icon-button.active i.fa-retweet {
   }
 
   &.hidden-on-mobile {
-    @media screen and (max-width: 1024px) {
+    @include single-column('screen and (max-width: 1024px)') {
       display: none;
     }
   }
@@ -2872,6 +2891,7 @@ button.icon-button.active i.fa-retweet {
 
 .search {
   position: relative;
+  margin-bottom: 10px;
 }
 
 .search__input {
@@ -2904,7 +2924,7 @@ button.icon-button.active i.fa-retweet {
     background: lighten($ui-base-color, 4%);
   }
 
-  @media screen and (max-width: 600px) {
+  @include limited-single-column('screen and (max-width: 600px)') {
     font-size: 16px;
   }
 }
diff --git a/app/javascript/styles/custom.scss b/app/javascript/styles/custom.scss
index b03231102..7a0509842 100644
--- a/app/javascript/styles/custom.scss
+++ b/app/javascript/styles/custom.scss
@@ -1,6 +1,6 @@
 @import 'application';
 
-@media screen and (min-width: 1300px) { 
+@include multi-columns('screen and (min-width: 1300px)', $parent: null) {
   .column {
     flex-grow: 1 !important;
     max-width: 400px;