about summary refs log tree commit diff
path: root/app/javascript/mastodon/components
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2017-06-04 01:39:38 +0200
committerGitHub <noreply@github.com>2017-06-04 01:39:38 +0200
commit8ee2eb5d2e7bd3c601c0277f12d8ad0c5f84cc43 (patch)
tree2c51c5c5cd47273cf1b66d553e1cc5c7f762a0f8 /app/javascript/mastodon/components
parent20b647020bf8de2af6d2ce44ed76566d137dd1f6 (diff)
Allow mounting arbitrary columns (#3207)
* Allow mounting arbitrary columns

* Refactor column headers, allow pinning/unpinning and moving columns around

* Collapse animation

* Re-introduce scroll to top

* Save column settings properly, do not display pin options in
single-column view, do not display collapse icon if there is
nothing to collapse

* Fix one instance of public timeline being closed closing the stream
Fix back buttons inconsistently sending you back to / even if history exists

* Getting started displays links to columns that are not mounted
Diffstat (limited to 'app/javascript/mastodon/components')
-rw-r--r--app/javascript/mastodon/components/column.js45
-rw-r--r--app/javascript/mastodon/components/column_back_button.js2
-rw-r--r--app/javascript/mastodon/components/column_back_button_slim.js3
-rw-r--r--app/javascript/mastodon/components/column_header.js138
4 files changed, 186 insertions, 2 deletions
diff --git a/app/javascript/mastodon/components/column.js b/app/javascript/mastodon/components/column.js
new file mode 100644
index 000000000..157a89c0e
--- /dev/null
+++ b/app/javascript/mastodon/components/column.js
@@ -0,0 +1,45 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import scrollTop from '../scroll';
+
+class Column extends React.PureComponent {
+
+  static propTypes = {
+    children: PropTypes.node,
+  };
+
+  scrollTop () {
+    const scrollable = this.node.querySelector('.scrollable');
+
+    if (!scrollable) {
+      return;
+    }
+
+    this._interruptScrollAnimation = scrollTop(scrollable);
+  }
+
+  handleWheel = () => {
+    if (typeof this._interruptScrollAnimation !== 'function') {
+      return;
+    }
+
+    this._interruptScrollAnimation();
+  }
+
+  setRef = c => {
+    this.node = c;
+  }
+
+  render () {
+    const { children } = this.props;
+
+    return (
+      <div role='region' className='column' ref={this.setRef} onWheel={this.handleWheel}>
+        {children}
+      </div>
+    );
+  }
+
+}
+
+export default Column;
diff --git a/app/javascript/mastodon/components/column_back_button.js b/app/javascript/mastodon/components/column_back_button.js
index 9d2de40f5..6c61ca6d4 100644
--- a/app/javascript/mastodon/components/column_back_button.js
+++ b/app/javascript/mastodon/components/column_back_button.js
@@ -9,7 +9,7 @@ class ColumnBackButton extends React.PureComponent {
   };
 
   handleClick = () => {
-    if (window.history && window.history.length === 1) this.context.router.push("/");
+    if (window.history && window.history.length === 1) this.context.router.push('/');
     else this.context.router.goBack();
   }
 
diff --git a/app/javascript/mastodon/components/column_back_button_slim.js b/app/javascript/mastodon/components/column_back_button_slim.js
index 6f6bbc0b8..2d3f1b57a 100644
--- a/app/javascript/mastodon/components/column_back_button_slim.js
+++ b/app/javascript/mastodon/components/column_back_button_slim.js
@@ -9,7 +9,8 @@ class ColumnBackButtonSlim extends React.PureComponent {
   };
 
   handleClick = () => {
-    this.context.router.push('/');
+    if (window.history && window.history.length === 1) this.context.router.push('/');
+    else this.context.router.goBack();
   }
 
   render () {
diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js
new file mode 100644
index 000000000..e6349a399
--- /dev/null
+++ b/app/javascript/mastodon/components/column_header.js
@@ -0,0 +1,138 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import { FormattedMessage } from 'react-intl';
+
+class ColumnHeader extends React.PureComponent {
+
+  static contextTypes = {
+    router: PropTypes.object,
+  };
+
+  static propTypes = {
+    title: PropTypes.string.isRequired,
+    icon: PropTypes.string.isRequired,
+    active: PropTypes.bool,
+    multiColumn: PropTypes.bool,
+    children: PropTypes.node,
+    pinned: PropTypes.bool,
+    onPin: PropTypes.func,
+    onMove: PropTypes.func,
+    onClick: PropTypes.func,
+  };
+
+  state = {
+    collapsed: true,
+    animating: false,
+  };
+
+  handleToggleClick = (e) => {
+    e.stopPropagation();
+    this.setState({ collapsed: !this.state.collapsed, animating: true });
+  }
+
+  handleTitleClick = () => {
+    this.props.onClick();
+  }
+
+  handleMoveLeft = () => {
+    this.props.onMove(-1);
+  }
+
+  handleMoveRight = () => {
+    this.props.onMove(1);
+  }
+
+  handleBackClick = () => {
+    if (window.history && window.history.length === 1) this.context.router.push('/');
+    else this.context.router.goBack();
+  }
+
+  handleTransitionEnd = () => {
+    this.setState({ animating: false });
+  }
+
+  render () {
+    const { title, icon, active, children, pinned, onPin, multiColumn } = this.props;
+    const { collapsed, animating } = this.state;
+
+    const buttonClassName = classNames('column-header', {
+      'active': active,
+    });
+
+    const collapsibleClassName = classNames('column-header__collapsible', {
+      'collapsed': collapsed,
+      'animating': animating,
+    });
+
+    const collapsibleButtonClassName = classNames('column-header__button', {
+      'active': !collapsed,
+    });
+
+    let extraContent, pinButton, moveButtons, backButton, collapseButton;
+
+    if (children) {
+      extraContent = (
+        <div key='extra-content' className='column-header__collapsible__extra'>
+          {children}
+        </div>
+      );
+    }
+
+    if (multiColumn && pinned) {
+      pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={onPin}><i className='fa fa fa-times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
+
+      moveButtons = (
+        <div key='move-buttons' className='column-header__setting-arrows'>
+          <button className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button>
+          <button className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button>
+        </div>
+      );
+    } else if (multiColumn) {
+      pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={onPin}><i className='fa fa fa-plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
+
+      backButton = (
+        <button onClick={this.handleBackClick} className='column-header__back-button'>
+          <i className='fa fa-fw fa-chevron-left column-back-button__icon' />
+          <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
+        </button>
+      );
+    }
+
+    const collapsedContent = [
+      extraContent,
+    ];
+
+    if (multiColumn) {
+      collapsedContent.push(moveButtons);
+      collapsedContent.push(pinButton);
+    }
+
+    if (children || multiColumn) {
+      collapseButton = <button className={collapsibleButtonClassName} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>;
+    }
+
+    return (
+      <div>
+        <div role='button heading' tabIndex='0' className={buttonClassName} onClick={this.handleTitleClick}>
+          <i className={`fa fa-fw fa-${icon} column-header__icon`} />
+          {title}
+
+          <div className='column-header__buttons'>
+            {backButton}
+            {collapseButton}
+          </div>
+        </div>
+
+        <div className={collapsibleClassName} onTransitionEnd={this.handleTransitionEnd}>
+          <div>
+            {(!collapsed || animating) && collapsedContent}
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default ColumnHeader;