-
- } />
+
+
+
+ }
+ />
);
}
diff --git a/app/javascript/mastodon/features/ui/components/column.js b/app/javascript/mastodon/features/ui/components/column.js
index e8973f595..fb3d35b98 100644
--- a/app/javascript/mastodon/features/ui/components/column.js
+++ b/app/javascript/mastodon/features/ui/components/column.js
@@ -2,34 +2,7 @@ import React from 'react';
import ColumnHeader from './column_header';
import PropTypes from 'prop-types';
import { debounce } from 'lodash';
-
-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;
- };
-};
+import scrollTop from '../../../scroll';
class Column extends React.PureComponent {
@@ -43,9 +16,11 @@ class Column extends React.PureComponent {
handleHeaderClick = () => {
const scrollable = this.node.querySelector('.scrollable');
+
if (!scrollable) {
return;
}
+
this._interruptScrollAnimation = scrollTop(scrollable);
}
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index ccff417ef..6ed8bc20d 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -1,16 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import HomeTimeline from '../../home_timeline';
+import Notifications from '../../notifications';
+import PublicTimeline from '../../public_timeline';
+import CommunityTimeline from '../../community_timeline';
+import HashtagTimeline from '../../hashtag_timeline';
+import Compose from '../../compose';
-class ColumnsArea extends React.PureComponent {
+const componentMap = {
+ 'COMPOSE': Compose,
+ 'HOME': HomeTimeline,
+ 'NOTIFICATIONS': Notifications,
+ 'PUBLIC': PublicTimeline,
+ 'COMMUNITY': CommunityTimeline,
+ 'HASHTAG': HashtagTimeline,
+};
+
+class ColumnsArea extends ImmutablePureComponent {
static propTypes = {
+ columns: ImmutablePropTypes.list.isRequired,
+ singleColumn: PropTypes.bool,
children: PropTypes.node,
};
render () {
+ const { columns, children, singleColumn } = this.props;
+
+ if (singleColumn) {
+ return (
+
+ {children}
+
+ );
+ }
+
return (
- {this.props.children}
+ {columns.map(column => {
+ const SpecificComponent = componentMap[column.get('id')];
+ const params = column.get('params', null) === null ? null : column.get('params').toJS();
+ return ;
+ })}
+
+ {React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))}
);
}
diff --git a/app/javascript/mastodon/features/ui/containers/columns_area_container.js b/app/javascript/mastodon/features/ui/containers/columns_area_container.js
new file mode 100644
index 000000000..6420f0784
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/containers/columns_area_container.js
@@ -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);
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index af124b1ee..9452e7fcf 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -1,13 +1,9 @@
import React from 'react';
-import ColumnsArea from './components/columns_area';
import NotificationsContainer from './containers/notifications_container';
import PropTypes from 'prop-types';
import LoadingBarContainer from './containers/loading_bar_container';
-import HomeTimeline from '../home_timeline';
-import Compose from '../compose';
import TabsBar from './components/tabs_bar';
import ModalContainer from './containers/modal_container';
-import Notifications from '../notifications';
import { connect } from 'react-redux';
import { isMobile } from '../../is_mobile';
import { debounce } from 'lodash';
@@ -15,6 +11,7 @@ import { uploadCompose } from '../../actions/compose';
import { refreshTimeline } from '../../actions/timelines';
import { refreshNotifications } from '../../actions/notifications';
import UploadArea from './components/upload_area';
+import ColumnsAreaContainer from './containers/columns_area_container';
const noOp = () => false;
@@ -119,31 +116,10 @@ class UI extends React.PureComponent {
const { width, draggingOver } = this.state;
const { children } = this.props;
- let mountedColumns;
-
- if (isMobile(width)) {
- mountedColumns = (
-
- {children}
-
- );
- } else {
- mountedColumns = (
-
-
-
-
- {children}
-
- );
- }
-
return (
-
- {mountedColumns}
-
+ {children}
diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js
index ababd4983..ad70806b1 100644
--- a/app/javascript/mastodon/reducers/settings.js
+++ b/app/javascript/mastodon/reducers/settings.js
@@ -1,10 +1,18 @@
import { SETTING_CHANGE } from '../actions/settings';
+import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from '../actions/columns';
import { STORE_HYDRATE } from '../actions/store';
import Immutable from 'immutable';
+import uuid from '../uuid';
const initialState = Immutable.Map({
onboarded: false,
+ columns: Immutable.fromJS([
+ { id: 'COMPOSE', uuid: uuid(), params: {} },
+ { id: 'HOME', uuid: uuid(), params: {} },
+ { id: 'NOTIFICATIONS', uuid: uuid(), params: {} },
+ ]),
+
home: Immutable.Map({
shows: Immutable.Map({
reblog: true,
@@ -40,12 +48,31 @@ const initialState = Immutable.Map({
}),
});
+const moveColumn = (state, uuid, direction) => {
+ const columns = state.get('columns');
+ const index = columns.findIndex(item => item.get('uuid') === uuid);
+ const newIndex = index + direction;
+
+ let newColumns;
+
+ newColumns = columns.splice(index, 1);
+ newColumns = newColumns.splice(newIndex, 0, columns.get(index));
+
+ return state.set('columns', newColumns);
+};
+
export default function settings(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:
return state.mergeDeep(action.state.get('settings'));
case SETTING_CHANGE:
return state.setIn(action.key, action.value);
+ case COLUMN_ADD:
+ return state.update('columns', list => list.push(Immutable.fromJS({ id: action.id, uuid: uuid(), params: action.params })));
+ case COLUMN_REMOVE:
+ return state.update('columns', list => list.filterNot(item => item.get('uuid') === action.uuid));
+ case COLUMN_MOVE:
+ return moveColumn(state, action.uuid, action.direction);
default:
return state;
}
diff --git a/app/javascript/mastodon/scroll.js b/app/javascript/mastodon/scroll.js
new file mode 100644
index 000000000..c089d37db
--- /dev/null
+++ b/app/javascript/mastodon/scroll.js
@@ -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;
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss
index a61705282..ddd7e4ced 100644
--- a/app/javascript/styles/components.scss
+++ b/app/javascript/styles/components.scss
@@ -1526,6 +1526,22 @@
}
}
+.column-header__back-button {
+ background: lighten($ui-base-color, 4%);
+ border: 0;
+ font-family: inherit;
+ color: $ui-highlight-color;
+ cursor: pointer;
+ flex: 0 0 auto;
+ font-size: 16px;
+ padding: 15px;
+ z-index: 3;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
.column-back-button__icon {
display: inline-block;
margin-right: 5px;
@@ -2030,6 +2046,89 @@ button.icon-button.active i.fa-retweet {
}
}
+.column-header__buttons {
+ position: absolute;
+ right: 0;
+ top: 0;
+ display: flex;
+}
+
+.column-header__button {
+ background: lighten($ui-base-color, 4%);
+ border: 0;
+ color: $ui-primary-color;
+ cursor: pointer;
+ font-size: 16px;
+ padding: 15px;
+
+ &:hover {
+ color: lighten($ui-primary-color, 7%);
+ }
+
+ &.active {
+ color: $primary-text-color;
+ background: lighten($ui-base-color, 8%);
+
+ &:hover {
+ color: $primary-text-color;
+ background: lighten($ui-base-color, 8%);
+ }
+ }
+}
+
+.column-header__collapsible {
+ max-height: 70vh;
+ overflow: hidden;
+ overflow-y: auto;
+ color: $ui-primary-color;
+ transition: max-height 150ms ease-in-out, opacity 300ms linear;
+ opacity: 1;
+
+ & > div {
+ background: lighten($ui-base-color, 8%);
+ padding: 15px;
+ }
+
+ &.collapsed {
+ max-height: 0;
+ opacity: 0.5;
+ }
+
+ &.animating {
+ overflow-y: hidden;
+ }
+}
+
+.column-header__setting-btn {
+ &:hover {
+ color: lighten($ui-primary-color, 4%);
+ text-decoration: underline;
+ }
+}
+
+.column-header__setting-arrows {
+ float: right;
+
+ .column-header__setting-btn {
+ padding: 0 10px;
+
+ &:last-child {
+ padding-right: 0;
+ }
+ }
+}
+
+.text-btn {
+ display: inline-block;
+ padding: 0;
+ font-family: inherit;
+ font-size: inherit;
+ color: inherit;
+ border: 0;
+ background: transparent;
+ cursor: pointer;
+}
+
.column-header__icon {
display: inline-block;
margin-right: 5px;