about summary refs log tree commit diff
diff options
context:
space:
mode:
authorkibigo! <marrus-sh@users.noreply.github.com>2017-12-26 16:54:28 -0800
committerkibigo! <marrus-sh@users.noreply.github.com>2018-01-04 18:21:59 -0800
commit3c29f5740447270a4122b334281a907ecbdd4165 (patch)
treedd3daba631f2eddf0ede5af76b8e29eda8874854
parent924ffe81d477a8cf890c8117efb94b908760bccc (diff)
WIP <Compose> Refactor; <Drawer> ed.
-rw-r--r--app/javascript/flavours/glitch/features/composer/index.js15
-rw-r--r--app/javascript/flavours/glitch/features/composer/options/dropdown/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/drawer/components/navigation_bar.js38
-rw-r--r--app/javascript/flavours/glitch/features/drawer/components/search.js129
-rw-r--r--app/javascript/flavours/glitch/features/drawer/components/search_results.js65
-rw-r--r--app/javascript/flavours/glitch/features/drawer/containers/navigation_container.js11
-rw-r--r--app/javascript/flavours/glitch/features/drawer/containers/search_container.js35
-rw-r--r--app/javascript/flavours/glitch/features/drawer/containers/search_results_container.js8
-rw-r--r--app/javascript/flavours/glitch/features/drawer/header/index.js117
-rw-r--r--app/javascript/flavours/glitch/features/drawer/index.js268
-rw-r--r--app/javascript/flavours/glitch/features/drawer/pager/account/index.js70
-rw-r--r--app/javascript/flavours/glitch/features/drawer/pager/index.js43
-rw-r--r--app/javascript/flavours/glitch/features/drawer/results/index.js114
-rw-r--r--app/javascript/flavours/glitch/features/drawer/search/index.js149
-rw-r--r--app/javascript/flavours/glitch/features/drawer/search/popout/index.js95
-rw-r--r--app/javascript/flavours/glitch/util/dom_helpers.js8
-rw-r--r--app/javascript/flavours/glitch/util/react_helpers.js2
-rw-r--r--app/javascript/flavours/glitch/util/redux_helpers.js9
18 files changed, 723 insertions, 455 deletions
diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js
index 25c2622d8..506c668a7 100644
--- a/app/javascript/flavours/glitch/features/composer/index.js
+++ b/app/javascript/flavours/glitch/features/composer/index.js
@@ -2,9 +2,6 @@
 import PropTypes from 'prop-types';
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import { injectIntl } from 'react-intl';
-import { connect } from 'react-redux';
-import { withRouter } from 'react-router';
 
 //  Actions.
 import {
@@ -43,7 +40,7 @@ import { countableText } from 'flavours/glitch/util/counter';
 import { me } from 'flavours/glitch/util/initial_state';
 import { isMobile } from 'flavours/glitch/util/is_mobile';
 import { assignHandlers } from 'flavours/glitch/util/react_helpers';
-import { mergeProps } from 'flavours/glitch/util/redux_helpers';
+import { wrap } from 'flavours/glitch/util/redux_helpers';
 
 //  State mapping.
 function mapStateToProps (state) {
@@ -204,9 +201,7 @@ const handlers = {
 };
 
 //  The component.
-@injectIntl
-@connect(mapStateToProps, mapDispatchToProps, mergeProps)
-export default class Composer extends React.Component {
+class Composer extends React.Component {
 
   //  Constructor.
   constructor (props) {
@@ -408,7 +403,7 @@ export default class Composer extends React.Component {
 //  Context
 Composer.contextTypes = {
   history: PropTypes.object,
-}
+};
 
 //  Props.
 Composer.propTypes = {
@@ -438,3 +433,7 @@ Composer.propTypes = {
     text: PropTypes.string,
   }).isRequired,
 };
+
+//  Connecting and export.
+export { Composer as WrappedComponent };
+export default wrap(Composer, mapStateToProps, mapDispatchToProps, true);
diff --git a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js
index 0f304bc88..ee52008a7 100644
--- a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js
+++ b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js
@@ -70,7 +70,7 @@ const handlers = {
     //  dropdown.
     if (onModalClose && isUserTouching()) {
       if (open) {
-        onModalClose()
+        onModalClose();
       } else if (onChange && onModalOpen) {
         onModalOpen({
           actions: items.map(
diff --git a/app/javascript/flavours/glitch/features/drawer/components/navigation_bar.js b/app/javascript/flavours/glitch/features/drawer/components/navigation_bar.js
deleted file mode 100644
index 1b6d74123..000000000
--- a/app/javascript/flavours/glitch/features/drawer/components/navigation_bar.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import Avatar from 'flavours/glitch/components/avatar';
-import IconButton from 'flavours/glitch/components/icon_button';
-import Permalink from 'flavours/glitch/components/permalink';
-import { FormattedMessage } from 'react-intl';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-
-export default class NavigationBar extends ImmutablePureComponent {
-
-  static propTypes = {
-    account: ImmutablePropTypes.map.isRequired,
-    onClose: PropTypes.func.isRequired,
-  };
-
-  render () {
-    return (
-      <div className='navigation-bar'>
-        <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
-          <span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
-          <Avatar account={this.props.account} size={40} />
-        </Permalink>
-
-        <div className='navigation-bar__profile'>
-          <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}>
-            <strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong>
-          </Permalink>
-
-          <a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
-        </div>
-
-        <IconButton title='' icon='close' onClick={this.props.onClose} />
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/flavours/glitch/features/drawer/components/search.js b/app/javascript/flavours/glitch/features/drawer/components/search.js
deleted file mode 100644
index 1ce66b19d..000000000
--- a/app/javascript/flavours/glitch/features/drawer/components/search.js
+++ /dev/null
@@ -1,129 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import Overlay from 'react-overlays/lib/Overlay';
-import Motion from 'flavours/glitch/util/optional_motion';
-import spring from 'react-motion/lib/spring';
-
-const messages = defineMessages({
-  placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
-});
-
-class SearchPopout extends React.PureComponent {
-
-  static propTypes = {
-    style: PropTypes.object,
-  };
-
-  render () {
-    const { style } = this.props;
-
-    return (
-      <div style={{ ...style, position: 'absolute', width: 285 }}>
-        <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
-          {({ opacity, scaleX, scaleY }) => (
-            <div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
-              <h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4>
-
-              <ul>
-                <li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li>
-                <li><em>@username@domain</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
-                <li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
-                <li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li>
-              </ul>
-
-              <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' />
-            </div>
-          )}
-        </Motion>
-      </div>
-    );
-  }
-
-}
-
-@injectIntl
-export default class Search extends React.PureComponent {
-
-  static propTypes = {
-    value: PropTypes.string.isRequired,
-    submitted: PropTypes.bool,
-    onChange: PropTypes.func.isRequired,
-    onSubmit: PropTypes.func.isRequired,
-    onClear: PropTypes.func.isRequired,
-    onShow: PropTypes.func.isRequired,
-    intl: PropTypes.object.isRequired,
-  };
-
-  state = {
-    expanded: false,
-  };
-
-  handleChange = (e) => {
-    this.props.onChange(e.target.value);
-  }
-
-  handleClear = (e) => {
-    e.preventDefault();
-
-    if (this.props.value.length > 0 || this.props.submitted) {
-      this.props.onClear();
-    }
-  }
-
-  handleKeyDown = (e) => {
-    if (e.key === 'Enter') {
-      e.preventDefault();
-      this.props.onSubmit();
-    } else if (e.key === 'Escape') {
-      document.querySelector('.ui').parentElement.focus();
-    }
-  }
-
-  noop () {
-
-  }
-
-  handleFocus = () => {
-    this.setState({ expanded: true });
-    this.props.onShow();
-  }
-
-  handleBlur = () => {
-    this.setState({ expanded: false });
-  }
-
-  render () {
-    const { intl, value, submitted } = this.props;
-    const { expanded } = this.state;
-    const hasValue = value.length > 0 || submitted;
-
-    return (
-      <div className='search'>
-        <label>
-          <span style={{ display: 'none' }}>{intl.formatMessage(messages.placeholder)}</span>
-          <input
-            className='search__input'
-            type='text'
-            placeholder={intl.formatMessage(messages.placeholder)}
-            value={value}
-            onChange={this.handleChange}
-            onKeyUp={this.handleKeyDown}
-            onFocus={this.handleFocus}
-            onBlur={this.handleBlur}
-          />
-        </label>
-
-        <div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
-          <i className={`fa fa-search ${hasValue ? '' : 'active'}`} />
-          <i aria-label={intl.formatMessage(messages.placeholder)} className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} />
-        </div>
-
-        <Overlay show={expanded && !hasValue} placement='bottom' target={this}>
-          <SearchPopout />
-        </Overlay>
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/flavours/glitch/features/drawer/components/search_results.js b/app/javascript/flavours/glitch/features/drawer/components/search_results.js
deleted file mode 100644
index 2a4818d4e..000000000
--- a/app/javascript/flavours/glitch/features/drawer/components/search_results.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { FormattedMessage } from 'react-intl';
-import AccountContainer from 'flavours/glitch/containers/account_container';
-import StatusContainer from 'flavours/glitch/containers/status_container';
-import { Link } from 'react-router-dom';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-
-export default class SearchResults extends ImmutablePureComponent {
-
-  static propTypes = {
-    results: ImmutablePropTypes.map.isRequired,
-  };
-
-  render () {
-    const { results } = this.props;
-
-    let accounts, statuses, hashtags;
-    let count = 0;
-
-    if (results.get('accounts') && results.get('accounts').size > 0) {
-      count   += results.get('accounts').size;
-      accounts = (
-        <div className='search-results__section'>
-          {results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)}
-        </div>
-      );
-    }
-
-    if (results.get('statuses') && results.get('statuses').size > 0) {
-      count   += results.get('statuses').size;
-      statuses = (
-        <div className='search-results__section'>
-          {results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)}
-        </div>
-      );
-    }
-
-    if (results.get('hashtags') && results.get('hashtags').size > 0) {
-      count += results.get('hashtags').size;
-      hashtags = (
-        <div className='search-results__section'>
-          {results.get('hashtags').map(hashtag =>
-            <Link key={hashtag} className='search-results__hashtag' to={`/timelines/tag/${hashtag}`}>
-              #{hashtag}
-            </Link>
-          )}
-        </div>
-      );
-    }
-
-    return (
-      <div className='search-results'>
-        <div className='search-results__header'>
-          <FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} />
-        </div>
-
-        {accounts}
-        {statuses}
-        {hashtags}
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/flavours/glitch/features/drawer/containers/navigation_container.js b/app/javascript/flavours/glitch/features/drawer/containers/navigation_container.js
deleted file mode 100644
index eb630ffbb..000000000
--- a/app/javascript/flavours/glitch/features/drawer/containers/navigation_container.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { connect }   from 'react-redux';
-import NavigationBar from '../components/navigation_bar';
-import { me } from 'flavours/glitch/util/initial_state';
-
-const mapStateToProps = state => {
-  return {
-    account: state.getIn(['accounts', me]),
-  };
-};
-
-export default connect(mapStateToProps)(NavigationBar);
diff --git a/app/javascript/flavours/glitch/features/drawer/containers/search_container.js b/app/javascript/flavours/glitch/features/drawer/containers/search_container.js
deleted file mode 100644
index 8f4bfcf08..000000000
--- a/app/javascript/flavours/glitch/features/drawer/containers/search_container.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { connect } from 'react-redux';
-import {
-  changeSearch,
-  clearSearch,
-  submitSearch,
-  showSearch,
-} from 'flavours/glitch/actions/search';
-import Search from '../components/search';
-
-const mapStateToProps = state => ({
-  value: state.getIn(['search', 'value']),
-  submitted: state.getIn(['search', 'submitted']),
-});
-
-const mapDispatchToProps = dispatch => ({
-
-  onChange (value) {
-    dispatch(changeSearch(value));
-  },
-
-  onClear () {
-    dispatch(clearSearch());
-  },
-
-  onSubmit () {
-    dispatch(submitSearch());
-  },
-
-  onShow () {
-    dispatch(showSearch());
-  },
-
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(Search);
diff --git a/app/javascript/flavours/glitch/features/drawer/containers/search_results_container.js b/app/javascript/flavours/glitch/features/drawer/containers/search_results_container.js
deleted file mode 100644
index 16d95d417..000000000
--- a/app/javascript/flavours/glitch/features/drawer/containers/search_results_container.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { connect } from 'react-redux';
-import SearchResults from '../components/search_results';
-
-const mapStateToProps = state => ({
-  results: state.getIn(['search', 'results']),
-});
-
-export default connect(mapStateToProps)(SearchResults);
diff --git a/app/javascript/flavours/glitch/features/drawer/header/index.js b/app/javascript/flavours/glitch/features/drawer/header/index.js
new file mode 100644
index 000000000..fd79b6e18
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/drawer/header/index.js
@@ -0,0 +1,117 @@
+//  Package imports.
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { defineMessages } from 'react-intl';
+import { Link } from 'react-router-dom';
+
+//  Components.
+import Icon from 'flavours/glitch/components/icon';
+
+//  Utils.
+import { conditionalRender } from 'flavours/glitch/util/react_helpers';
+
+//  Messages.
+const messages = defineMessages({
+  community: {
+    defaultMessage: 'Local timeline',
+    id: 'navigation_bar.community_timeline',
+  },
+  home_timeline: {
+    defaultMessage: 'Home',
+    id: 'tabs_bar.home',
+  },
+  logout: {
+    defaultMessage: 'Logout',
+    id: 'navigation_bar.logout',
+  },
+  notifications: {
+    defaultMessage: 'Notifications',
+    id: 'tabs_bar.notifications',
+  },
+  public: {
+    defaultMessage: 'Federated timeline',
+    id: 'navigation_bar.public_timeline',
+  },
+  settings: {
+    defaultMessage: 'App settings',
+    id: 'navigation_bar.app_settings',
+  },
+  start: {
+    defaultMessage: 'Getting started',
+    id: 'getting_started.heading',
+  },
+});
+
+//  The component.
+export default function DrawerHeader ({
+  columns,
+  intl,
+  onSettingsClick,
+}) {
+
+  //  Only renders the component if the column isn't being shown.
+  const renderForColumn = conditionalRender.bind(
+    columnId => !columns || !columns.some(
+      column => column.get('id') === columnId
+    )
+  );
+
+  //  The result.
+  return (
+    <nav className='drawer--header'>
+      <Link
+        aria-label={intl.formatMessage(messages.start)}
+        title={intl.formatMessage(messages.start)}
+        to='/getting-started'
+      ><Icon icon='asterisk' /></Link>
+      {renderForColumn('HOME', (
+        <Link
+          aria-label={intl.formatMessage(messages.home_timeline)}
+          title={intl.formatMessage(messages.home_timeline)}
+          to='/timelines/home'
+        ><Icon icon='home' /></Link>
+      ))}
+      {renderForColumn('NOTIFICATIONS', (
+        <Link
+          aria-label={intl.formatMessage(messages.notifications)}
+          title={intl.formatMessage(messages.notifications)}
+          to='/notifications'
+        ><Icon icon='bell' /></Link>
+      ))}
+      {renderForColumn('COMMUNITY', (
+        <Link
+          aria-label={intl.formatMessage(messages.community)}
+          title={intl.formatMessage(messages.community)}
+          to='/timelines/public/local'
+        ><Icon icon='users' /></Link>
+      ))}
+      {renderForColumn('PUBLIC', (
+        <Link
+          aria-label={intl.formatMessage(messages.public)}
+          title={intl.formatMessage(messages.public)}
+          to='/timelines/public'
+        ><Icon icon='globe' /></Link>
+      ))}
+      <a
+        aria-label={intl.formatMessage(messages.settings)}
+        onClick={onSettingsClick}
+        role='button'
+        title={intl.formatMessage(messages.settings)}
+        tabIndex='0'
+      ><Icon icon='cogs' /></a>
+      <a
+        aria-label={intl.formatMessage(messages.logout)}
+        data-method='delete'
+        href='/auth/sign_out'
+        title={intl.formatMessage(messages.logout)}
+      ><Icon icon='sign-out' /></a>
+    </nav>
+  );
+}
+
+DrawerHeader.propTypes = {
+  columns: ImmutablePropTypes.list,
+  intl: PropTypes.object,
+  onSettingsClick: PropTypes.func,
+};
diff --git a/app/javascript/flavours/glitch/features/drawer/index.js b/app/javascript/flavours/glitch/features/drawer/index.js
index 8386ae47c..01ec18fc5 100644
--- a/app/javascript/flavours/glitch/features/drawer/index.js
+++ b/app/javascript/flavours/glitch/features/drawer/index.js
@@ -2,197 +2,147 @@
 import PropTypes from 'prop-types';
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import { injectIntl, defineMessages } from 'react-intl';
-import spring from 'react-motion/lib/spring';
-import { connect } from 'react-redux';
-import { Link } from 'react-router-dom';
 
 //  Actions.
 import { changeComposing } from 'flavours/glitch/actions/compose';
-import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
 import { openModal } from 'flavours/glitch/actions/modal';
+import {
+  changeSearch,
+  clearSearch,
+  showSearch,
+  submitSearch,
+} from 'flavours/glitch/actions/search';
 
 //  Components.
-import Icon from 'flavours/glitch/components/icon';
-import Compose from 'flavours/glitch/features/compose';
-import NavigationContainer from './containers/navigation_container';
-import SearchContainer from './containers/search_container';
-import SearchResultsContainer from './containers/search_results_container';
+import DrawerHeader from './header';
+import DrawerPager from './pager';
+import DrawerResults from './results';
+import DrawerSearch from './search';
 
 //  Utils.
-import Motion from 'flavours/glitch/util/optional_motion';
-import {
-  assignHandlers,
-  conditionalRender,
-} from 'flavours/glitch/util/react_helpers';
-
-//  Messages.
-const messages = defineMessages({
-  community: {
-    defaultMessage: 'Local timeline',
-    id: 'navigation_bar.community_timeline',
-  },
-  home_timeline: {
-    defaultMessage: 'Home',
-    id: 'tabs_bar.home',
-  },
-  logout: {
-    defaultMessage: 'Logout',
-    id: 'navigation_bar.logout',
-  },
-  notifications: {
-    defaultMessage: 'Notifications',
-    id: 'tabs_bar.notifications',
-  },
-  public: {
-    defaultMessage: 'Federated timeline',
-    id: 'navigation_bar.public_timeline',
-  },
-  settings: {
-    defaultMessage: 'App settings',
-    id: 'navigation_bar.app_settings',
-  },
-  start: {
-    defaultMessage: 'Getting started',
-    id: 'getting_started.heading',
-  },
-});
+import { me } from 'flavours/glitch/util/initial_state';
+import { wrap } from 'flavours/glitch/util/redux_helpers';
 
 //  State mapping.
 const mapStateToProps = state => ({
+  account: state.getIn(['accounts', me]),
   columns: state.getIn(['settings', 'columns']),
-  showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
+  isComposing: state.getIn(['compose', 'is_composing']),
+  results: state.getIn(['search', 'results']),
+  searchHidden: state.getIn(['search', 'hidden']),
+  searchValue: state.getIn(['search', 'value']),
+  submitted: state.getIn(['search', 'submitted']),
 });
 
 //  Dispatch mapping.
 const mapDispatchToProps = dispatch => ({
-  onBlur () {
+  change (value) {
+    dispatch(changeSearch(value));
+  },
+  changeComposingOff () {
     dispatch(changeComposing(false));
   },
-  onFocus () {
+  changeComposingOn () {
     dispatch(changeComposing(true));
   },
-  onSettingsOpen () {
+  clear () {
+    dispatch(clearSearch());
+  },
+  show () {
+    dispatch(showSearch());
+  },
+  submit () {
+    dispatch(submitSearch());
+  },
+  openSettings () {
     dispatch(openModal('SETTINGS', {}));
   },
 });
 
 //  The component.
-@connect(mapStateToProps, mapDispatchToProps)
-@injectIntl
-export default function Drawer ({
-  columns,
-  intl,
-  multiColumn,
-  onBlur,
-  onFocus,
-  onSettingsOpen,
-  showSearch,
-}) {
+class Drawer extends React.Component {
 
-  //  Only renders the component if the column isn't being shown.
-  const renderForColumn = conditionalRender.bind(
-    columnId => !columns.some(column => column.get('id') === columnId)
-  );
+  //  Constructor.
+  constructor (props) {
+    super(props);
+  }
 
-  //  The result.
-  return (
-    <div className='drawer'>
-      {multiColumn ? (
-        <nav className='drawer__header'>
-          <Link
-            aria-label={intl.formatMessage(messages.start)}
-            className='drawer__tab'
-            title={intl.formatMessage(messages.start)}
-            to='/getting-started'
-          ><Icon icon='asterisk' /></Link>
-          {renderForColumn('HOME', (
-            <Link
-              aria-label={intl.formatMessage(messages.home_timeline)}
-              className='drawer__tab'
-              title={intl.formatMessage(messages.home_timeline)}
-              to='/timelines/home'
-            ><Icon icon='home' /></Link>
-          ))}
-          {renderForColumn('NOTIFICATIONS', (
-            <Link
-              aria-label={intl.formatMessage(messages.notifications)}
-              className='drawer__tab'
-              title={intl.formatMessage(messages.notifications)}
-              to='/notifications'
-            ><Icon icon='bell' /></Link>
-          ))}
-          {renderForColumn('COMMUNITY', (
-            <Link
-              aria-label={intl.formatMessage(messages.community)}
-              className='drawer__tab'
-              title={intl.formatMessage(messages.community)}
-              to='/timelines/public/local'
-            ><Icon icon='users' /></Link>
-          ))}
-          {renderForColumn('PUBLIC', (
-            <Link
-              aria-label={intl.formatMessage(messages.public)}
-              className='drawer__tab'
-              title={intl.formatMessage(messages.public)}
-              to='/timelines/public'
-            ><Icon icon='globe' /></Link>
-          ))}
-          <a
-            aria-label={intl.formatMessage(messages.settings)}
-            className='drawer__tab'
-            onClick={settings}
-            role='button'
-            title={intl.formatMessage(messages.settings)}
-            tabIndex='0'
-          ><Icon icon='cogs' /></a>
-          <a
-            aria-label={intl.formatMessage(messages.logout)}
-            className='drawer__tab'
-            data-method='delete'
-            href='/auth/sign_out'
-            title={intl.formatMessage(messages.logout)}
-          ><Icon icon='sign-out' /></a>
-        </nav>
-      ) : null}
-      <SearchContainer />
-      <div className='drawer__pager'>
-        <div
-          className='drawer__inner scrollable optionally-scrollable'
-          onFocus={focus}
-        >
-          <NavigationContainer onClose={blur} />
-          <Compose />
-        </div>
-        <Motion
-          defaultStyle={{ x: -100 }}
-          style={{
-            x: spring(showSearch ? 0 : -100, {
-              stiffness: 210,
-              damping: 20,
-            })
-          }}
-        >
-          {({ x }) => (
-            <div
-              className='drawer__inner darker scrollable optionally-scrollable'
-              style={{
-                transform: `translateX(${x}%)`,
-                visibility: x === -100 ? 'hidden' : 'visible'
-              }}
-            ><SearchResultsContainer /></div>
-          )}
-        </Motion>
+  //  Rendering.
+  render () {
+    const {
+      dispatch: {
+        change,
+        changeComposingOff,
+        changeComposingOn,
+        clear,
+        openSettings,
+        show,
+        submit,
+      },
+      intl,
+      multiColumn,
+      state: {
+        account,
+        columns,
+        isComposing,
+        results,
+        searchHidden,
+        searchValue,
+        submitted,
+      },
+    } = this.props;
+
+    //  The result.
+    return (
+      <div className='drawer'>
+        {multiColumn ? (
+          <DrawerHeader
+            columns={columns}
+            intl={intl}
+            onSettingsClick={openSettings}
+          />
+        ) : null}
+        <DrawerSearch
+          intl={intl}
+          onChange={change}
+          onClear={clear}
+          onShow={show}
+          onSubmit={submit}
+          submitted={submitted}
+          value={searchValue}
+        />
+        <DrawerPager
+          account={account}
+          active={isComposing}
+          onBlur={changeComposingOff}
+          onFocus={changeComposingOn}
+        />
+        <DrawerResults
+          results={results}
+          visible={submitted && !searchHidden}
+        />
       </div>
-    </div>
-  );
+    );
+  }
+
 }
 
 //  Props.
 Drawer.propTypes = {
   dispatch: PropTypes.func.isRequired,
-  columns: ImmutablePropTypes.list.isRequired,
-  multiColumn: PropTypes.bool,
-  showSearch: PropTypes.bool,
   intl: PropTypes.object.isRequired,
+  multiColumn: PropTypes.bool,
+  state: PropTypes.shape({
+    account: ImmutablePropTypes.map,
+    columns: ImmutablePropTypes.list,
+    isComposing: PropTypes.bool,
+    results: ImmutablePropTypes.map,
+    searchHidden: PropTypes.bool,
+    searchValue: PropTypes.string,
+    submitted: PropTypes.bool,
+  }).isRequired,
 };
+
+//  Connecting and export.
+export { Drawer as WrappedComponent };
+export default wrap(Drawer, mapStateToProps, mapDispatchToProps, true);
diff --git a/app/javascript/flavours/glitch/features/drawer/pager/account/index.js b/app/javascript/flavours/glitch/features/drawer/pager/account/index.js
new file mode 100644
index 000000000..2ee95d5b9
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/drawer/pager/account/index.js
@@ -0,0 +1,70 @@
+//  Package imports.
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import {
+  FormattedMessage,
+  defineMessages,
+} from 'react-intl';
+
+//  Components.
+import Avatar from 'flavours/glitch/components/avatar';
+import Permalink from 'flavours/glitch/components/permalink';
+
+//  Utils.
+import { hiddenComponent } from 'flavours/glitch/util/react_helpers';
+
+//  Messages.
+const messages = defineMessages({
+  edit: {
+    defaultMessage: 'Edit profile',
+    id: 'navigation_bar.edit_profile',
+  },
+});
+
+//  The component.
+export default function DrawerPagerAccount ({ account }) {
+
+  //  We need an account to render.
+  if (!account) {
+    return (
+      <div className='drawer--pager--account'>
+        <a
+          className='edit'
+          href='/settings/profile'
+        >
+          <FormattedMessage {...messages.edit} />
+        </a>
+      </div>
+    );
+  }
+
+  //  The result.
+  return (
+    <div className='drawer--pager--account'>
+      <Permalink
+        className='avatar'
+        href={account.get('url')}
+        to={`/accounts/${account.get('id')}`}
+      >
+        <span {...hiddenComponent}>{account.get('acct')}</span>
+        <Avatar
+          account={account}
+          size={40}
+        />
+      </Permalink>
+      <Permalink
+        className='acct'
+        href={account.get('url')}
+        to={`/accounts/${account.get('id')}`}
+      >
+        <strong>@{account.get('acct')}</strong>
+      </Permalink>
+      <a
+        className='edit'
+        href='/settings/profile'
+      ><FormattedMessage {...messages.edit} /></a>
+    </div>
+  );
+}
+
+DrawerPagerAccount.propTypes = { account: ImmutablePropTypes.map };
diff --git a/app/javascript/flavours/glitch/features/drawer/pager/index.js b/app/javascript/flavours/glitch/features/drawer/pager/index.js
new file mode 100644
index 000000000..8dc2d3ee9
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/drawer/pager/index.js
@@ -0,0 +1,43 @@
+//  Package imports.
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+//  Components.
+import IconButton from 'flavours/glitch/components/icon_button';
+import Composer from 'flavours/glitch/features/composer';
+import DrawerPagerAccount from './account';
+
+//  The component.
+export default function DrawerPager ({
+  account,
+  active,
+  onClose,
+  onFocus,
+}) {
+  const computedClass = classNames('drawer--pager', { active });
+
+  //  The result.
+  return (
+    <div
+      className={computedClass}
+      onFocus={onFocus}
+    >
+      <DrawerPagerAccount account={account} />
+      <IconButton
+        icon='close'
+        onClick={onClose}
+        title=''
+      />
+      <Composer />
+    </div>
+  );
+}
+
+DrawerPager.propTypes = {
+  account: ImmutablePropTypes.map,
+  active: PropTypes.bool,
+  onClose: PropTypes.func,
+  onFocus: PropTypes.func,
+};
diff --git a/app/javascript/flavours/glitch/features/drawer/results/index.js b/app/javascript/flavours/glitch/features/drawer/results/index.js
new file mode 100644
index 000000000..559d56da5
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/drawer/results/index.js
@@ -0,0 +1,114 @@
+//  Package imports.
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import {
+  FormattedMessage,
+  defineMessages,
+} from 'react-intl';
+import spring from 'react-motion/lib/spring';
+import { Link } from 'react-router-dom';
+
+//  Components.
+import AccountContainer from 'flavours/glitch/containers/account_container';
+import StatusContainer from 'flavours/glitch/containers/status_container';
+
+//  Utils.
+import Motion from 'flavours/glitch/util/optional_motion';
+
+//  Messages.
+const messages = defineMessages({
+  total: {
+    defaultMessage: '{count, number} {count, plural, one {result} other {results}}',
+    id: 'search_results.total',
+  },
+});
+
+//  The component.
+export default function DrawerPager ({
+  results,
+  visible,
+}) {
+  const accounts = results ? results.get('accounts') : null;
+  const statuses = results ? results.get('statuses') : null;
+  const hashtags = results ? results.get('hashtags') : null;
+
+  const count = [accounts, statuses, hashtags].reduce(function (size, item) {
+    if (item && item.size) {
+      return size + item.size;
+    }
+    return size;
+  }, 0);
+
+  //  The result.
+  return (
+    <Motion
+      defaultStyle={{ x: -100 }}
+      style={{
+        x: spring(visible ? 0 : -100, {
+          stiffness: 210,
+          damping: 20,
+        }),
+      }}
+    >
+      {({ x }) => (
+        <div
+          className='drawer--results'
+          style={{
+            transform: `translateX(${x}%)`,
+            visibility: x === -100 ? 'hidden' : 'visible',
+          }}
+        >
+          <header>
+            <FormattedMessage
+              {...messages.total}
+              values={{ count }}
+            />
+          </header>
+          {accounts && accounts.size ? (
+            <section>
+              {accounts.map(
+                accountId => (
+                  <AccountContainer
+                    id={accountId}
+                    key={accountId}
+                  />
+                )
+              )}
+            </section>
+          ) : null}
+          {statuses && statuses.size ? (
+            <section>
+              {statuses.map(
+                statusId => (
+                  <StatusContainer
+                    id={statusId}
+                    key={statusId}
+                  />
+                )
+              )}
+            </section>
+          ) : null}
+          {hashtags && hashtags.size ? (
+            <section>
+              {hashtags.map(
+                hashtag => (
+                  <Link
+                    className='hashtag'
+                    key={hashtag}
+                    to={`/timelines/tag/${hashtag}`}
+                  >#{hashtag}</Link>
+                )
+              )}
+            </section>
+          ) : null}
+        </div>
+      )}
+    </Motion>
+  );
+}
+
+DrawerPager.propTypes = {
+  results: ImmutablePropTypes.map,
+  visible: PropTypes.bool,
+};
diff --git a/app/javascript/flavours/glitch/features/drawer/search/index.js b/app/javascript/flavours/glitch/features/drawer/search/index.js
new file mode 100644
index 000000000..ccb2ba859
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/drawer/search/index.js
@@ -0,0 +1,149 @@
+//  Package imports.
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import {
+  FormattedMessage,
+  defineMessages,
+} from 'react-intl';
+import Overlay from 'react-overlays/lib/Overlay';
+
+//  Components.
+import Icon from 'flavours/glitch/components/icon';
+import DrawerSearchPopout from './popout';
+
+//  Utils.
+import { focusRoot } from 'flavours/glitch/util/dom_helpers';
+import {
+  assignHandlers,
+  hiddenComponent,
+} from 'flavours/glitch/util/react_helpers';
+
+//  Messages.
+const messages = defineMessages({
+  placeholder: {
+    defaultMessage: 'Search',
+    id: 'search.placeholder',
+  },
+});
+
+//  Handlers.
+const handlers = {
+
+  blur () {
+    this.setState({ expanded: false });
+  },
+
+  change ({ target: { value } }) {
+    const { onChange } = this.props;
+    if (onChange) {
+      onChange(value);
+    }
+  },
+
+  clear (e) {
+    const {
+      onClear,
+      submitted,
+      value: { length },
+    } = this.props;
+    e.preventDefault();  //  Prevents focus change ??
+    if (onClear && (submitted || length)) {
+      onClear();
+    }
+  },
+
+  focus () {
+    const { onShow } = this.props;
+    this.setState({ expanded: true });
+    if (onShow) {
+      onShow();
+    }
+  },
+
+  keyUp (e) {
+    const { onSubmit } = this.props;
+    switch (e.key) {
+    case 'Enter':
+      if (onSubmit) {
+        onSubmit();
+      }
+      break;
+    case 'Escape':
+      focusRoot();
+    }
+  },
+};
+
+//  The component.
+export default class DrawerSearch extends React.PureComponent {
+
+  constructor (props) {
+    super(props);
+    assignHandlers(this, handlers);
+    this.state = { expanded: false };
+  }
+
+  render () {
+    const {
+      blur,
+      change,
+      clear,
+      focus,
+      keyUp,
+    } = this.handlers;
+    const {
+      intl,
+      submitted,
+      value,
+    } = this.props;
+    const { expanded } = this.state;
+    const computedClass = classNames('drawer--search', { active: value.length || submitted });
+
+    return (
+      <div className={computedClass}>
+        <label>
+          <span {...hiddenComponent}>
+            <FormattedMessage {...messages.placeholder} />
+          </span>
+          <input
+            type='text'
+            placeholder={intl.formatMessage(messages.placeholder)}
+            value={value}
+            onChange={change}
+            onKeyUp={keyUp}
+            onFocus={focus}
+            onBlur={blur}
+          />
+        </label>
+        <div
+          aria-label={intl.formatMessage(messages.placeholder)}
+          className='icon'
+          onClick={clear}
+          role='button'
+          tabIndex='0'
+        >
+          <Icon icon='search' />
+          <Icon icon='fa-times-circle' />
+        </div>
+
+        <Overlay
+          placement='bottom'
+          show={expanded && !value.length && !submitted}
+          target={this}
+        ><DrawerSearchPopout /></Overlay>
+      </div>
+    );
+  }
+
+}
+
+DrawerSearch.propTypes = {
+  value: PropTypes.string,
+  submitted: PropTypes.bool,
+  onChange: PropTypes.func,
+  onSubmit: PropTypes.func,
+  onClear: PropTypes.func,
+  onShow: PropTypes.func,
+  intl: PropTypes.object,
+};
diff --git a/app/javascript/flavours/glitch/features/drawer/search/popout/index.js b/app/javascript/flavours/glitch/features/drawer/search/popout/index.js
new file mode 100644
index 000000000..bd36275f5
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/drawer/search/popout/index.js
@@ -0,0 +1,95 @@
+//  Package imports.
+import PropTypes from 'prop-types';
+import React from 'react';
+import {
+  FormattedMessage,
+  defineMessages,
+} from 'react-intl';
+import spring from 'react-motion/lib/spring';
+
+//  Utils.
+import Motion from 'flavours/glitch/util/optional_motion';
+
+//  Messages.
+const messages = defineMessages({
+  format: {
+    defaultMessage: 'Advanced search format',
+    id: 'search_popout.search_format',
+  },
+  hashtag: {
+    defaultMessage: 'hashtag',
+    id: 'search_popout.tips.hashtag',
+  },
+  status: {
+    defaultMessage: 'status',
+    id: 'search_popout.tips.status',
+  },
+  text: {
+    defaultMessage: 'Simple text returns matching display names, usernames and hashtags',
+    id: 'search_popout.tips.text',
+  },
+  user: {
+    defaultMessage: 'user',
+    id: 'search_popout.tips.user',
+  },
+});
+
+const motionSpring = spring(1, { damping: 35, stiffness: 400 });
+
+export default function DrawerSearchPopout ({ style }) {
+  return (
+    <Motion
+      defaultStyle={{
+        opacity: 0,
+        scaleX: 0.85,
+        scaleY: 0.75,
+      }}
+      style={{
+        opacity: motionSpring,
+        scaleX: motionSpring,
+        scaleY: motionSpring,
+      }}
+    >
+      {({ opacity, scaleX, scaleY }) => (
+        <div
+          className='drawer--search--popout'
+          style={{
+            ...style,
+            position: 'absolute',
+            width: 285,
+            opacity: opacity,
+            transform: `scale(${scaleX}, ${scaleY})`,
+          }}
+        >
+          <h4><FormattedMessage {...messages.format} /></h4>
+          <ul>
+            <li>
+              <em>#example</em>
+              {' '}
+              <FormattedMessage {...messages.hashtag} />
+            </li>
+            <li>
+              <em>@username@domain</em>
+              {' '}
+              <FormattedMessage {...messages.user} />
+            </li>
+            <li>
+              <em>URL</em>
+              {' '}
+              <FormattedMessage {...messages.user} />
+            </li>
+            <li>
+              <em>URL</em>
+              {' '}
+              <FormattedMessage {...messages.status} />
+            </li>
+          </ul>
+          <FormattedMessage {...messages.text} />
+        </div>
+      )}
+    </Motion>
+  );
+}
+
+//  Props.
+DrawerSearchPopout.propTypes = { style: PropTypes.object };
diff --git a/app/javascript/flavours/glitch/util/dom_helpers.js b/app/javascript/flavours/glitch/util/dom_helpers.js
index ee95ef8dd..3e1f4a26d 100644
--- a/app/javascript/flavours/glitch/util/dom_helpers.js
+++ b/app/javascript/flavours/glitch/util/dom_helpers.js
@@ -4,3 +4,11 @@ import detectPassiveEvents from 'detect-passive-events';
 //  This will either be a passive lister options object (if passive
 //  events are supported), or `false`.
 export const withPassive = detectPassiveEvents.hasSupport ? { passive: true } : false;
+
+//  Focuses the root element.
+export function focusRoot () {
+  let e;
+  if (document && (e = document.querySelector('.ui')) && (e = e.parentElement)) {
+    e.focus();
+  }
+}
diff --git a/app/javascript/flavours/glitch/util/react_helpers.js b/app/javascript/flavours/glitch/util/react_helpers.js
index 0826f3584..087e3969d 100644
--- a/app/javascript/flavours/glitch/util/react_helpers.js
+++ b/app/javascript/flavours/glitch/util/react_helpers.js
@@ -14,7 +14,7 @@ export function assignHandlers (target, handlers) {
 //  This function only returns the component if the result of calling
 //  `test` with `data` is `true`.  Useful with funciton binding.
 export function conditionalRender (test, data, component) {
-  return test ? component : null;
+  return test(data) ? component : null;
 }
 
 //  This object provides props to make the component not visible.
diff --git a/app/javascript/flavours/glitch/util/redux_helpers.js b/app/javascript/flavours/glitch/util/redux_helpers.js
index 3bc8bc86f..c0f5eeb28 100644
--- a/app/javascript/flavours/glitch/util/redux_helpers.js
+++ b/app/javascript/flavours/glitch/util/redux_helpers.js
@@ -1,3 +1,6 @@
+import { injectIntl } from 'react-intl';
+import { connect } from 'react-redux';
+
 //  Merges react-redux props.
 export function mergeProps (stateProps, dispatchProps, ownProps) {
   Object.assign({}, ownProps, {
@@ -5,3 +8,9 @@ export function mergeProps (stateProps, dispatchProps, ownProps) {
     state: Object.assign({}, stateProps, ownProps.state || {}),
   });
 }
+
+//  Connects a component.
+export function wrap (Component, mapStateToProps, mapDispatchToProps, options) {
+  const withIntl = typeof options === 'object' ? options.withIntl : !!options;
+  return (withIntl ? injectIntl : i => i)(connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component));
+}