about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/features/drawer
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours/glitch/features/drawer')
-rw-r--r--app/javascript/flavours/glitch/features/drawer/account/index.js71
-rw-r--r--app/javascript/flavours/glitch/features/drawer/header/index.js118
-rw-r--r--app/javascript/flavours/glitch/features/drawer/index.js137
-rw-r--r--app/javascript/flavours/glitch/features/drawer/results/index.js116
-rw-r--r--app/javascript/flavours/glitch/features/drawer/search/index.js152
-rw-r--r--app/javascript/flavours/glitch/features/drawer/search/popout/index.js104
6 files changed, 698 insertions, 0 deletions
diff --git a/app/javascript/flavours/glitch/features/drawer/account/index.js b/app/javascript/flavours/glitch/features/drawer/account/index.js
new file mode 100644
index 000000000..168d0c2cf
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/drawer/account/index.js
@@ -0,0 +1,71 @@
+//  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 DrawerAccount ({ account }) {
+
+  //  We need an account to render.
+  if (!account) {
+    return (
+      <div className='drawer--account'>
+        <a
+          className='edit'
+          href='/settings/profile'
+        >
+          <FormattedMessage {...messages.edit} />
+        </a>
+      </div>
+    );
+  }
+
+  //  The result.
+  return (
+    <div className='drawer--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>
+  );
+}
+
+//  Props.
+DrawerAccount.propTypes = { account: ImmutablePropTypes.map };
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..6949cd028
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/drawer/header/index.js
@@ -0,0 +1,118 @@
+//  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(null,
+    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>
+  );
+}
+
+//  Props.
+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
new file mode 100644
index 000000000..e6a689575
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/drawer/index.js
@@ -0,0 +1,137 @@
+//  Package imports.
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import classNames from 'classnames';
+
+//  Actions.
+import { openModal } from 'flavours/glitch/actions/modal';
+import {
+  changeSearch,
+  clearSearch,
+  showSearch,
+  submitSearch,
+} from 'flavours/glitch/actions/search';
+import { cycleElefriendCompose } from 'flavours/glitch/actions/compose';
+
+//  Components.
+import Composer from 'flavours/glitch/features/composer';
+import DrawerAccount from './account';
+import DrawerHeader from './header';
+import DrawerResults from './results';
+import DrawerSearch from './search';
+
+//  Utils.
+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']),
+  elefriend: state.getIn(['compose', 'elefriend']),
+  results: state.getIn(['search', 'results']),
+  searchHidden: state.getIn(['search', 'hidden']),
+  searchValue: state.getIn(['search', 'value']),
+  submitted: state.getIn(['search', 'submitted']),
+});
+
+//  Dispatch mapping.
+const mapDispatchToProps = {
+  onChange: changeSearch,
+  onClear: clearSearch,
+  onClickElefriend: cycleElefriendCompose,
+  onShow: showSearch,
+  onSubmit: submitSearch,
+  onOpenSettings: openModal.bind(null, 'SETTINGS', {}),
+};
+
+//  The component.
+class Drawer extends React.Component {
+
+  //  Constructor.
+  constructor (props) {
+    super(props);
+  }
+
+  //  Rendering.
+  render () {
+    const {
+      account,
+      columns,
+      elefriend,
+      intl,
+      multiColumn,
+      onChange,
+      onClear,
+      onClickElefriend,
+      onOpenSettings,
+      onShow,
+      onSubmit,
+      results,
+      searchHidden,
+      searchValue,
+      submitted,
+    } = this.props;
+    const computedClass = classNames('drawer', `mbstobon-${elefriend}`);
+
+    //  The result.
+    return (
+      <div className={computedClass}>
+        {multiColumn ? (
+          <DrawerHeader
+            columns={columns}
+            intl={intl}
+            onSettingsClick={onOpenSettings}
+          />
+        ) : null}
+        <DrawerSearch
+          intl={intl}
+          onChange={onChange}
+          onClear={onClear}
+          onShow={onShow}
+          onSubmit={onSubmit}
+          submitted={submitted}
+          value={searchValue}
+        />
+        <div className='contents'>
+          <DrawerAccount account={account} />
+          <Composer />
+          {multiColumn && <button className='mastodon' onClick={onClickElefriend} />}
+          <DrawerResults
+            results={results}
+            visible={submitted && !searchHidden}
+          />
+        </div>
+      </div>
+    );
+  }
+
+}
+
+//  Props.
+Drawer.propTypes = {
+  intl: PropTypes.object.isRequired,
+  multiColumn: PropTypes.bool,
+
+  //  State props.
+  account: ImmutablePropTypes.map,
+  columns: ImmutablePropTypes.list,
+  results: ImmutablePropTypes.map,
+  elefriend: PropTypes.number,
+  searchHidden: PropTypes.bool,
+  searchValue: PropTypes.string,
+  submitted: PropTypes.bool,
+
+  //  Dispatch props.
+  onChange: PropTypes.func,
+  onClear: PropTypes.func,
+  onClickElefriend: PropTypes.func,
+  onShow: PropTypes.func,
+  onSubmit: PropTypes.func,
+  onOpenSettings: PropTypes.func,
+};
+
+//  Connecting and export.
+export { Drawer as WrappedComponent };
+export default wrap(Drawer, mapStateToProps, mapDispatchToProps, true);
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..f2a79eb59
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/drawer/results/index.js
@@ -0,0 +1,116 @@
+//  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 DrawerResults ({
+  results,
+  visible,
+}) {
+  const accounts = results ? results.get('accounts') : null;
+  const statuses = results ? results.get('statuses') : null;
+  const hashtags = results ? results.get('hashtags') : null;
+
+  //  This gets the total number of items.
+  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>
+  );
+}
+
+//  Props.
+DrawerResults.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..8cbb0906c
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/drawer/search/index.js
@@ -0,0 +1,152 @@
+//  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 = {
+
+  handleBlur () {
+    this.setState({ expanded: false });
+  },
+
+  handleChange ({ target: { value } }) {
+    const { onChange } = this.props;
+    if (onChange) {
+      onChange(value);
+    }
+  },
+
+  handleClear (e) {
+    const {
+      onClear,
+      submitted,
+      value,
+    } = this.props;
+    e.preventDefault();  //  Prevents focus change ??
+    if (onClear && (submitted || value && value.length)) {
+      onClear();
+    }
+  },
+
+  handleFocus () {
+    const { onShow } = this.props;
+    this.setState({ expanded: true });
+    if (onShow) {
+      onShow();
+    }
+  },
+
+  handleKeyUp (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.
+  constructor (props) {
+    super(props);
+    assignHandlers(this, handlers);
+    this.state = { expanded: false };
+  }
+
+  //  Rendering.
+  render () {
+    const {
+      handleBlur,
+      handleChange,
+      handleClear,
+      handleFocus,
+      handleKeyUp,
+    } = this.handlers;
+    const {
+      intl,
+      submitted,
+      value,
+    } = this.props;
+    const { expanded } = this.state;
+    const active = value && value.length || submitted;
+    const computedClass = classNames('drawer--search', { active });
+
+    return (
+      <div className={computedClass}>
+        <label>
+          <span {...hiddenComponent}>
+            <FormattedMessage {...messages.placeholder} />
+          </span>
+          <input
+            type='text'
+            placeholder={intl.formatMessage(messages.placeholder)}
+            value={value || ''}
+            onChange={handleChange}
+            onKeyUp={handleKeyUp}
+            onFocus={handleFocus}
+            onBlur={handleBlur}
+          />
+        </label>
+        <div
+          aria-label={intl.formatMessage(messages.placeholder)}
+          className='icon'
+          onClick={handleClear}
+          role='button'
+          tabIndex='0'
+        >
+          <Icon icon='search' />
+          <Icon icon='times-circle' />
+        </div>
+        <Overlay
+          placement='bottom'
+          show={expanded && !active}
+          target={this}
+        ><DrawerSearchPopout /></Overlay>
+      </div>
+    );
+  }
+
+}
+
+//  Props.
+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..6219f46ca
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/drawer/search/popout/index.js
@@ -0,0 +1,104 @@
+//  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',
+  },
+});
+
+//  The spring used by our motion.
+const motionSpring = spring(1, { damping: 35, stiffness: 400 });
+
+//  The component.
+export default function DrawerSearchPopout ({ style }) {
+
+  //  The result.
+  return (
+    <div
+      className='drawer--search--popout'
+      style={{
+        ...style,
+        position: 'absolute',
+        width: 285,
+      }}
+    >
+      <Motion
+        defaultStyle={{
+          opacity: 0,
+          scaleX: 0.85,
+          scaleY: 0.75,
+        }}
+        style={{
+          opacity: motionSpring,
+          scaleX: motionSpring,
+          scaleY: motionSpring,
+        }}
+      >
+        {({ opacity, scaleX, scaleY }) => (
+          <div
+            style={{
+              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>
+    </div>
+  );
+}
+
+//  Props.
+DrawerSearchPopout.propTypes = { style: PropTypes.object };