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 mountedlocal
parent
20b647020b
commit
8ee2eb5d2e
21 changed files with 754 additions and 153 deletions
@ -0,0 +1,40 @@ |
||||
import { saveSettings } from './settings'; |
||||
|
||||
export const COLUMN_ADD = 'COLUMN_ADD'; |
||||
export const COLUMN_REMOVE = 'COLUMN_REMOVE'; |
||||
export const COLUMN_MOVE = 'COLUMN_MOVE'; |
||||
|
||||
export function addColumn(id, params) { |
||||
return dispatch => { |
||||
dispatch({ |
||||
type: COLUMN_ADD, |
||||
id, |
||||
params, |
||||
}); |
||||
|
||||
dispatch(saveSettings()); |
||||
}; |
||||
}; |
||||
|
||||
export function removeColumn(uuid) { |
||||
return dispatch => { |
||||
dispatch({ |
||||
type: COLUMN_REMOVE, |
||||
uuid, |
||||
}); |
||||
|
||||
dispatch(saveSettings()); |
||||
}; |
||||
}; |
||||
|
||||
export function moveColumn(uuid, direction) { |
||||
return dispatch => { |
||||
dispatch({ |
||||
type: COLUMN_MOVE, |
||||
uuid, |
||||
direction, |
||||
}); |
||||
|
||||
dispatch(saveSettings()); |
||||
}; |
||||
}; |
@ -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; |
@ -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; |
@ -0,0 +1,8 @@ |
||||
import { connect } from 'react-redux'; |
||||
import ColumnsArea from '../components/columns_area'; |
||||
|
||||
const mapStateToProps = state => ({ |
||||
columns: state.getIn(['settings', 'columns']), |
||||
}); |
||||
|
||||
export default connect(mapStateToProps)(ColumnsArea); |
@ -0,0 +1,29 @@ |
||||
const easingOutQuint = (x, t, b, c, d) => c * ((t = t / d - 1) * t * t * t * t + 1) + b; |
||||
|
||||
const scrollTop = (node) => { |
||||
const startTime = Date.now(); |
||||
const offset = node.scrollTop; |
||||
const targetY = -offset; |
||||
const duration = 1000; |
||||
let interrupt = false; |
||||
|
||||
const step = () => { |
||||
const elapsed = Date.now() - startTime; |
||||
const percentage = elapsed / duration; |
||||
|
||||
if (percentage > 1 || interrupt) { |
||||
return; |
||||
} |
||||
|
||||
node.scrollTop = easingOutQuint(0, elapsed, offset, targetY, duration); |
||||
requestAnimationFrame(step); |
||||
}; |
||||
|
||||
step(); |
||||
|
||||
return () => { |
||||
interrupt = true; |
||||
}; |
||||
}; |
||||
|
||||
export default scrollTop; |
Loading…
Reference in new issue