about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app/javascript/flavours/glitch/actions/compose.js26
-rw-r--r--app/javascript/flavours/glitch/actions/statuses.js7
-rw-r--r--app/javascript/flavours/glitch/components/autosuggest_input.js2
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/navigation_bar.js2
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/search.js11
-rw-r--r--app/javascript/flavours/glitch/features/getting_started/index.js17
-rw-r--r--app/javascript/flavours/glitch/features/search/index.js17
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/columns_area.js14
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/compose_panel.js41
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/list_panel.js55
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/navigation_panel.js27
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/notifications_counter_icon.js6
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/tabs_bar.js13
-rw-r--r--app/javascript/flavours/glitch/features/ui/index.js9
-rw-r--r--app/javascript/flavours/glitch/reducers/settings.js2
-rw-r--r--app/javascript/flavours/glitch/styles/components/columns.scss57
-rw-r--r--app/javascript/flavours/glitch/styles/components/index.scss157
-rw-r--r--app/javascript/flavours/glitch/util/async-components.js4
-rw-r--r--app/javascript/flavours/glitch/util/initial_state.js1
19 files changed, 374 insertions, 94 deletions
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js
index 2fb97fa17..093952316 100644
--- a/app/javascript/flavours/glitch/actions/compose.js
+++ b/app/javascript/flavours/glitch/actions/compose.js
@@ -68,6 +68,14 @@ const messages = defineMessages({
   uploadErrorPoll:  { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
 });
 
+const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 3);
+
+export const ensureComposeIsVisible = (getState, routerHistory) => {
+  if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) {
+    routerHistory.push('/statuses/new');
+  }
+};
+
 export function changeCompose(text) {
   return {
     type: COMPOSE_CHANGE,
@@ -81,16 +89,14 @@ export function cycleElefriendCompose() {
   };
 };
 
-export function replyCompose(status, router) {
+export function replyCompose(status, routerHistory) {
   return (dispatch, getState) => {
     dispatch({
       type: COMPOSE_REPLY,
       status: status,
     });
 
-    if (router && !getState().getIn(['compose', 'mounted'])) {
-      router.push('/statuses/new');
-    }
+    ensureComposeIsVisible(getState, routerHistory);
   };
 };
 
@@ -106,29 +112,25 @@ export function resetCompose() {
   };
 };
 
-export function mentionCompose(account, router) {
+export function mentionCompose(account, routerHistory) {
   return (dispatch, getState) => {
     dispatch({
       type: COMPOSE_MENTION,
       account: account,
     });
 
-    if (!getState().getIn(['compose', 'mounted'])) {
-      router.push('/statuses/new');
-    }
+    ensureComposeIsVisible(getState, routerHistory);
   };
 };
 
-export function directCompose(account, router) {
+export function directCompose(account, routerHistory) {
   return (dispatch, getState) => {
     dispatch({
       type: COMPOSE_DIRECT,
       account: account,
     });
 
-    if (!getState().getIn(['compose', 'mounted'])) {
-      router.push('/statuses/new');
-    }
+    ensureComposeIsVisible(getState, routerHistory);
   };
 };
 
diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js
index 7e22a7f98..4d2bda78b 100644
--- a/app/javascript/flavours/glitch/actions/statuses.js
+++ b/app/javascript/flavours/glitch/actions/statuses.js
@@ -2,6 +2,7 @@ import api from 'flavours/glitch/util/api';
 
 import { deleteFromTimelines } from './timelines';
 import { importFetchedStatus, importFetchedStatuses } from './importer';
+import { ensureComposeIsVisible } from './compose';
 
 export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
 export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
@@ -80,7 +81,7 @@ export function redraft(status, raw_text, content_type) {
   };
 };
 
-export function deleteStatus(id, router, withRedraft = false) {
+export function deleteStatus(id, routerHistory, withRedraft = false) {
   return (dispatch, getState) => {
     let status = getState().getIn(['statuses', id]);
 
@@ -97,9 +98,7 @@ export function deleteStatus(id, router, withRedraft = false) {
       if (withRedraft) {
         dispatch(redraft(status, response.data.text, response.data.content_type));
 
-        if (!getState().getIn(['compose', 'mounted'])) {
-          router.push('/statuses/new');
-        }
+        ensureComposeIsVisible(getState, routerHistory);
       }
     }).catch(error => {
       dispatch(deleteStatusFail(id, error));
diff --git a/app/javascript/flavours/glitch/components/autosuggest_input.js b/app/javascript/flavours/glitch/components/autosuggest_input.js
index a6ae3bc75..5fc952d8e 100644
--- a/app/javascript/flavours/glitch/components/autosuggest_input.js
+++ b/app/javascript/flavours/glitch/components/autosuggest_input.js
@@ -49,7 +49,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
     autoFocus: PropTypes.bool,
     className: PropTypes.string,
     id: PropTypes.string,
-    searchTokens: ImmutablePropTypes.list,
+    searchTokens: PropTypes.arrayOf(PropTypes.string),
     maxLength: PropTypes.number,
   };
 
diff --git a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js
index 59172bb23..3148434f1 100644
--- a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js
+++ b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js
@@ -17,7 +17,7 @@ export default class NavigationBar extends ImmutablePureComponent {
       <div className='drawer--account'>
         <Permalink className='avatar' href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
           <span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
-          <Avatar account={this.props.account} size={40} />
+          <Avatar account={this.props.account} size={48} />
         </Permalink>
 
         <Permalink className='acct' href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
diff --git a/app/javascript/flavours/glitch/features/compose/components/search.js b/app/javascript/flavours/glitch/features/compose/components/search.js
index 5fed1567a..5f8c6cd52 100644
--- a/app/javascript/flavours/glitch/features/compose/components/search.js
+++ b/app/javascript/flavours/glitch/features/compose/components/search.js
@@ -60,6 +60,10 @@ class SearchPopout extends React.PureComponent {
 export default @injectIntl
 class Search extends React.PureComponent {
 
+  static contextTypes = {
+    router: PropTypes.object.isRequired,
+  };
+
   static propTypes = {
     value: PropTypes.string.isRequired,
     submitted: PropTypes.bool,
@@ -67,6 +71,7 @@ class Search extends React.PureComponent {
     onSubmit: PropTypes.func.isRequired,
     onClear: PropTypes.func.isRequired,
     onShow: PropTypes.func.isRequired,
+    openInRoute: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
 
@@ -109,8 +114,10 @@ class Search extends React.PureComponent {
     const { onSubmit } = this.props;
     switch (e.key) {
     case 'Enter':
-      if (onSubmit) {
-        onSubmit();
+      onSubmit();
+
+      if (this.props.openInRoute) {
+        this.context.router.history.push('/search');
       }
       break;
     case 'Escape':
diff --git a/app/javascript/flavours/glitch/features/getting_started/index.js b/app/javascript/flavours/glitch/features/getting_started/index.js
index 25fff1974..7b645c9d0 100644
--- a/app/javascript/flavours/glitch/features/getting_started/index.js
+++ b/app/javascript/flavours/glitch/features/getting_started/index.js
@@ -10,12 +10,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { me, invitesEnabled, version } from 'flavours/glitch/util/initial_state';
 import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
-import { changeSetting } from 'flavours/glitch/actions/settings';
 import { List as ImmutableList } from 'immutable';
 import { createSelector } from 'reselect';
 import { fetchLists } from 'flavours/glitch/actions/lists';
 import { preferencesLink, profileLink, signOutLink } from 'flavours/glitch/util/backend_links';
-import Toggle from 'react-toggle';
 
 const messages = defineMessages({
   heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
@@ -54,7 +52,6 @@ const makeMapStateToProps = () => {
     columns: state.getIn(['settings', 'columns']),
     unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
     unreadNotifications: state.getIn(['notifications', 'unread']),
-    forceSingleColumn: state.getIn(['settings', 'forceSingleColumn'], false),
   });
 
   return mapStateToProps;
@@ -64,7 +61,6 @@ const mapDispatchToProps = dispatch => ({
   fetchFollowRequests: () => dispatch(fetchFollowRequests()),
   fetchLists: () => dispatch(fetchLists()),
   openSettings: () => dispatch(openModal('SETTINGS', {})),
-  changeForceSingleColumn: checked => dispatch(changeSetting(['forceSingleColumn'], checked)),
 });
 
 const badgeDisplay = (number, limit) => {
@@ -92,8 +88,6 @@ export default class GettingStarted extends ImmutablePureComponent {
     lists: ImmutablePropTypes.list,
     fetchLists: PropTypes.func.isRequired,
     openSettings: PropTypes.func.isRequired,
-    forceSingleColumn: PropTypes.bool,
-    changeForceSingleColumn: PropTypes.func.isRequired,
   };
 
   componentWillMount () {
@@ -108,12 +102,8 @@ export default class GettingStarted extends ImmutablePureComponent {
     }
   }
 
-  handleForceSingleColumnChange = ({ target }) => {
-    this.props.changeForceSingleColumn(target.checked);
-  }
-
   render () {
-    const { intl, myAccount, columns, multiColumn, unreadFollowRequests, unreadNotifications, lists, openSettings, forceSingleColumn } = this.props;
+    const { intl, myAccount, columns, multiColumn, unreadFollowRequests, unreadNotifications, lists, openSettings } = this.props;
 
     const navItems = [];
     let listItems = [];
@@ -193,11 +183,6 @@ export default class GettingStarted extends ImmutablePureComponent {
             </p>
           </div>
         </div>
-
-        <label className='navigational-toggle'>
-          <FormattedMessage id='getting_started.use_simple_layout' defaultMessage='Use simple layout' />
-          <Toggle checked={forceSingleColumn} onChange={this.handleForceSingleColumnChange} />
-        </label>
       </Column>
     );
   }
diff --git a/app/javascript/flavours/glitch/features/search/index.js b/app/javascript/flavours/glitch/features/search/index.js
new file mode 100644
index 000000000..b35c8ed49
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/search/index.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import SearchContainer from 'flavours/glitch/features/compose/containers/search_container';
+import SearchResultsContainer from 'flavours/glitch/features/compose/containers/search_results_container';
+
+const Search = () => (
+  <div className='column search-page'>
+    <SearchContainer />
+
+    <div className='drawer__pager'>
+      <div className='drawer__inner darker'>
+        <SearchResultsContainer />
+      </div>
+    </div>
+  </div>
+);
+
+export default Search;
diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js
index d3d8aebd1..2e28bbe50 100644
--- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js
+++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js
@@ -13,6 +13,8 @@ import ColumnLoading from './column_loading';
 import DrawerLoading from './drawer_loading';
 import BundleColumnError from './bundle_column_error';
 import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, BookmarkedStatuses, ListTimeline } from 'flavours/glitch/util/async-components';
+import ComposePanel from './compose_panel';
+import NavigationPanel from './navigation_panel';
 
 import detectPassiveEvents from 'detect-passive-events';
 import { scrollRight } from 'flavours/glitch/util/scroll';
@@ -173,14 +175,22 @@ export default class ColumnsArea extends ImmutablePureComponent {
 
       return (
         <div className='columns-area__panels'>
-          <div className='columns-area__panels__pane' />
+          <div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
+            <div className='columns-area__panels__pane__inner'>
+              <ComposePanel />
+            </div>
+          </div>
 
           <div className='columns-area__panels__main'>
             <TabsBar key='tabs' />
             {content}
           </div>
 
-          <div className='columns-area__panels__pane' />
+          <div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
+            <div className='columns-area__panels__pane__inner'>
+              <NavigationPanel />
+            </div>
+          </div>
 
           {floatingActionButton}
         </div>
diff --git a/app/javascript/flavours/glitch/features/ui/components/compose_panel.js b/app/javascript/flavours/glitch/features/ui/components/compose_panel.js
new file mode 100644
index 000000000..8acf89950
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/compose_panel.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import SearchContainer from 'flavours/glitch/features/compose/containers/search_container';
+import ComposeFormContainer from 'flavours/glitch/features/compose/containers/compose_form_container';
+import NavigationContainer from 'flavours/glitch/features/compose/containers/navigation_container';
+import { invitesEnabled, version, repository, source_url } from 'mastodon/initial_state';
+import { Link } from 'react-router-dom';
+import { FormattedMessage } from 'react-intl';
+
+const ComposePanel = () => (
+  <div className='compose-panel'>
+    <SearchContainer openInRoute />
+    <NavigationContainer />
+    <ComposeFormContainer />
+
+    <div className='flex-spacer' />
+
+    <div className='getting-started__footer'>
+      <ul>
+        {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
+        <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>
+        <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
+        <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
+        <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
+        <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
+        <li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
+        <li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
+        <li><a href='/auth/sign_out' data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
+      </ul>
+
+      <p>
+        <FormattedMessage
+          id='getting_started.open_source_notice'
+          defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
+          values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
+        />
+      </p>
+    </div>
+  </div>
+);
+
+export default ComposePanel;
diff --git a/app/javascript/flavours/glitch/features/ui/components/list_panel.js b/app/javascript/flavours/glitch/features/ui/components/list_panel.js
new file mode 100644
index 000000000..50592d357
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/list_panel.js
@@ -0,0 +1,55 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { fetchLists } from 'flavours/glitch/actions/lists';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { NavLink, withRouter } from 'react-router-dom';
+import Icon from 'flavours/glitch/components/icon';
+
+const getOrderedLists = createSelector([state => state.get('lists')], lists => {
+  if (!lists) {
+    return lists;
+  }
+
+  return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
+});
+
+const mapStateToProps = state => ({
+  lists: getOrderedLists(state),
+});
+
+export default @withRouter
+@connect(mapStateToProps)
+class ListPanel extends ImmutablePureComponent {
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    lists: ImmutablePropTypes.list,
+  };
+
+  componentDidMount () {
+    const { dispatch } = this.props;
+    dispatch(fetchLists());
+  }
+
+  render () {
+    const { lists } = this.props;
+
+    if (!lists) {
+      return null;
+    }
+
+    return (
+      <div>
+        <hr />
+
+        {lists.map(list => (
+          <NavLink key={list.get('id')} className='column-link column-link--transparent' strict to={`/timelines/list/${list.get('id')}`}><Icon className='column-link__icon' icon='list-ul' fixedWidth />{list.get('title')}</NavLink>
+        ))}
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js
new file mode 100644
index 000000000..68dde7c5c
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import { NavLink, withRouter } from 'react-router-dom';
+import { FormattedMessage } from 'react-intl';
+import Icon from 'flavours/glitch/components/icon';
+import NotificationsCounterIcon from './notifications_counter_icon';
+import ListPanel from './list_panel';
+
+const NavigationPanel = () => (
+  <div className='navigation-panel'>
+    <NavLink className='column-link column-link--transparent' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' icon='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>
+    <NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>
+    <NavLink className='column-link column-link--transparent' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' icon='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>
+    <NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' icon='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
+    <NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' icon='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
+    <NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' icon='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
+    <NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' icon='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
+
+    <ListPanel />
+
+    <hr />
+
+    <a className='column-link column-link--transparent' href='/settings/preferences' target='_blank'><Icon className='column-link__icon' icon='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a>
+    <a className='column-link column-link--transparent' href='/relationships' target='_blank'><Icon className='column-link__icon' icon='address-book-o' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>
+  </div>
+);
+
+export default withRouter(NavigationPanel);
diff --git a/app/javascript/flavours/glitch/features/ui/components/notifications_counter_icon.js b/app/javascript/flavours/glitch/features/ui/components/notifications_counter_icon.js
index 137658b94..25df35264 100644
--- a/app/javascript/flavours/glitch/features/ui/components/notifications_counter_icon.js
+++ b/app/javascript/flavours/glitch/features/ui/components/notifications_counter_icon.js
@@ -10,15 +10,17 @@ const mapStateToProps = state => ({
 
 const formatNumber = num => num > 99 ? '99+' : num;
 
-const NotificationsCounterIcon = ({ count, showBadge }) => (
+const NotificationsCounterIcon = ({ count, className, showBadge }) => (
   <i className='icon-with-badge'>
-    <Icon icon='bell' fixedWidth />
+    <Icon icon='bell' fixedWidth className={className} />
     {showBadge && count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>}
   </i>
 );
 
 NotificationsCounterIcon.propTypes = {
   count: PropTypes.number.isRequired,
+  showBadge: PropTypes.bool,
+  className: PropTypes.string,
 };
 
 export default connect(mapStateToProps)(NotificationsCounterIcon);
diff --git a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js
index 8963b16b3..dbd08aa2b 100644
--- a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js
+++ b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js
@@ -7,14 +7,13 @@ import { isUserTouching } from 'flavours/glitch/util/is_mobile';
 import NotificationsCounterIcon from './notifications_counter_icon';
 
 export const links = [
-  <NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
-  <NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
+  <NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
+  <NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
 
-  <NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
-  <NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
-  <NavLink className='tabs-bar__link primary' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><i className='fa fa-fw fa-search' /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
-
-  <NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><i className='fa fa-fw fa-bars' /></NavLink>,
+  <NavLink className='tabs-bar__link' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
+  <NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
+  <NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><i className='fa fa-fw fa-search' /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
+  <NavLink className='tabs-bar__link' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><i className='fa fa-fw fa-bars' /></NavLink>,
 ];
 
 export function getIndex (path) {
diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js
index 36156a5ef..f08b2dab8 100644
--- a/app/javascript/flavours/glitch/features/ui/index.js
+++ b/app/javascript/flavours/glitch/features/ui/index.js
@@ -44,10 +44,11 @@ import {
   Mutes,
   PinnedStatuses,
   Lists,
+  Search,
   GettingStartedMisc,
 } from 'flavours/glitch/util/async-components';
 import { HotKeys } from 'react-hotkeys';
-import { me } from 'flavours/glitch/util/initial_state';
+import { me, forceSingleColumn } from 'flavours/glitch/util/initial_state';
 import { defineMessages, injectIntl } from 'react-intl';
 
 // Dummy import, to make sure that <Status /> ends up in the application bundle.
@@ -68,7 +69,6 @@ const mapStateToProps = state => ({
   unreadNotifications: state.getIn(['notifications', 'unread']),
   showFaviconBadge: state.getIn(['local_settings', 'notifications', 'favicon_badge']),
   hicolorPrivacyIcons: state.getIn(['local_settings', 'hicolor_privacy_icons']),
-  forceSingleColumn: state.getIn(['settings', 'forceSingleColumn'], false),
 });
 
 const keyMap = {
@@ -126,7 +126,6 @@ export default class UI extends React.Component {
     dropdownMenuIsOpen: PropTypes.bool,
     unreadNotifications: PropTypes.number,
     showFaviconBadge: PropTypes.bool,
-    forceSingleColumn: PropTypes.bool,
   };
 
   state = {
@@ -432,7 +431,7 @@ export default class UI extends React.Component {
 
   render () {
     const { width, draggingOver } = this.state;
-    const { children, layout, isWide, navbarUnder, dropdownMenuIsOpen, forceSingleColumn } = this.props;
+    const { children, layout, isWide, navbarUnder, dropdownMenuIsOpen } = this.props;
     const singleColumn = forceSingleColumn || isMobile(width, layout);
     const redirect = singleColumn ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />;
 
@@ -494,7 +493,7 @@ export default class UI extends React.Component {
               <WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
               <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
 
-              <WrappedRoute path='/search' component={Compose} content={children} componentParams={{ isSearchPage: true }} />
+              <WrappedRoute path='/search' component={Search} content={children} />
 
               <WrappedRoute path='/statuses/new' component={Compose} content={children} />
               <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
diff --git a/app/javascript/flavours/glitch/reducers/settings.js b/app/javascript/flavours/glitch/reducers/settings.js
index a568183df..a37863a69 100644
--- a/app/javascript/flavours/glitch/reducers/settings.js
+++ b/app/javascript/flavours/glitch/reducers/settings.js
@@ -15,8 +15,6 @@ const initialState = ImmutableMap({
 
   skinTone: 1,
 
-  forceSingleColumn: false,
-
   home: ImmutableMap({
     shows: ImmutableMap({
       reblog: true,
diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss
index f372a4830..80d198ff7 100644
--- a/app/javascript/flavours/glitch/styles/components/columns.scss
+++ b/app/javascript/flavours/glitch/styles/components/columns.scss
@@ -26,7 +26,12 @@
       display: flex;
       justify-content: flex-end;
 
+      &--start {
+        justify-content: flex-start;
+      }
+
       &__inner {
+        width: 285px;
         pointer-events: auto;
         height: 100%;
       }
@@ -178,9 +183,31 @@
   padding: 15px;
   text-decoration: none;
 
-  &:hover {
+  &:hover,
+  &:focus,
+  &:active {
     background: lighten($ui-base-color, 11%);
   }
+
+  &:focus {
+    outline: 0;
+  }
+
+  &--transparent {
+    background: transparent;
+    color: $ui-secondary-color;
+
+    &:hover,
+    &:focus,
+    &:active {
+      background: transparent;
+      color: $primary-text-color;
+    }
+
+    &.active {
+      color: $ui-highlight-color;
+    }
+  }
 }
 
 .column-link__icon {
@@ -506,31 +533,3 @@
     margin: 0 5px;
   }
 }
-
-.floating-action-button {
-  position: fixed;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  width: 3.9375rem;
-  height: 3.9375rem;
-  bottom: 1.3125rem;
-  right: 1.3125rem;
-  background: darken($ui-highlight-color, 3%);
-  color: $white;
-  border-radius: 50%;
-  font-size: 21px;
-  line-height: 21px;
-  text-decoration: none;
-  box-shadow: 2px 3px 9px rgba($base-shadow-color, 0.4);
-
-  &:hover,
-  &:focus,
-  &:active {
-    background: lighten($ui-highlight-color, 7%);
-  }
-
-  @media screen and (min-width: 630px) {
-    display: none;
-  }
-}
diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss
index 53b678c9a..81098e52c 100644
--- a/app/javascript/flavours/glitch/styles/components/index.scss
+++ b/app/javascript/flavours/glitch/styles/components/index.scss
@@ -564,6 +564,7 @@
   display: block;
   flex: 1 1 auto;
   padding: 15px 10px;
+  padding-bottom: 13px;
   color: $primary-text-color;
   text-decoration: none;
   text-align: center;
@@ -588,6 +589,7 @@
   &:active {
     @include multi-columns('screen and (min-width: 631px)') {
       background: lighten($ui-base-color, 14%);
+      border-bottom-color: lighten($ui-base-color, 14%);
     }
   }
 
@@ -617,11 +619,21 @@
     padding: 0;
   }
 
-  .search__input,
   .autosuggest-textarea__textarea {
     font-size: 16px;
   }
 
+  .search__input {
+    line-height: 18px;
+    font-size: 16px;
+    padding: 15px;
+    padding-right: 30px;
+  }
+
+  .search__icon .fa {
+    top: 15px;
+  }
+
   @media screen and (min-width: 360px) {
     padding: 10px 0;
   }
@@ -677,6 +689,58 @@
         margin-top: 10px;
       }
     }
+
+    .account {
+      padding: 15px 10px;
+    }
+
+    .notification {
+      &__message {
+        margin-left: 48px + 15px * 2;
+        padding-top: 15px;
+      }
+
+      &__favourite-icon-wrapper {
+        left: -32px;
+      }
+
+      .status {
+        padding-top: 8px;
+      }
+
+      .account {
+        padding-top: 8px;
+      }
+
+      .account__avatar-wrapper {
+        margin-left: 17px;
+        margin-right: 15px;
+      }
+    }
+  }
+}
+
+.floating-action-button {
+  position: fixed;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 3.9375rem;
+  height: 3.9375rem;
+  bottom: 1.3125rem;
+  right: 1.3125rem;
+  background: darken($ui-highlight-color, 3%);
+  color: $white;
+  border-radius: 50%;
+  font-size: 21px;
+  line-height: 21px;
+  text-decoration: none;
+  box-shadow: 2px 3px 9px rgba($base-shadow-color, 0.4);
+
+  &:hover,
+  &:focus,
+  &:active {
+    background: lighten($ui-highlight-color, 7%);
   }
 }
 
@@ -698,12 +762,41 @@
   }
 }
 
+@media screen and (max-width: 600px + (285px * 1) + (10px * 1)) {
+  .columns-area__panels__pane--compositional {
+    display: none;
+  }
+}
+
+@media screen and (min-width: 600px + (285px * 1) + (10px * 1)) {
+  .floating-action-button,
+  .tabs-bar__link.optional {
+    display: none;
+  }
+
+  .search-page .search {
+    display: none;
+  }
+}
+
+@media screen and (max-width: 600px + (285px * 2) + (10px * 2)) {
+  .columns-area__panels__pane--navigational {
+    display: none;
+  }
+}
+
+@media screen and (min-width: 600px + (285px * 2) + (10px * 2)) {
+  .tabs-bar {
+    display: none;
+  }
+}
+
 .icon-with-badge {
   position: relative;
 
   &__badge {
     position: absolute;
-    right: -13px;
+    left: 9px;
     top: -13px;
     background: $ui-highlight-color;
     border: 2px solid lighten($ui-base-color, 8%);
@@ -716,6 +809,57 @@
   }
 }
 
+.column-link--transparent .icon-with-badge__badge {
+  border-color: darken($ui-base-color, 8%);
+}
+
+.compose-panel {
+  width: 285px;
+  margin-top: 10px;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+
+  .search__input {
+    line-height: 18px;
+    font-size: 16px;
+    padding: 15px;
+    padding-right: 30px;
+  }
+
+  .search__icon .fa {
+    top: 15px;
+  }
+
+  .navigation-bar {
+    padding-top: 20px;
+    padding-bottom: 20px;
+  }
+
+  .flex-spacer {
+    background: transparent;
+  }
+
+  .autosuggest-textarea__textarea {
+    max-height: 200px;
+  }
+
+  .compose-form__upload-thumbnail {
+    height: 80px;
+  }
+}
+
+.navigation-panel {
+  margin-top: 10px;
+
+  hr {
+    border: 0;
+    background: transparent;
+    border-top: 1px solid lighten($ui-base-color, 4%);
+    margin: 10px 0;
+  }
+}
+
 .scrollable {
   overflow-y: scroll;
   overflow-x: hidden;
@@ -1386,15 +1530,6 @@
   height: 1em;
 }
 
-.navigational-toggle {
-  padding: 10px;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  font-size: 14px;
-  color: $dark-text-color;
-}
-
 .layout-toggle {
   display: flex;
   padding: 5px;
diff --git a/app/javascript/flavours/glitch/util/async-components.js b/app/javascript/flavours/glitch/util/async-components.js
index 094952204..f2aeda834 100644
--- a/app/javascript/flavours/glitch/util/async-components.js
+++ b/app/javascript/flavours/glitch/util/async-components.js
@@ -149,3 +149,7 @@ export function GettingStartedMisc () {
 export function ListAdder () {
   return import(/* webpackChunkName: "features/glitch/async/list_adder" */'flavours/glitch/features/list_adder');
 }
+
+export function Search () {
+  return import(/*webpackChunkName: "features/glitch/async/search" */'flavours/glitch/features/search');
+}
diff --git a/app/javascript/flavours/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js
index 99d8a4dbc..72fd2e6f4 100644
--- a/app/javascript/flavours/glitch/util/initial_state.js
+++ b/app/javascript/flavours/glitch/util/initial_state.js
@@ -28,5 +28,6 @@ export const version = getMeta('version');
 export const mascot = getMeta('mascot');
 export const isStaff = getMeta('is_staff');
 export const defaultContentType = getMeta('default_content_type');
+export const forceSingleColumn = !getMeta('advanced_layout');
 
 export default initialState;