about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/features
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2022-10-04 20:13:23 +0200
committerClaire <claire.github-309c@sitedethib.com>2022-10-28 19:24:02 +0200
commit206e9593ac2c808fe85177ab156027c226da8cb6 (patch)
tree7b71e55f601eeadecf5db77f97ee8919b67eba96 /app/javascript/flavours/glitch/features
parent14ddb85c3bd5b9e57c1f3e6fe6eaf59200f0a34c (diff)
[Glitch] Fix logged-out web UI on smaller screens
Port e2b561e3a521ff893943c0e9e32952e35934ca54 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
Diffstat (limited to 'app/javascript/flavours/glitch/features')
-rw-r--r--app/javascript/flavours/glitch/features/about/index.js34
-rw-r--r--app/javascript/flavours/glitch/features/compose/index.js50
-rw-r--r--app/javascript/flavours/glitch/features/getting_started/index.js65
-rw-r--r--app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js13
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/columns_area.js85
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/compose_panel.js2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/header.js53
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/link_footer.js44
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/navigation_panel.js9
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/tabs_bar.js86
-rw-r--r--app/javascript/flavours/glitch/features/ui/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/ui/util/async-components.js4
12 files changed, 197 insertions, 254 deletions
diff --git a/app/javascript/flavours/glitch/features/about/index.js b/app/javascript/flavours/glitch/features/about/index.js
new file mode 100644
index 000000000..9f7c90d91
--- /dev/null
+++ b/app/javascript/flavours/glitch/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 'flavours/glitch/components/column';
+import LinkFooter from 'flavours/glitch/features/ui/components/link_footer';
+import { Helmet } from 'react-helmet';
+import { title } from 'flavours/glitch/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/flavours/glitch/features/compose/index.js b/app/javascript/flavours/glitch/features/compose/index.js
index 567bb3711..150e78c48 100644
--- a/app/javascript/flavours/glitch/features/compose/index.js
+++ b/app/javascript/flavours/glitch/features/compose/index.js
@@ -14,6 +14,7 @@ import SearchResultsContainer from './containers/search_results_container';
 import { me, mascot } from 'flavours/glitch/initial_state';
 import { cycleElefriendCompose } from 'flavours/glitch/actions/compose';
 import HeaderContainer from './containers/header_container';
+import Column from 'flavours/glitch/components/column';
 
 const messages = defineMessages({
   compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' },
@@ -79,32 +80,41 @@ class Compose extends React.PureComponent {
     } = this.props;
     const computedClass = classNames('drawer', `mbstobon-${elefriend}`);
 
-    return (
-      <div className={computedClass} role='region' aria-label={intl.formatMessage(messages.compose)}>
-        {multiColumn && <HeaderContainer />}
-
-        {(multiColumn || isSearchPage) && <SearchContainer />}
+    if (multiColumn) {
+      return (
+        <div className={computedClass} role='region' aria-label={intl.formatMessage(messages.compose)}>
+          <HeaderContainer />
 
-        <div className='drawer__pager'>
-          {!isSearchPage && <div className='drawer__inner'>
-            <NavigationContainer />
+          {(multiColumn || isSearchPage) && <SearchContainer />}
 
-            <ComposeFormContainer />
+          <div className='drawer__pager'>
+            {!isSearchPage && <div className='drawer__inner'>
+              <NavigationContainer />
 
-            <div className='drawer__inner__mastodon'>
-              {mascot ? <img alt='' draggable='false' src={mascot} /> : <button className='mastodon' onClick={onClickElefriend} />}
-            </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'>
+                {mascot ? <img alt='' draggable='false' src={mascot} /> : <button className='mastodon' onClick={onClickElefriend} />}
               </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>
+        <NavigationContainer />
+        <ComposeFormContainer />
+      </Column>
     );
   }
 
diff --git a/app/javascript/flavours/glitch/features/getting_started/index.js b/app/javascript/flavours/glitch/features/getting_started/index.js
index f52b769a3..4d3427877 100644
--- a/app/javascript/flavours/glitch/features/getting_started/index.js
+++ b/app/javascript/flavours/glitch/features/getting_started/index.js
@@ -84,11 +84,12 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
 
   static contextTypes = {
     router: PropTypes.object.isRequired,
+    identity: PropTypes.object,
   };
 
   static propTypes = {
     intl: PropTypes.object.isRequired,
-    myAccount: ImmutablePropTypes.map.isRequired,
+    myAccount: ImmutablePropTypes.map,
     columns: ImmutablePropTypes.list,
     multiColumn: PropTypes.bool,
     fetchFollowRequests: PropTypes.func.isRequired,
@@ -104,10 +105,10 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
   }
 
   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;
     }
 
@@ -121,7 +122,7 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
     let listItems = [];
 
     if (multiColumn) {
-      if (!columns.find(item => item.get('id') === 'HOME')) {
+      if (signedIn && !columns.find(item => item.get('id') === 'HOME')) {
         navItems.push(<ColumnLink key='home' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/home' />);
       }
 
@@ -142,41 +143,47 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
       navItems.push(<ColumnLink key='explore' icon='hashtag' text={intl.formatMessage(messages.explore)} to='/explore' />);
     }
 
-    if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) {
-      navItems.push(<ColumnLink key='conversations' icon='envelope' text={intl.formatMessage(messages.direct)} to='/conversations' />);
-    }
+    if (signedIn) {
+      if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) {
+        navItems.push(<ColumnLink key='conversations' icon='envelope' text={intl.formatMessage(messages.direct)} to='/conversations' />);
+      }
 
-    if (!multiColumn || !columns.find(item => item.get('id') === 'BOOKMARKS')) {
-      navItems.push(<ColumnLink key='bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />);
-    }
+      if (!multiColumn || !columns.find(item => item.get('id') === 'BOOKMARKS')) {
+        navItems.push(<ColumnLink key='bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />);
+      }
 
-    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' />);
+      }
 
-    navItems.push(<ColumnLink key='getting_started' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />);
+      navItems.push(<ColumnLink key='getting_started' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />);
 
-    listItems = listItems.concat([
-      <div key='9'>
-        <ColumnLink key='lists' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
-        {lists.filter(list => !columns.find(item => item.get('id') === 'LIST' && item.getIn(['params', 'id']) === list.get('id'))).map(list =>
-          <ColumnLink key={`list-${list.get('id')}`} to={`/lists/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
-        )}
-      </div>,
-    ]);
+      listItems = listItems.concat([
+        <div key='9'>
+          <ColumnLink key='lists' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
+          {lists.filter(list => !columns.find(item => item.get('id') === 'LIST' && item.getIn(['params', 'id']) === list.get('id'))).map(list =>
+            <ColumnLink key={`list-${list.get('id')}`} to={`/lists/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
+          )}
+        </div>,
+      ]);
+    }
 
     return (
       <Column bindToDocument={!multiColumn} name='getting-started' icon='asterisk' heading={intl.formatMessage(messages.heading)} label={intl.formatMessage(messages.menu)} hideHeadingOnMobile>
         <div className='scrollable optionally-scrollable'>
           <div className='getting-started__wrapper'>
-            {!multiColumn && <NavigationBar account={myAccount} />}
+            {!multiColumn && signedIn && <NavigationBar account={myAccount} />}
             {multiColumn && <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />}
             {navItems}
-            <ColumnSubheading text={intl.formatMessage(messages.lists_subheading)} />
-            {listItems}
-            <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
-            { preferencesLink !== undefined && <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href={preferencesLink} /> }
-            <ColumnLink icon='cogs' text={intl.formatMessage(messages.settings)} onClick={openSettings} />
+            {signedIn && (
+              <React.Fragment>
+                <ColumnSubheading text={intl.formatMessage(messages.lists_subheading)} />
+                {listItems}
+                <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
+                { preferencesLink !== undefined && <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href={preferencesLink} /> }
+                <ColumnLink icon='cogs' text={intl.formatMessage(messages.settings)} onClick={openSettings} />
+              </React.Fragment>
+            )}
           </div>
 
           <LinkFooter />
diff --git a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js
index 481f76763..06df2ed99 100644
--- a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js
+++ b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js
@@ -1,10 +1,10 @@
 import React from 'react';
-import Column from 'flavours/glitch/features/ui/components/column';
-import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';
+import Column from 'flavours/glitch/components/column';
 import { connect } from 'react-redux';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import PropTypes from 'prop-types';
 import ImmutablePureComponent from 'react-immutable-pure-component';
+import ColumnHeader from 'flavours/glitch/components/column_header';
 
 const messages = defineMessages({
   heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' },
@@ -28,8 +28,13 @@ class KeyboardShortcuts extends ImmutablePureComponent {
     const { intl, collapseEnabled, 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/flavours/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js
index 718b4a27f..8037c195d 100644
--- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js
+++ b/app/javascript/flavours/glitch/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 'flavours/glitch/initial_state';
-
 import BundleContainer from '../containers/bundle_container';
 import ColumnLoading from './column_loading';
 import DrawerLoading from './drawer_loading';
@@ -72,20 +66,13 @@ class ColumnsArea extends ImmutablePureComponent {
     openSettings: PropTypes.func,
   };
 
-   // 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);
@@ -100,10 +87,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) {
@@ -116,13 +100,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 () {
@@ -150,31 +127,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;
@@ -187,22 +139,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 />;
   }
@@ -213,22 +149,12 @@ class ColumnsArea extends ImmutablePureComponent {
 
   render () {
     const { columns, children, singleColumn, intl, navbarUnder, openSettings } = 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'>
@@ -238,9 +164,8 @@ class ColumnsArea extends ImmutablePureComponent {
           </div>
 
           <div className={`columns-area__panels__main ${floatingActionButton && 'with-fab'}`}>
-            {!navbarUnder && <TabsBar key='tabs' />}
-            {content}
-            {navbarUnder && <TabsBar key='tabs' />}
+            <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/flavours/glitch/features/ui/components/compose_panel.js b/app/javascript/flavours/glitch/features/ui/components/compose_panel.js
index 6e1c51d74..894a00747 100644
--- a/app/javascript/flavours/glitch/features/ui/components/compose_panel.js
+++ b/app/javascript/flavours/glitch/features/ui/components/compose_panel.js
@@ -34,7 +34,7 @@ class ComposePanel extends React.PureComponent {
           </React.Fragment>
         )}
 
-        <LinkFooter withHotkeys />
+        <LinkFooter />
       </div>
     );
   }
diff --git a/app/javascript/flavours/glitch/features/ui/components/header.js b/app/javascript/flavours/glitch/features/ui/components/header.js
new file mode 100644
index 000000000..041bdff05
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/ui/components/header.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import Logo from 'flavours/glitch/components/logo';
+import { Link } from 'react-router-dom';
+import { FormattedMessage } from 'react-intl';
+import { registrationsOpen, me } from 'flavours/glitch/initial_state';
+import Avatar from 'flavours/glitch/components/avatar';
+import Permalink from 'flavours/glitch/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/flavours/glitch/features/ui/components/link_footer.js b/app/javascript/flavours/glitch/features/ui/components/link_footer.js
index 39576f17b..cd32c8a3d 100644
--- a/app/javascript/flavours/glitch/features/ui/components/link_footer.js
+++ b/app/javascript/flavours/glitch/features/ui/components/link_footer.js
@@ -3,8 +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 'flavours/glitch/initial_state';
-import { signOutLink, securityLink, privacyPolicyLink } from 'flavours/glitch/utils/backend_links';
+import { version, repository, source_url, profile_directory as profileDirectory } from 'flavours/glitch/initial_state';
 import { logOut } from 'flavours/glitch/utils/log_out';
 import { openModal } from 'flavours/glitch/actions/modal';
 import { PERMISSION_INVITE_USERS } from 'flavours/glitch/permissions';
@@ -34,7 +33,6 @@ class LinkFooter extends React.PureComponent {
   };
 
   static propTypes = {
-    withHotkeys: PropTypes.bool,
     onLogout: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
   };
@@ -49,44 +47,26 @@ class LinkFooter extends React.PureComponent {
   }
 
   render () {
-    const { withHotkeys } = this.props;
     const { signedIn, permissions } = this.context.identity;
-
     const items = [];
 
-    if ((this.context.identity.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 && securityLink) {
-      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(<a key='apps' href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a>);
-
-    if (privacyPolicyLink) {
-      items.push(<a key='terms' href={privacyPolicyLink} target='_blank'><FormattedMessage id='getting_started.privacy_policy' defaultMessage='Privacy Policy' /></a>);
+      items.push(<Link key='directory' to='/directory'><FormattedMessage id='getting_started.directory' defaultMessage='Directory' /></Link>);
     }
 
     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>);
     }
 
diff --git a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js
index 57fbfb285..cb884ec75 100644
--- a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js
+++ b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js
@@ -42,10 +42,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 && (
@@ -64,6 +64,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/flavours/glitch/features/ui/components/tabs_bar.js b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js
deleted file mode 100644
index 9c82fc91d..000000000
--- a/app/javascript/flavours/glitch/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 'flavours/glitch/is_mobile';
-import Icon from 'flavours/glitch/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='/search' 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/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js
index c8cc905e7..8ea7a0b02 100644
--- a/app/javascript/flavours/glitch/features/ui/index.js
+++ b/app/javascript/flavours/glitch/features/ui/index.js
@@ -52,12 +52,14 @@ import {
   Directory,
   Explore,
   FollowRecommendations,
+  About,
 } from './util/async-components';
 import { HotKeys } from 'react-hotkeys';
 import { me, title } from 'flavours/glitch/initial_state';
 import { closeOnboarding, INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding';
 import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
 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.
@@ -183,6 +185,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} />
@@ -652,6 +655,9 @@ class UI extends React.Component {
               )}}
             />
           </div>)}
+
+          <Header />
+
           <SwitchingColumnsArea location={location} mobile={layout === 'mobile' || layout === 'single-column'} navbarUnder={navbarUnder}>
             {children}
           </SwitchingColumnsArea>
diff --git a/app/javascript/flavours/glitch/features/ui/util/async-components.js b/app/javascript/flavours/glitch/features/ui/util/async-components.js
index eef3a941d..7ef06ceb7 100644
--- a/app/javascript/flavours/glitch/features/ui/util/async-components.js
+++ b/app/javascript/flavours/glitch/features/ui/util/async-components.js
@@ -181,3 +181,7 @@ export function FilterModal () {
 export function Explore () {
   return import(/* webpackChunkName: "flavours/glitch/async/explore" */'flavours/glitch/features/explore');
 }
+
+export function About () {
+  return import(/*webpackChunkName: "features/glitch/async/about" */'flavours/glitch/features/about');
+}