about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app/javascript/mastodon/components/not_signed_in_indicator.js12
-rw-r--r--app/javascript/mastodon/features/about/index.js34
-rw-r--r--app/javascript/mastodon/features/compose/index.js87
-rw-r--r--app/javascript/mastodon/features/explore/index.js29
-rw-r--r--app/javascript/mastodon/features/getting_started/index.js104
-rw-r--r--app/javascript/mastodon/features/home_timeline/index.js30
-rw-r--r--app/javascript/mastodon/features/keyboard_shortcuts/index.js13
-rw-r--r--app/javascript/mastodon/features/notifications/index.js61
-rw-r--r--app/javascript/mastodon/features/ui/components/columns_area.js84
-rw-r--r--app/javascript/mastodon/features/ui/components/compose_panel.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/header.js53
-rw-r--r--app/javascript/mastodon/features/ui/components/link_footer.js45
-rw-r--r--app/javascript/mastodon/features/ui/components/navigation_panel.js16
-rw-r--r--app/javascript/mastodon/features/ui/components/tabs_bar.js86
-rw-r--r--app/javascript/mastodon/features/ui/index.js5
-rw-r--r--app/javascript/mastodon/features/ui/util/async-components.js4
-rw-r--r--app/javascript/mastodon/locales/en.json6
-rw-r--r--app/javascript/styles/mastodon/components.scss270
-rw-r--r--app/javascript/styles/mastodon/variables.scss2
19 files changed, 479 insertions, 464 deletions
diff --git a/app/javascript/mastodon/components/not_signed_in_indicator.js b/app/javascript/mastodon/components/not_signed_in_indicator.js
new file mode 100644
index 000000000..b440c6be2
--- /dev/null
+++ b/app/javascript/mastodon/components/not_signed_in_indicator.js
@@ -0,0 +1,12 @@
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+
+const NotSignedInIndicator = () => (
+  <div className='scrollable scrollable--flex'>
+    <div className='empty-column-indicator'>
+      <FormattedMessage id='not_signed_in_indicator.not_signed_in' defaultMessage='You need to sign in to access this resource.' />
+    </div>
+  </div>
+);
+
+export default NotSignedInIndicator;
diff --git a/app/javascript/mastodon/features/about/index.js b/app/javascript/mastodon/features/about/index.js
new file mode 100644
index 000000000..c48d81d9a
--- /dev/null
+++ b/app/javascript/mastodon/features/about/index.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import PropTypes from 'prop-types';
+import Column from 'mastodon/components/column';
+import LinkFooter from 'mastodon/features/ui/components/link_footer';
+import { Helmet } from 'react-helmet';
+import { title } from 'mastodon/initial_state';
+
+const messages = defineMessages({
+  title: { id: 'column.about', defaultMessage: 'About' },
+});
+
+export default @injectIntl
+class About extends React.PureComponent {
+
+  static propTypes = {
+    intl: PropTypes.object.isRequired,
+  };
+
+  render () {
+    const { intl } = this.props;
+
+    return (
+      <Column>
+        <LinkFooter />
+
+        <Helmet>
+          <title>{intl.formatMessage(messages.title)} - {title}</title>
+        </Helmet>
+      </Column>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js
index 711a55904..c27556a0e 100644
--- a/app/javascript/mastodon/features/compose/index.js
+++ b/app/javascript/mastodon/features/compose/index.js
@@ -17,6 +17,7 @@ import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
 import { mascot } from '../../initial_state';
 import Icon from 'mastodon/components/icon';
 import { logOut } from 'mastodon/utils/log_out';
+import Column from 'mastodon/components/column';
 
 const messages = defineMessages({
   start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
@@ -92,57 +93,59 @@ class Compose extends React.PureComponent {
   render () {
     const { multiColumn, showSearch, isSearchPage, intl } = this.props;
 
-    let header = '';
-
     if (multiColumn) {
       const { columns } = this.props;
-      header = (
-        <nav className='drawer__header'>
-          <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><Icon id='bars' fixedWidth /></Link>
-          {!columns.some(column => column.get('id') === 'HOME') && (
-            <Link to='/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><Icon id='home' fixedWidth /></Link>
-          )}
-          {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
-            <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' fixedWidth /></Link>
-          )}
-          {!columns.some(column => column.get('id') === 'COMMUNITY') && (
-            <Link to='/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' fixedWidth /></Link>
-          )}
-          {!columns.some(column => column.get('id') === 'PUBLIC') && (
-            <Link to='/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link>
-          )}
-          <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' fixedWidth /></a>
-          <a href='/auth/sign_out' className='drawer__tab' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}><Icon id='sign-out' fixedWidth /></a>
-        </nav>
-      );
-    }
 
-    return (
-      <div className='drawer' role='region' aria-label={intl.formatMessage(messages.compose)}>
-        {header}
-
-        {(multiColumn || isSearchPage) && <SearchContainer /> }
+      return (
+        <div className='drawer' role='region' aria-label={intl.formatMessage(messages.compose)}>
+          <nav className='drawer__header'>
+            <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><Icon id='bars' fixedWidth /></Link>
+            {!columns.some(column => column.get('id') === 'HOME') && (
+              <Link to='/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><Icon id='home' fixedWidth /></Link>
+            )}
+            {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
+              <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' fixedWidth /></Link>
+            )}
+            {!columns.some(column => column.get('id') === 'COMMUNITY') && (
+              <Link to='/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' fixedWidth /></Link>
+            )}
+            {!columns.some(column => column.get('id') === 'PUBLIC') && (
+              <Link to='/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link>
+            )}
+            <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' fixedWidth /></a>
+            <a href='/auth/sign_out' className='drawer__tab' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}><Icon id='sign-out' fixedWidth /></a>
+          </nav>
 
-        <div className='drawer__pager'>
-          {!isSearchPage && <div className='drawer__inner' onFocus={this.onFocus}>
-            <NavigationContainer onClose={this.onBlur} />
+          {(multiColumn || isSearchPage) && <SearchContainer /> }
 
-            <ComposeFormContainer />
+          <div className='drawer__pager'>
+            {!isSearchPage && <div className='drawer__inner' onFocus={this.onFocus}>
+              <NavigationContainer onClose={this.onBlur} />
 
-            <div className='drawer__inner__mastodon'>
-              <img alt='' draggable='false' src={mascot || elephantUIPlane} />
-            </div>
-          </div>}
+              <ComposeFormContainer />
 
-          <Motion defaultStyle={{ x: isSearchPage ? 0 : -100 }} style={{ x: spring(showSearch || isSearchPage ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
-            {({ x }) => (
-              <div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
-                <SearchResultsContainer />
+              <div className='drawer__inner__mastodon'>
+                <img alt='' draggable='false' src={mascot || elephantUIPlane} />
               </div>
-            )}
-          </Motion>
+            </div>}
+
+            <Motion defaultStyle={{ x: isSearchPage ? 0 : -100 }} style={{ x: spring(showSearch || isSearchPage ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
+              {({ x }) => (
+                <div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
+                  <SearchResultsContainer />
+                </div>
+              )}
+            </Motion>
+          </div>
         </div>
-      </div>
+      );
+    }
+
+    return (
+      <Column onFocus={this.onFocus}>
+        <NavigationContainer onClose={this.onBlur} />
+        <ComposeFormContainer />
+      </Column>
     );
   }
 
diff --git a/app/javascript/mastodon/features/explore/index.js b/app/javascript/mastodon/features/explore/index.js
index e1d1eb563..21bb6e828 100644
--- a/app/javascript/mastodon/features/explore/index.js
+++ b/app/javascript/mastodon/features/explore/index.js
@@ -30,13 +30,13 @@ class Explore extends React.PureComponent {
 
   static contextTypes = {
     router: PropTypes.object,
+    identity: PropTypes.object,
   };
 
   static propTypes = {
     intl: PropTypes.object.isRequired,
     multiColumn: PropTypes.bool,
     isSearching: PropTypes.bool,
-    layout: PropTypes.string,
   };
 
   handleHeaderClick = () => {
@@ -48,22 +48,21 @@ class Explore extends React.PureComponent {
   }
 
   render () {
-    const { intl, multiColumn, isSearching, layout } = this.props;
+    const { intl, multiColumn, isSearching } = this.props;
+    const { signedIn } = this.context.identity;
 
     return (
       <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
-        {layout === 'mobile' ? (
-          <div className='explore__search-header'>
-            <Search />
-          </div>
-        ) : (
-          <ColumnHeader
-            icon={isSearching ? 'search' : 'hashtag'}
-            title={intl.formatMessage(isSearching ? messages.searchResults : messages.title)}
-            onClick={this.handleHeaderClick}
-            multiColumn={multiColumn}
-          />
-        )}
+        <ColumnHeader
+          icon={isSearching ? 'search' : 'hashtag'}
+          title={intl.formatMessage(isSearching ? messages.searchResults : messages.title)}
+          onClick={this.handleHeaderClick}
+          multiColumn={multiColumn}
+        />
+
+        <div className='explore__search-header'>
+          <Search />
+        </div>
 
         <div className='scrollable scrollable--flex'>
           {isSearching ? (
@@ -74,7 +73,7 @@ class Explore extends React.PureComponent {
                 <NavLink exact to='/explore'><FormattedMessage id='explore.trending_statuses' defaultMessage='Posts' /></NavLink>
                 <NavLink exact to='/explore/tags'><FormattedMessage id='explore.trending_tags' defaultMessage='Hashtags' /></NavLink>
                 <NavLink exact to='/explore/links'><FormattedMessage id='explore.trending_links' defaultMessage='News' /></NavLink>
-                <NavLink exact to='/explore/suggestions'><FormattedMessage id='explore.suggested_follows' defaultMessage='For you' /></NavLink>
+                {signedIn && <NavLink exact to='/explore/suggestions'><FormattedMessage id='explore.suggested_follows' defaultMessage='For you' /></NavLink>}
               </div>
 
               <Switch>
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index 65cee7498..d998127a2 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -1,19 +1,20 @@
 import React from 'react';
-import Column from '../ui/components/column';
+import Column from 'mastodon/components/column';
+import ColumnHeader from 'mastodon/components/column_header';
 import ColumnLink from '../ui/components/column_link';
 import ColumnSubheading from '../ui/components/column_subheading';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { defineMessages, injectIntl } from 'react-intl';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { me, showTrends } from '../../initial_state';
+import { me, title, showTrends } from '../../initial_state';
 import { fetchFollowRequests } from 'mastodon/actions/accounts';
 import { List as ImmutableList } from 'immutable';
 import NavigationContainer from '../compose/containers/navigation_container';
-import Icon from 'mastodon/components/icon';
 import LinkFooter from 'mastodon/features/ui/components/link_footer';
 import TrendsContainer from './containers/trends_container';
+import { Helmet } from 'react-helmet';
 
 const messages = defineMessages({
   home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
@@ -40,7 +41,6 @@ const messages = defineMessages({
 
 const mapStateToProps = state => ({
   myAccount: state.getIn(['accounts', me]),
-  columns: state.getIn(['settings', 'columns']),
   unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
 });
 
@@ -58,20 +58,18 @@ const badgeDisplay = (number, limit) => {
   }
 };
 
-const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
-
 export default @connect(mapStateToProps, mapDispatchToProps)
 @injectIntl
 class GettingStarted extends ImmutablePureComponent {
 
   static contextTypes = {
     router: PropTypes.object.isRequired,
+    identity: PropTypes.object,
   };
 
   static propTypes = {
     intl: PropTypes.object.isRequired,
-    myAccount: ImmutablePropTypes.map.isRequired,
-    columns: ImmutablePropTypes.list,
+    myAccount: ImmutablePropTypes.map,
     multiColumn: PropTypes.bool,
     fetchFollowRequests: PropTypes.func.isRequired,
     unreadFollowRequests: PropTypes.number,
@@ -79,10 +77,10 @@ class GettingStarted extends ImmutablePureComponent {
   };
 
   componentDidMount () {
-    const { fetchFollowRequests, multiColumn } = this.props;
+    const { fetchFollowRequests } = this.props;
+    const { signedIn } = this.context.identity;
 
-    if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
-      this.context.router.history.replace('/home');
+    if (!signedIn) {
       return;
     }
 
@@ -90,91 +88,57 @@ class GettingStarted extends ImmutablePureComponent {
   }
 
   render () {
-    const { intl, myAccount, columns, multiColumn, unreadFollowRequests } = this.props;
+    const { intl, myAccount, multiColumn, unreadFollowRequests } = this.props;
+    const { signedIn } = this.context.identity;
 
     const navItems = [];
-    let height = (multiColumn) ? 0 : 60;
-
-    if (multiColumn) {
-      navItems.push(
-        <ColumnSubheading key='header-discover' text={intl.formatMessage(messages.discover)} />,
-      );
-      height += 34;
-    }
 
     navItems.push(
+      <ColumnSubheading key='header-discover' text={intl.formatMessage(messages.discover)} />,
       <ColumnLink key='explore' icon='hashtag' text={intl.formatMessage(messages.explore)} to='/explore' />,
+      <ColumnLink key='community_timeline' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/public/local' />,
+      <ColumnLink key='public_timeline' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/public' />,
     );
-    height += 48;
-
-    if (multiColumn) {
-      navItems.push(
-        <ColumnLink key='community_timeline' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/public/local' />,
-        <ColumnLink key='public_timeline' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/public' />,
-      );
-
-      height += 48*2;
 
+    if (signedIn) {
       navItems.push(
         <ColumnSubheading key='header-personal' text={intl.formatMessage(messages.personal)} />,
-      );
-
-      height += 34;
-    }
-
-    if (multiColumn && !columns.find(item => item.get('id') === 'HOME')) {
-      navItems.push(
         <ColumnLink key='home' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/home' />,
+        <ColumnLink key='direct' icon='at' text={intl.formatMessage(messages.direct)} to='/conversations' />,
+        <ColumnLink key='bookmark' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />,
+        <ColumnLink key='favourites' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
+        <ColumnLink key='lists' icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />,
       );
-      height += 48;
-    }
-
-    navItems.push(
-      <ColumnLink key='direct' icon='at' text={intl.formatMessage(messages.direct)} to='/conversations' />,
-      <ColumnLink key='bookmark' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />,
-      <ColumnLink key='favourites' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
-      <ColumnLink key='lists' icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />,
-    );
 
-    height += 48*4;
+      if (myAccount.get('locked') || unreadFollowRequests > 0) {
+        navItems.push(<ColumnLink key='follow_requests' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
+      }
 
-    if (myAccount.get('locked') || unreadFollowRequests > 0) {
-      navItems.push(<ColumnLink key='follow_requests' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
-      height += 48;
-    }
-
-    if (!multiColumn) {
       navItems.push(
         <ColumnSubheading key='header-settings' text={intl.formatMessage(messages.settings_subheading)} />,
         <ColumnLink key='preferences' icon='gears' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />,
       );
-
-      height += 34 + 48;
     }
 
     return (
-      <Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.menu)}>
-        {multiColumn && <div className='column-header__wrapper'>
-          <h1 className='column-header'>
-            <button>
-              <Icon id='bars' className='column-header__icon' fixedWidth />
-              <FormattedMessage id='getting_started.heading' defaultMessage='Getting started' />
-            </button>
-          </h1>
-        </div>}
-
-        <div className='getting-started'>
-          <div className='getting-started__wrapper' style={{ height }}>
-            {!multiColumn && <NavigationContainer />}
+      <Column>
+        {(signedIn && !multiColumn) ? <NavigationContainer /> : <ColumnHeader title={intl.formatMessage(messages.menu)} icon='bars' multiColumn={multiColumn} />}
+
+        <div className='getting-started scrollable scrollable--flex'>
+          <div className='getting-started__wrapper'>
             {navItems}
           </div>
 
           {!multiColumn && <div className='flex-spacer' />}
 
-          <LinkFooter withHotkeys={multiColumn} />
+          <LinkFooter />
         </div>
 
-        {multiColumn && showTrends && <TrendsContainer />}
+        {(multiColumn && showTrends) && <TrendsContainer />}
+
+        <Helmet>
+          <title>{intl.formatMessage(messages.menu)} - {title}</title>
+        </Helmet>
       </Column>
     );
   }
diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js
index dc440f2fe..aac92244d 100644
--- a/app/javascript/mastodon/features/home_timeline/index.js
+++ b/app/javascript/mastodon/features/home_timeline/index.js
@@ -13,6 +13,9 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/an
 import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
 import classNames from 'classnames';
 import IconWithBadge from 'mastodon/components/icon_with_badge';
+import NotSignedInIndicator from 'mastodon/components/not_signed_in_indicator';
+import { Helmet } from 'react-helmet';
+import { title } from 'mastodon/initial_state';
 
 const messages = defineMessages({
   title: { id: 'column.home', defaultMessage: 'Home' },
@@ -32,6 +35,10 @@ export default @connect(mapStateToProps)
 @injectIntl
 class HomeTimeline extends React.PureComponent {
 
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
   static propTypes = {
     dispatch: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
@@ -113,6 +120,7 @@ class HomeTimeline extends React.PureComponent {
   render () {
     const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
     const pinned = !!columnId;
+    const { signedIn } = this.context.identity;
 
     let announcementsButton = null;
 
@@ -147,14 +155,20 @@ class HomeTimeline extends React.PureComponent {
           <ColumnSettingsContainer />
         </ColumnHeader>
 
-        <StatusListContainer
-          trackScroll={!pinned}
-          scrollKey={`home_timeline-${columnId}`}
-          onLoadMore={this.handleLoadMore}
-          timelineId='home'
-          emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up. {suggestions}' values={{ suggestions: <Link to='/start'><FormattedMessage id='empty_column.home.suggestions' defaultMessage='See some suggestions' /></Link> }} />}
-          bindToDocument={!multiColumn}
-        />
+        {signedIn ? (
+          <StatusListContainer
+            trackScroll={!pinned}
+            scrollKey={`home_timeline-${columnId}`}
+            onLoadMore={this.handleLoadMore}
+            timelineId='home'
+            emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up. {suggestions}' values={{ suggestions: <Link to='/start'><FormattedMessage id='empty_column.home.suggestions' defaultMessage='See some suggestions' /></Link> }} />}
+            bindToDocument={!multiColumn}
+          />
+        ) : <NotSignedInIndicator />}
+
+        <Helmet>
+          <title>{intl.formatMessage(messages.title)} - {title}</title>
+        </Helmet>
       </Column>
     );
   }
diff --git a/app/javascript/mastodon/features/keyboard_shortcuts/index.js b/app/javascript/mastodon/features/keyboard_shortcuts/index.js
index 8f1631d82..2a32577ba 100644
--- a/app/javascript/mastodon/features/keyboard_shortcuts/index.js
+++ b/app/javascript/mastodon/features/keyboard_shortcuts/index.js
@@ -1,9 +1,9 @@
 import React from 'react';
-import Column from '../ui/components/column';
-import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+import Column from 'mastodon/components/column';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import PropTypes from 'prop-types';
 import ImmutablePureComponent from 'react-immutable-pure-component';
+import ColumnHeader from 'mastodon/components/column_header';
 
 const messages = defineMessages({
   heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' },
@@ -21,8 +21,13 @@ class KeyboardShortcuts extends ImmutablePureComponent {
     const { intl, multiColumn } = this.props;
 
     return (
-      <Column bindToDocument={!multiColumn} icon='question' heading={intl.formatMessage(messages.heading)}>
-        <ColumnBackButtonSlim />
+      <Column>
+        <ColumnHeader
+          title={intl.formatMessage(messages.heading)}
+          icon='question'
+          multiColumn={multiColumn}
+        />
+
         <div className='keyboard-shortcuts scrollable optionally-scrollable'>
           <table>
             <thead>
diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js
index a6a277d7e..d9f8101c3 100644
--- a/app/javascript/mastodon/features/notifications/index.js
+++ b/app/javascript/mastodon/features/notifications/index.js
@@ -26,6 +26,9 @@ import LoadGap from '../../components/load_gap';
 import Icon from 'mastodon/components/icon';
 import compareId from 'mastodon/compare_id';
 import NotificationsPermissionBanner from './components/notifications_permission_banner';
+import NotSignedInIndicator from 'mastodon/components/not_signed_in_indicator';
+import { Helmet } from 'react-helmet';
+import { title } from 'mastodon/initial_state';
 
 const messages = defineMessages({
   title: { id: 'column.notifications', defaultMessage: 'Notifications' },
@@ -69,6 +72,10 @@ export default @connect(mapStateToProps)
 @injectIntl
 class Notifications extends React.PureComponent {
 
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
   static propTypes = {
     columnId: PropTypes.string,
     notifications: ImmutablePropTypes.list.isRequired,
@@ -178,10 +185,11 @@ class Notifications extends React.PureComponent {
     const { intl, notifications, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props;
     const pinned = !!columnId;
     const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. When other people interact with you, you will see it here." />;
+    const { signedIn } = this.context.identity;
 
     let scrollableContent = null;
 
-    const filterBarContainer = showFilterBar
+    const filterBarContainer = (signedIn && showFilterBar)
       ? (<FilterBarContainer />)
       : null;
 
@@ -211,26 +219,32 @@ class Notifications extends React.PureComponent {
 
     this.scrollableContent = scrollableContent;
 
-    const scrollContainer = (
-      <ScrollableList
-        scrollKey={`notifications-${columnId}`}
-        trackScroll={!pinned}
-        isLoading={isLoading}
-        showLoading={isLoading && notifications.size === 0}
-        hasMore={hasMore}
-        numPending={numPending}
-        prepend={needsNotificationPermission && <NotificationsPermissionBanner />}
-        alwaysPrepend
-        emptyMessage={emptyMessage}
-        onLoadMore={this.handleLoadOlder}
-        onLoadPending={this.handleLoadPending}
-        onScrollToTop={this.handleScrollToTop}
-        onScroll={this.handleScroll}
-        bindToDocument={!multiColumn}
-      >
-        {scrollableContent}
-      </ScrollableList>
-    );
+    let scrollContainer;
+
+    if (signedIn) {
+      scrollContainer = (
+        <ScrollableList
+          scrollKey={`notifications-${columnId}`}
+          trackScroll={!pinned}
+          isLoading={isLoading}
+          showLoading={isLoading && notifications.size === 0}
+          hasMore={hasMore}
+          numPending={numPending}
+          prepend={needsNotificationPermission && <NotificationsPermissionBanner />}
+          alwaysPrepend
+          emptyMessage={emptyMessage}
+          onLoadMore={this.handleLoadOlder}
+          onLoadPending={this.handleLoadPending}
+          onScrollToTop={this.handleScrollToTop}
+          onScroll={this.handleScroll}
+          bindToDocument={!multiColumn}
+        >
+          {scrollableContent}
+        </ScrollableList>
+      );
+    } else {
+      scrollContainer = <NotSignedInIndicator />;
+    }
 
     let extraButton = null;
 
@@ -262,8 +276,13 @@ class Notifications extends React.PureComponent {
         >
           <ColumnSettingsContainer />
         </ColumnHeader>
+
         {filterBarContainer}
         {scrollContainer}
+
+        <Helmet>
+          <title>{intl.formatMessage(messages.title)} - {title}</title>
+        </Helmet>
       </Column>
     );
   }
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index 83e10e003..cc1bc83e0 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -3,13 +3,7 @@ import PropTypes from 'prop-types';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-
-import ReactSwipeableViews from 'react-swipeable-views';
-import TabsBar, { links, getIndex, getLink } from './tabs_bar';
 import { Link } from 'react-router-dom';
-
-import { disableSwiping } from 'mastodon/initial_state';
-
 import BundleContainer from '../containers/bundle_container';
 import ColumnLoading from './column_loading';
 import DrawerLoading from './drawer_loading';
@@ -71,20 +65,13 @@ class ColumnsArea extends ImmutablePureComponent {
     children: PropTypes.node,
   };
 
-   // Corresponds to (max-width: 600px + (285px * 1) + (10px * 1)) in SCSS
-   mediaQuery = 'matchMedia' in window && window.matchMedia('(max-width: 895px)');
+  // Corresponds to (max-width: $no-gap-breakpoint + 285px - 1px) in SCSS
+  mediaQuery = 'matchMedia' in window && window.matchMedia('(max-width: 1174px)');
 
   state = {
-    shouldAnimate: false,
     renderComposePanel: !(this.mediaQuery && this.mediaQuery.matches),
   }
 
-  componentWillReceiveProps() {
-    if (typeof this.pendingIndex !== 'number' && this.lastIndex !== getIndex(this.context.router.history.location.pathname)) {
-      this.setState({ shouldAnimate: false });
-    }
-  }
-
   componentDidMount() {
     if (!this.props.singleColumn) {
       this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
@@ -99,10 +86,7 @@ class ColumnsArea extends ImmutablePureComponent {
       this.setState({ renderComposePanel: !this.mediaQuery.matches });
     }
 
-    this.lastIndex   = getIndex(this.context.router.history.location.pathname);
     this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
-
-    this.setState({ shouldAnimate: true });
   }
 
   componentWillUpdate(nextProps) {
@@ -115,13 +99,6 @@ class ColumnsArea extends ImmutablePureComponent {
     if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
       this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
     }
-
-    const newIndex = getIndex(this.context.router.history.location.pathname);
-
-    if (this.lastIndex !== newIndex) {
-      this.lastIndex = newIndex;
-      this.setState({ shouldAnimate: true });
-    }
   }
 
   componentWillUnmount () {
@@ -149,31 +126,6 @@ class ColumnsArea extends ImmutablePureComponent {
     this.setState({ renderComposePanel: !e.matches });
   }
 
-  handleSwipe = (index) => {
-    this.pendingIndex = index;
-
-    const nextLinkTranslationId = links[index].props['data-preview-title-id'];
-    const currentLinkSelector = '.tabs-bar__link.active';
-    const nextLinkSelector = `.tabs-bar__link[data-preview-title-id="${nextLinkTranslationId}"]`;
-
-    // HACK: Remove the active class from the current link and set it to the next one
-    // React-router does this for us, but too late, feeling laggy.
-    document.querySelector(currentLinkSelector).classList.remove('active');
-    document.querySelector(nextLinkSelector).classList.add('active');
-
-    if (!this.state.shouldAnimate && typeof this.pendingIndex === 'number') {
-      this.context.router.history.push(getLink(this.pendingIndex));
-      this.pendingIndex = null;
-    }
-  }
-
-  handleAnimationEnd = () => {
-    if (typeof this.pendingIndex === 'number') {
-      this.context.router.history.push(getLink(this.pendingIndex));
-      this.pendingIndex = null;
-    }
-  }
-
   handleWheel = () => {
     if (typeof this._interruptScrollAnimation !== 'function') {
       return;
@@ -186,22 +138,6 @@ class ColumnsArea extends ImmutablePureComponent {
     this.node = node;
   }
 
-  renderView = (link, index) => {
-    const columnIndex = getIndex(this.context.router.history.location.pathname);
-    const title = this.props.intl.formatMessage({ id: link.props['data-preview-title-id'] });
-    const icon = link.props['data-preview-icon'];
-
-    const view = (index === columnIndex) ?
-      React.cloneElement(this.props.children) :
-      <ColumnLoading title={title} icon={icon} />;
-
-    return (
-      <div className='columns-area columns-area--mobile' key={index}>
-        {view}
-      </div>
-    );
-  }
-
   renderLoading = columnId => () => {
     return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading />;
   }
@@ -212,22 +148,12 @@ class ColumnsArea extends ImmutablePureComponent {
 
   render () {
     const { columns, children, singleColumn, isModalOpen, intl } = this.props;
-    const { shouldAnimate, renderComposePanel } = this.state;
+    const { renderComposePanel } = this.state;
     const { signedIn } = this.context.identity;
 
-    const columnIndex = getIndex(this.context.router.history.location.pathname);
-
     if (singleColumn) {
       const floatingActionButton = (!signedIn || shouldHideFAB(this.context.router.history.location.pathname)) ? null : <Link key='floating-action-button' to='/publish' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
 
-      const content = columnIndex !== -1 ? (
-        <ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }} disabled={disableSwiping}>
-          {links.map(this.renderView)}
-        </ReactSwipeableViews>
-      ) : (
-        <div key='content' className='columns-area columns-area--mobile'>{children}</div>
-      );
-
       return (
         <div className='columns-area__panels'>
           <div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
@@ -237,8 +163,8 @@ class ColumnsArea extends ImmutablePureComponent {
           </div>
 
           <div className={`columns-area__panels__main ${floatingActionButton && 'with-fab'}`}>
-            <TabsBar key='tabs' />
-            {content}
+            <div className='tabs-bar__wrapper'><div id='tabs-bar__portal' /></div>
+            <div className='columns-area columns-area--mobile'>{children}</div>
           </div>
 
           <div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
diff --git a/app/javascript/mastodon/features/ui/components/compose_panel.js b/app/javascript/mastodon/features/ui/components/compose_panel.js
index 1c128188f..655c202fa 100644
--- a/app/javascript/mastodon/features/ui/components/compose_panel.js
+++ b/app/javascript/mastodon/features/ui/components/compose_panel.js
@@ -46,7 +46,7 @@ class ComposePanel extends React.PureComponent {
           </React.Fragment>
         )}
 
-        <LinkFooter withHotkeys />
+        <LinkFooter />
       </div>
     );
   }
diff --git a/app/javascript/mastodon/features/ui/components/header.js b/app/javascript/mastodon/features/ui/components/header.js
new file mode 100644
index 000000000..cddab820c
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/header.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import Logo from 'mastodon/components/logo';
+import { Link } from 'react-router-dom';
+import { FormattedMessage } from 'react-intl';
+import { registrationsOpen, me } from 'mastodon/initial_state';
+import Avatar from 'mastodon/components/avatar';
+import Permalink from 'mastodon/components/permalink';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+
+const Account = connect(state => ({
+  account: state.getIn(['accounts', me]),
+}))(({ account }) => (
+  <Permalink href={account.get('url')} to={`/@${account.get('acct')}`}>
+    <span style={{ display: 'none' }}>{account.get('acct')}</span>
+    <Avatar account={account} size={35} />
+  </Permalink>
+));
+
+export default class Header extends React.PureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
+
+  render () {
+    const { signedIn } = this.context.identity;
+
+    let content;
+
+    if (signedIn) {
+      content = <Account />;
+    } else {
+      content = (
+        <React.Fragment>
+          <a href='/auth/sign_in' className='button'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
+          <a href={registrationsOpen ? '/auth/sign_up' : 'https://joinmastodon.org/servers'} className='button button-tertiary'><FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /></a>
+        </React.Fragment>
+      );
+    }
+
+    return (
+      <div className='ui__header'>
+        <Link to='/' className='ui__header__logo'><Logo /></Link>
+
+        <div className='ui__header__links'>
+          {content}
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/ui/components/link_footer.js b/app/javascript/mastodon/features/ui/components/link_footer.js
index dd05d03dd..2b092a182 100644
--- a/app/javascript/mastodon/features/ui/components/link_footer.js
+++ b/app/javascript/mastodon/features/ui/components/link_footer.js
@@ -3,7 +3,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
 import { Link } from 'react-router-dom';
-import { limitedFederationMode, version, repository, source_url, profile_directory as profileDirectory } from 'mastodon/initial_state';
+import { version, repository, source_url, profile_directory as profileDirectory } from 'mastodon/initial_state';
 import { logOut } from 'mastodon/utils/log_out';
 import { openModal } from 'mastodon/actions/modal';
 import { PERMISSION_INVITE_USERS } from 'mastodon/permissions';
@@ -33,7 +33,6 @@ class LinkFooter extends React.PureComponent {
   };
 
   static propTypes = {
-    withHotkeys: PropTypes.bool,
     onLogout: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
   };
@@ -48,40 +47,26 @@ class LinkFooter extends React.PureComponent {
   }
 
   render () {
-    const { withHotkeys } = this.props;
     const { signedIn, permissions } = this.context.identity;
     const items = [];
 
-    if ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS) {
-      items.push(<a key='invites' href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a>);
-    }
-
-    if (signedIn && withHotkeys) {
-      items.push(<Link key='hotkeys' to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link>);
-    }
-
-    if (signedIn) {
-      items.push(<a key='security' href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a>);
-    }
-
-    if (!limitedFederationMode) {
-      items.push(<a key='about' href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a>);
-    }
+    items.push(<a key='apps' href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Get the app' /></a>);
+    items.push(<a key='about' href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About' /></a>);
+    items.push(<a key='mastodon' href='https://joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.what_is_mastodon' defaultMessage='About Mastodon' /></a>);
+    items.push(<a key='docs' href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a>);
+    items.push(<a key='privacy-policy' href='/privacy-policy' target='_blank'><FormattedMessage id='getting_started.privacy_policy' defaultMessage='Privacy Policy' /></a>);
+    items.push(<Link key='hotkeys' to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link>);
 
     if (profileDirectory) {
-      items.push(<Link key='directory' to='/directory'><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></Link>);
+      items.push(<Link key='directory' to='/directory'><FormattedMessage id='getting_started.directory' defaultMessage='Directory' /></Link>);
     }
 
-    items.push(<a key='apps' href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a>);
-    items.push(<a key='privacy-policy' href='/privacy-policy' target='_blank'><FormattedMessage id='getting_started.privacy_policy' defaultMessage='Privacy Policy' /></a>);
-
     if (signedIn) {
-      items.push(<a key='developers' href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a>);
-    }
+      if ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS) {
+        items.push(<a key='invites' href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a>);
+      }
 
-    items.push(<a key='docs' href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a>);
-
-    if (signedIn) {
+      items.push(<a key='security' href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a>);
       items.push(<a key='logout' href='/auth/sign_out' onClick={this.handleLogoutClick}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a>);
     }
 
@@ -93,9 +78,9 @@ class LinkFooter extends React.PureComponent {
 
         <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 noreferrer' target='_blank'>{repository}</a> (v{version})</span> }}
+            id='getting_started.free_software_notice'
+            defaultMessage='Mastodon is free, open source software. You can view the source code, contribute or report issues at {repository}.'
+            values={{ repository: <span><a href={source_url} rel='noopener noreferrer' target='_blank'>{repository}</a> (v{version})</span> }}
           />
         </p>
       </div>
diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js
index 00ae04761..7c7c9056f 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.js
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js
@@ -23,9 +23,10 @@ export default class NavigationPanel extends React.Component {
 
     return (
       <div className='navigation-panel'>
-        <Link to='/' className='column-link column-link--logo'><Logo /></Link>
-
-        <hr />
+        <div className='navigation-panel__logo'>
+          <Link to='/' className='column-link column-link--logo'><Logo /></Link>
+          <hr />
+        </div>
 
         {signedIn && (
           <React.Fragment>
@@ -40,10 +41,10 @@ export default class NavigationPanel extends React.Component {
         <NavLink className='column-link column-link--transparent' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
 
         {!signedIn && (
-          <React.Fragment>
+          <div className='navigation-panel__sign-in-banner'>
             <hr />
             <SignInBanner />
-          </React.Fragment>
+          </div>
         )}
 
         {signedIn && (
@@ -62,6 +63,11 @@ export default class NavigationPanel extends React.Component {
           </React.Fragment>
         )}
 
+        <div className='navigation-panel__legal'>
+          <hr />
+          <NavLink className='column-link column-link--transparent' to='/about'><Icon className='column-link__icon' id='ellipsis-h' fixedWidth /><FormattedMessage id='navigation_bar.about' defaultMessage='About' /></NavLink>
+        </div>
+
         {showTrends && (
           <React.Fragment>
             <div className='flex-spacer' />
diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js
deleted file mode 100644
index 55668cab6..000000000
--- a/app/javascript/mastodon/features/ui/components/tabs_bar.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { NavLink, withRouter } from 'react-router-dom';
-import { FormattedMessage, injectIntl } from 'react-intl';
-import { debounce } from 'lodash';
-import { isUserTouching } from '../../../is_mobile';
-import Icon from 'mastodon/components/icon';
-import NotificationsCounterIcon from './notifications_counter_icon';
-
-export const links = [
-  <NavLink className='tabs-bar__link' to='/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><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' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
-  <NavLink className='tabs-bar__link' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
-  <NavLink className='tabs-bar__link optional' to='/explore' data-preview-title-id='tabs_bar.search' data-preview-icon='search' ><Icon id='search' fixedWidth /><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' ><Icon id='bars' fixedWidth /></NavLink>,
-];
-
-export function getIndex (path) {
-  return links.findIndex(link => link.props.to === path);
-}
-
-export function getLink (index) {
-  return links[index].props.to;
-}
-
-export default @injectIntl
-@withRouter
-class TabsBar extends React.PureComponent {
-
-  static propTypes = {
-    intl: PropTypes.object.isRequired,
-    history: PropTypes.object.isRequired,
-  }
-
-  setRef = ref => {
-    this.node = ref;
-  }
-
-  handleClick = (e) => {
-    // Only apply optimization for touch devices, which we assume are slower
-    // We thus avoid the 250ms delay for non-touch devices and the lag for touch devices
-    if (isUserTouching()) {
-      e.preventDefault();
-      e.persist();
-
-      requestAnimationFrame(() => {
-        const tabs = Array(...this.node.querySelectorAll('.tabs-bar__link'));
-        const currentTab = tabs.find(tab => tab.classList.contains('active'));
-        const nextTab = tabs.find(tab => tab.contains(e.target));
-        const { props: { to } } = links[Array(...this.node.childNodes).indexOf(nextTab)];
-
-
-        if (currentTab !== nextTab) {
-          if (currentTab) {
-            currentTab.classList.remove('active');
-          }
-
-          const listener = debounce(() => {
-            nextTab.removeEventListener('transitionend', listener);
-            this.props.history.push(to);
-          }, 50);
-
-          nextTab.addEventListener('transitionend', listener);
-          nextTab.classList.add('active');
-        }
-      });
-    }
-
-  }
-
-  render () {
-    const { intl: { formatMessage } } = this.props;
-
-    return (
-      <div className='tabs-bar__wrapper'>
-        <nav className='tabs-bar' ref={this.setRef}>
-          {links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))}
-        </nav>
-
-        <div id='tabs-bar__portal' />
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 5825db1e4..4e37908c8 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -51,10 +51,12 @@ import {
   Directory,
   Explore,
   FollowRecommendations,
+  About,
 } from './util/async-components';
 import { me, title } from '../../initial_state';
 import { closeOnboarding, INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
 import { Helmet } from 'react-helmet';
+import Header from './components/header';
 
 // Dummy import, to make sure that <Status /> ends up in the application bundle.
 // Without this it ends up in ~8 very commonly used bundles.
@@ -170,6 +172,7 @@ class SwitchingColumnsArea extends React.PureComponent {
 
           <WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
           <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
+          <WrappedRoute path='/about' component={About} content={children} />
 
           <WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
           <WrappedRoute path={['/public', '/timelines/public']} exact component={PublicTimeline} content={children} />
@@ -559,6 +562,8 @@ class UI extends React.PureComponent {
     return (
       <HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
         <div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
+          <Header />
+
           <SwitchingColumnsArea location={location} mobile={layout === 'mobile' || layout === 'single-column'}>
             {children}
           </SwitchingColumnsArea>
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 29b06206a..5907e0772 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -165,3 +165,7 @@ export function Explore () {
 export function FilterModal () {
   return import(/*webpackChunkName: "modals/filter_modal" */'../components/filter_modal');
 }
+
+export function About () {
+  return import(/*webpackChunkName: "features/about" */'../../about');
+}
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 463ec7bed..81a41b3fc 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -222,7 +222,7 @@
   "follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
   "generic.saved": "Saved",
   "getting_started.developers": "Developers",
-  "getting_started.directory": "Profile directory",
+  "getting_started.directory": "Directory",
   "getting_started.documentation": "Documentation",
   "getting_started.heading": "Getting started",
   "getting_started.invite": "Invite people",
@@ -310,7 +310,7 @@
   "mute_modal.duration": "Duration",
   "mute_modal.hide_notifications": "Hide notifications from this user?",
   "mute_modal.indefinite": "Indefinite",
-  "navigation_bar.apps": "Mobile apps",
+  "navigation_bar.apps": "Get the app",
   "navigation_bar.blocks": "Blocked users",
   "navigation_bar.bookmarks": "Bookmarks",
   "navigation_bar.community_timeline": "Local timeline",
@@ -324,7 +324,7 @@
   "navigation_bar.filters": "Muted words",
   "navigation_bar.follow_requests": "Follow requests",
   "navigation_bar.follows_and_followers": "Follows and followers",
-  "navigation_bar.info": "About this server",
+  "navigation_bar.info": "About",
   "navigation_bar.keyboard_shortcuts": "Hotkeys",
   "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Logout",
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 87ec6bb8a..e831fce53 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -2191,27 +2191,62 @@ a.account__display-name {
     &__main {
       box-sizing: border-box;
       width: 100%;
-      max-width: 600px;
       flex: 0 0 auto;
       display: flex;
       flex-direction: column;
 
       @media screen and (min-width: $no-gap-breakpoint) {
         padding: 0 10px;
+        max-width: 600px;
       }
     }
   }
 }
 
+$ui-header-height: 55px;
+
+.ui__header {
+  display: none;
+  box-sizing: border-box;
+  height: $ui-header-height;
+  position: sticky;
+  top: 0;
+  z-index: 2;
+  justify-content: space-between;
+  align-items: center;
+
+  &__logo {
+    display: inline-flex;
+    padding: 15px;
+
+    .logo {
+      height: $ui-header-height - 30px;
+      width: auto;
+    }
+  }
+
+  &__links {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    padding: 0 10px;
+
+    .button {
+      flex: 0 0 auto;
+    }
+  }
+}
+
 .tabs-bar__wrapper {
   background: darken($ui-base-color, 8%);
   position: sticky;
-  top: 0;
+  top: $ui-header-height;
   z-index: 2;
   padding-top: 0;
 
   @media screen and (min-width: $no-gap-breakpoint) {
     padding-top: 10px;
+    top: 0;
   }
 
   .tabs-bar {
@@ -2419,100 +2454,98 @@ a.account__display-name {
     padding-top: 0;
   }
 
-  @media screen and (min-width: 630px) {
-    .detailed-status {
-      padding: 15px;
+  .detailed-status {
+    padding: 15px;
 
-      .media-gallery,
-      .video-player,
-      .audio-player {
-        margin-top: 15px;
-      }
+    .media-gallery,
+    .video-player,
+    .audio-player {
+      margin-top: 15px;
     }
+  }
 
-    .account__header__bar {
-      padding: 5px 10px;
-    }
+  .account__header__bar {
+    padding: 5px 10px;
+  }
 
-    .navigation-bar,
-    .compose-form {
-      padding: 15px;
-    }
+  .navigation-bar,
+  .compose-form {
+    padding: 15px;
+  }
 
-    .compose-form .compose-form__publish .compose-form__publish-button-wrapper {
-      padding-top: 15px;
-    }
+  .compose-form .compose-form__publish .compose-form__publish-button-wrapper {
+    padding-top: 15px;
+  }
 
-    .notification__report {
-      padding: 15px 15px 15px (48px + 15px * 2);
-      min-height: 48px + 2px;
+  .notification__report {
+    padding: 15px 15px 15px (48px + 15px * 2);
+    min-height: 48px + 2px;
 
-      &__avatar {
-        left: 15px;
-        top: 17px;
-      }
+    &__avatar {
+      left: 15px;
+      top: 17px;
     }
+  }
 
-    .status {
-      padding: 15px 15px 15px (48px + 15px * 2);
-      min-height: 48px + 2px;
+  .status {
+    padding: 15px 15px 15px (48px + 15px * 2);
+    min-height: 48px + 2px;
 
-      &__avatar {
-        left: 15px;
-        top: 17px;
-      }
+    &__avatar {
+      left: 15px;
+      top: 17px;
+    }
 
-      &__content {
-        padding-top: 5px;
-      }
+    &__content {
+      padding-top: 5px;
+    }
 
-      &__prepend {
-        margin-left: 48px + 15px * 2;
-        padding-top: 15px;
-      }
+    &__prepend {
+      margin-left: 48px + 15px * 2;
+      padding-top: 15px;
+    }
 
-      &__prepend-icon-wrapper {
-        left: -32px;
-      }
+    &__prepend-icon-wrapper {
+      left: -32px;
+    }
 
-      .media-gallery,
-      &__action-bar,
-      .video-player,
-      .audio-player {
-        margin-top: 10px;
-      }
+    .media-gallery,
+    &__action-bar,
+    .video-player,
+    .audio-player {
+      margin-top: 10px;
     }
+  }
 
-    .account {
-      padding: 15px 10px;
+  .account {
+    padding: 15px 10px;
 
-      &__header__bio {
-        margin: 0 -10px;
-      }
+    &__header__bio {
+      margin: 0 -10px;
     }
+  }
 
-    .notification {
-      &__message {
-        margin-left: 48px + 15px * 2;
-        padding-top: 15px;
-      }
+  .notification {
+    &__message {
+      margin-left: 48px + 15px * 2;
+      padding-top: 15px;
+    }
 
-      &__favourite-icon-wrapper {
-        left: -32px;
-      }
+    &__favourite-icon-wrapper {
+      left: -32px;
+    }
 
-      .status {
-        padding-top: 8px;
-      }
+    .status {
+      padding-top: 8px;
+    }
 
-      .account {
-        padding-top: 8px;
-      }
+    .account {
+      padding-top: 8px;
+    }
 
-      .account__avatar-wrapper {
-        margin-left: 17px;
-        margin-right: 15px;
-      }
+    .account__avatar-wrapper {
+      margin-left: 17px;
+      margin-right: 15px;
     }
   }
 }
@@ -2554,39 +2587,85 @@ a.account__display-name {
   .search {
     margin-bottom: 10px;
   }
-}
 
-@media screen and (max-width: 600px + (285px * 1) + (10px * 1)) {
-  .columns-area__panels__pane--compositional {
+  .floating-action-button,
+  .tabs-bar__link.optional {
+    display: none;
+  }
+
+  .search-page .search {
+    display: none;
+  }
+
+  .navigation-panel__legal {
     display: none;
   }
+}
 
+@media screen and (max-width: $no-gap-breakpoint - 1px) {
   .with-fab .scrollable .item-list:last-child {
     padding-bottom: 5.25rem;
   }
-}
 
-@media screen and (min-width: 600px + (285px * 1) + (10px * 1)) {
-  .floating-action-button,
-  .tabs-bar__link.optional {
-    display: none;
+  .columns-area__panels__main {
+    width: calc(100% - 55px);
   }
 
-  .search-page .search {
-    display: none;
+  .columns-area__panels {
+    min-height: calc(100vh - $ui-header-height);
   }
-}
 
-@media screen and (max-width: 600px + (285px * 2) + (10px * 2)) {
   .columns-area__panels__pane--navigational {
-    display: none;
+    min-width: 55px;
+
+    .columns-area__panels__pane__inner {
+      width: 55px;
+    }
+
+    .navigation-panel {
+      margin: 0;
+      background: $ui-base-color;
+      border-left: 1px solid lighten($ui-base-color, 8%);
+      height: 100vh;
+    }
+
+    .column-link span,
+    .navigation-panel__sign-in-banner,
+    .navigation-panel__logo,
+    .getting-started__trends {
+      display: none;
+    }
+
+    .column-link__icon {
+      font-size: 18px;
+    }
+  }
+
+  .ui__header {
+    display: flex;
+    background: $ui-base-color;
+    border-bottom: 1px solid lighten($ui-base-color, 8%);
+  }
+
+  .column-header,
+  .column-back-button,
+  .scrollable {
+    border-radius: 0 !important;
   }
 }
 
-@media screen and (min-width: 600px + (285px * 2) + (10px * 2)) {
-  .tabs-bar {
+.explore__search-header {
+  display: none;
+}
+
+@media screen and (max-width: $no-gap-breakpoint + 285px - 1px) {
+  .columns-area__panels__pane--compositional {
     display: none;
   }
+
+  .explore__search-header {
+    display: flex;
+  }
 }
 
 .icon-with-badge {
@@ -7360,7 +7439,7 @@ noscript {
 
       path:first-child {
         fill: rgba($highlight-text-color, 0.25) !important;
-        fill-opacity: 1 !important;
+        fill-opacity: 100% !important;
       }
 
       path:last-child {
@@ -7832,10 +7911,9 @@ noscript {
 }
 
 .explore__search-header {
-  background: $ui-base-color;
-  display: flex;
-  align-items: flex-start;
+  background: darken($ui-base-color, 4%);
   justify-content: center;
+  align-items: center;
   padding: 15px;
 
   .search {
@@ -7844,14 +7922,8 @@ noscript {
   }
 
   .search__input {
-    border-radius: 4px;
-    color: $inverted-text-color;
-    background: $simple-background-color;
+    border: 1px solid lighten($ui-base-color, 8%);
     padding: 10px;
-
-    &::placeholder {
-      color: $dark-text-color;
-    }
   }
 
   .search .fa {
diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/variables.scss
index be2c900ea..775a12e68 100644
--- a/app/javascript/styles/mastodon/variables.scss
+++ b/app/javascript/styles/mastodon/variables.scss
@@ -53,7 +53,7 @@ $media-modal-media-max-width: 100%;
 // put margins on top and bottom of image to avoid the screen covered by image.
 $media-modal-media-max-height: 80%;
 
-$no-gap-breakpoint: 415px;
+$no-gap-breakpoint: 890px;
 
 $font-sans-serif: 'mastodon-font-sans-serif' !default;
 $font-display: 'mastodon-font-display' !default;