commit: 45776b55b0d97d6a6b8b202d3076f19f1d055573
parent: e2ff39bf5db8cecd2bbe348dc2c3cf431a6d36ec
Author: Eugen Rochko <eugen@zeonfederated.com>
Date: Wed, 12 Oct 2016 13:17:17 +0200
Responsively changing layout to single-column + nav on smaller screens
Diffstat:
13 files changed, 220 insertions(+), 100 deletions(-)
diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx
@@ -15,12 +15,15 @@ import {
hashHistory,
IndexRoute
} from 'react-router';
+import UI from '../features/ui';
import Account from '../features/account';
import Status from '../features/status';
import GettingStarted from '../features/getting_started';
import PublicTimeline from '../features/public_timeline';
-import UI from '../features/ui';
import AccountTimeline from '../features/account_timeline';
+import HomeTimeline from '../features/home_timeline';
+import MentionsTimeline from '../features/mentions_timeline';
+import Compose from '../features/compose';
const store = configureStore();
@@ -77,6 +80,9 @@ const Mastodon = React.createClass({
<Router history={hashHistory}>
<Route path='/' component={UI}>
<IndexRoute component={GettingStarted} />
+ <Route path='/statuses/new' component={Compose} />
+ <Route path='/statuses/home' component={HomeTimeline} />
+ <Route path='/statuses/mentions' component={MentionsTimeline} />
<Route path='/statuses/all' component={PublicTimeline} />
<Route path='/statuses/:statusId' component={Status} />
<Route path='/accounts/:accountId' component={Account}>
diff --git a/app/assets/javascripts/components/features/compose/index.jsx b/app/assets/javascripts/components/features/compose/index.jsx
@@ -0,0 +1,28 @@
+import Drawer from '../ui/components/drawer';
+import ComposeFormContainer from '../ui/containers/compose_form_container';
+import FollowFormContainer from '../ui/containers/follow_form_container';
+import UploadFormContainer from '../ui/containers/upload_form_container';
+import NavigationContainer from '../ui/containers/navigation_container';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+
+const Compose = React.createClass({
+
+ mixins: [PureRenderMixin],
+
+ render () {
+ return (
+ <Drawer>
+ <div style={{ flex: '1 1 auto' }}>
+ <NavigationContainer />
+ <ComposeFormContainer />
+ <UploadFormContainer />
+ </div>
+
+ <FollowFormContainer />
+ </Drawer>
+ );
+ }
+
+});
+
+export default Compose;
diff --git a/app/assets/javascripts/components/features/home_timeline/index.jsx b/app/assets/javascripts/components/features/home_timeline/index.jsx
@@ -0,0 +1,19 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import StatusListContainer from '../ui/containers/status_list_container';
+import Column from '../ui/components/column';
+
+const HomeTimeline = React.createClass({
+
+ mixins: [PureRenderMixin],
+
+ render () {
+ return (
+ <Column icon='home' heading='Home'>
+ <StatusListContainer type='home' />
+ </Column>
+ );
+ },
+
+});
+
+export default HomeTimeline;
diff --git a/app/assets/javascripts/components/features/mentions_timeline/index.jsx b/app/assets/javascripts/components/features/mentions_timeline/index.jsx
@@ -0,0 +1,19 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import StatusListContainer from '../ui/containers/status_list_container';
+import Column from '../ui/components/column';
+
+const MentionsTimeline = React.createClass({
+
+ mixins: [PureRenderMixin],
+
+ render () {
+ return (
+ <Column icon='at' heading='Mentions'>
+ <StatusListContainer type='mentions' />
+ </Column>
+ );
+ },
+
+});
+
+export default MentionsTimeline;
diff --git a/app/assets/javascripts/components/features/public_timeline/index.jsx b/app/assets/javascripts/components/features/public_timeline/index.jsx
@@ -1,43 +1,14 @@
import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import StatusList from '../../components/status_list';
+import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../ui/components/column';
-import Immutable from 'immutable';
-import { makeGetTimeline } from '../../selectors';
import {
- updateTimeline,
refreshTimeline,
- expandTimeline
+ updateTimeline
} from '../../actions/timelines';
-import { deleteStatus } from '../../actions/statuses';
-import { replyCompose } from '../../actions/compose';
-import {
- favourite,
- reblog,
- unreblog,
- unfavourite
-} from '../../actions/interactions';
-
-const makeMapStateToProps = () => {
- const getTimeline = makeGetTimeline();
-
- const mapStateToProps = (state) => ({
- statuses: getTimeline(state, 'public'),
- me: state.getIn(['timelines', 'me'])
- });
-
- return mapStateToProps;
-};
const PublicTimeline = React.createClass({
- propTypes: {
- statuses: ImmutablePropTypes.list.isRequired,
- me: React.PropTypes.number.isRequired,
- dispatch: React.PropTypes.func.isRequired
- },
-
mixins: [PureRenderMixin],
componentWillMount () {
@@ -62,44 +33,14 @@ const PublicTimeline = React.createClass({
}
},
- handleReply (status) {
- this.props.dispatch(replyCompose(status));
- },
-
- handleReblog (status) {
- if (status.get('reblogged')) {
- this.props.dispatch(unreblog(status));
- } else {
- this.props.dispatch(reblog(status));
- }
- },
-
- handleFavourite (status) {
- if (status.get('favourited')) {
- this.props.dispatch(unfavourite(status));
- } else {
- this.props.dispatch(favourite(status));
- }
- },
-
- handleDelete (status) {
- this.props.dispatch(deleteStatus(status.get('id')));
- },
-
- handleScrollToBottom () {
- this.props.dispatch(expandTimeline('public'));
- },
-
render () {
- const { statuses, me } = this.props;
-
return (
<Column icon='globe' heading='Public'>
- <StatusList statuses={statuses} me={me} onScrollToBottom={this.handleScrollToBottom} onReply={this.handleReply} onReblog={this.handleReblog} onFavourite={this.handleFavourite} onDelete={this.handleDelete} />
+ <StatusListContainer type='public' />
</Column>
);
},
});
-export default connect(makeMapStateToProps)(PublicTimeline);
+export default connect()(PublicTimeline);
diff --git a/app/assets/javascripts/components/features/ui/components/column.jsx b/app/assets/javascripts/components/features/ui/components/column.jsx
@@ -29,6 +29,15 @@ const scrollTop = (node) => {
};
};
+const style = {
+ height: '100%',
+ boxSizing: 'border-box',
+ flex: '0 0 auto',
+ background: '#282c37',
+ display: 'flex',
+ flexDirection: 'column'
+};
+
const Column = React.createClass({
propTypes: {
@@ -56,10 +65,8 @@ const Column = React.createClass({
header = <ColumnHeader icon={this.props.icon} type={this.props.heading} onClick={this.handleHeaderClick} />;
}
- const style = { width: '330px', flex: '0 0 auto', background: '#282c37', margin: '10px', marginRight: '0', marginBottom: '0', display: 'flex', flexDirection: 'column' };
-
return (
- <div style={style} onWheel={this.handleWheel}>
+ <div className='column' style={style} onWheel={this.handleWheel}>
{header}
{this.props.children}
</div>
diff --git a/app/assets/javascripts/components/features/ui/components/columns_area.jsx b/app/assets/javascripts/components/features/ui/components/columns_area.jsx
@@ -1,12 +1,20 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
+const style = {
+ display: 'flex',
+ flex: '1 1 auto',
+ flexDirection: 'row',
+ justifyContent: 'flex-start',
+ overflowX: 'auto'
+};
+
const ColumnsArea = React.createClass({
mixins: [PureRenderMixin],
render () {
return (
- <div style={{ display: 'flex', flexDirection: 'row', flex: '1', justifyContent: 'flex-start', marginRight: '10px', marginBottom: '10px', overflowX: 'auto' }}>
+ <div className='columns-area' style={style}>
{this.props.children}
</div>
);
diff --git a/app/assets/javascripts/components/features/ui/components/drawer.jsx b/app/assets/javascripts/components/features/ui/components/drawer.jsx
@@ -1,12 +1,22 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
+const style = {
+ height: '100%',
+ flex: '0 0 auto',
+ boxSizing: 'border-box',
+ background: '#454b5e',
+ padding: '0',
+ display: 'flex',
+ flexDirection: 'column'
+};
+
const Drawer = React.createClass({
mixins: [PureRenderMixin],
render () {
return (
- <div style={{ width: '280px', flex: '0 0 auto', boxSizing: 'border-box', background: '#454b5e', margin: '10px', marginRight: '0', padding: '0', display: 'flex', flexDirection: 'column' }}>
+ <div className='drawer' style={style}>
{this.props.children}
</div>
);
diff --git a/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx b/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx
@@ -0,0 +1,38 @@
+import { Link } from 'react-router';
+
+const outerStyle = {
+ background: '#373b4a',
+ margin: '10px',
+ flex: '0 0 auto',
+ marginBottom: '0',
+ display: 'flex'
+};
+
+const tabStyle = {
+ display: 'block',
+ flex: '1 1 auto',
+ padding: '10px',
+ color: '#fff',
+ textDecoration: 'none',
+ fontSize: '12px',
+ fontWeight: '500',
+ borderBottom: '2px solid #373b4a'
+};
+
+const tabActiveStyle = {
+ borderBottom: '2px solid #2b90d9',
+ color: '#2b90d9'
+};
+
+const TabsBar = () => {
+ return (
+ <div style={outerStyle}>
+ <Link style={tabStyle} activeStyle={tabActiveStyle} to='/statuses/new'><i className='fa fa-fw fa-pencil' /> Compose</Link>
+ <Link style={tabStyle} activeStyle={tabActiveStyle} to='/statuses/home'><i className='fa fa-fw fa-home' /> Home</Link>
+ <Link style={tabStyle} activeStyle={tabActiveStyle} to='/statuses/mentions'><i className='fa fa-fw fa-at' /> Mentions</Link>
+ <Link style={tabStyle} activeStyle={tabActiveStyle} to='/statuses/all'><i className='fa fa-fw fa-globe' /> Public</Link>
+ </div>
+ );
+};
+
+export default TabsBar;
diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx
@@ -1,47 +1,38 @@
import ColumnsArea from './components/columns_area';
-import Column from './components/column';
-import Drawer from './components/drawer';
-import ComposeFormContainer from './containers/compose_form_container';
-import FollowFormContainer from './containers/follow_form_container';
-import UploadFormContainer from './containers/upload_form_container';
-import StatusListContainer from './containers/status_list_container';
import NotificationsContainer from './containers/notifications_container';
-import NavigationContainer from './containers/navigation_container';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import LoadingBarContainer from './containers/loading_bar_container';
+import HomeTimeline from '../home_timeline';
+import MentionsTimeline from '../mentions_timeline';
+import Compose from '../compose';
+import MediaQuery from 'react-responsive';
+import TabsBar from './components/tabs_bar';
const UI = React.createClass({
- propTypes: {
- router: React.PropTypes.object
- },
-
mixins: [PureRenderMixin],
render () {
+ const layoutBreakpoint = 1024;
+
return (
- <div style={{ flex: '0 0 auto', display: 'flex', width: '100%', height: '100%', background: '#1a1c23' }}>
- <Drawer>
- <div style={{ flex: '1 1 auto' }}>
- <NavigationContainer />
- <ComposeFormContainer />
- <UploadFormContainer />
- </div>
-
- <FollowFormContainer />
- </Drawer>
-
- <ColumnsArea>
- <Column icon='home' heading='Home'>
- <StatusListContainer type='home' />
- </Column>
-
- <Column icon='at' heading='Mentions'>
- <StatusListContainer type='mentions' />
- </Column>
+ <div style={{ flex: '0 0 auto', display: 'flex', flexDirection: 'column', width: '100%', height: '100%', background: '#1a1c23' }}>
+ <MediaQuery maxWidth={layoutBreakpoint}>
+ <TabsBar />
+ </MediaQuery>
+ <MediaQuery maxWidth={layoutBreakpoint} component={ColumnsArea}>
{this.props.children}
- </ColumnsArea>
+ </MediaQuery>
+
+ <MediaQuery minWidth={layoutBreakpoint}>
+ <ColumnsArea>
+ <Compose />
+ <HomeTimeline />
+ <MentionsTimeline />
+ {this.props.children}
+ </ColumnsArea>
+ </MediaQuery>
<NotificationsContainer />
<LoadingBarContainer style={{ backgroundColor: '#2b90d9', left: '0', top: '0' }} />
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
@@ -227,3 +227,31 @@
margin-bottom: 20px;
}
}
+
+.columns-area {
+ margin: 10px;
+ margin-left: 0;
+}
+
+.column {
+ width: 330px;
+}
+
+.drawer {
+ width: 280px;
+}
+
+.column, .drawer {
+ margin-left: 10px;
+}
+
+@media screen and (max-width: 1024px) {
+ .column, .drawer {
+ width: 100%;
+ margin: 0;
+ }
+
+ .columns-area {
+ margin: 10px;
+ }
+}
diff --git a/package.json b/package.json
@@ -38,5 +38,8 @@
"redux-thunk": "^2.1.0",
"reselect": "^2.5.4",
"sinon": "^1.17.6"
+ },
+ "dependencies": {
+ "react-responsive": "^1.1.5"
}
}
diff --git a/yarn.lock b/yarn.lock
@@ -1541,6 +1541,10 @@ css-loader@0.25.0:
postcss-modules-values "^1.1.0"
source-list-map "^0.1.4"
+css-mediaquery@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/css-mediaquery/-/css-mediaquery-0.1.2.tgz#6a2c37344928618631c54bd33cedd301da18bea0"
+
css-select@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
@@ -2359,6 +2363,10 @@ https-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.0.tgz#b3ffdfe734b2a3d4a9efd58e8654c91fce86eafd"
+hyphenate-style-name@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.1.tgz#bc49b9446e02b4570641afdd29c1ce7609d1b9cc"
+
iconv-lite@^0.4.13, iconv-lite@~0.4.13:
version "0.4.13"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2"
@@ -2928,6 +2936,12 @@ mantra-core@^1.6.1:
react-komposer "^1.9.0"
react-simple-di "^1.2.0"
+matchmedia@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/matchmedia/-/matchmedia-0.1.2.tgz#cfd47f2bf68fbc7f5ea1bd3a3cf1715ecba3c1bd"
+ dependencies:
+ css-mediaquery "^0.1.2"
+
math-expression-evaluator@^1.2.14:
version "1.2.14"
resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.14.tgz#39511771ed9602405fba9affff17eb4d2a3843ab"
@@ -3823,6 +3837,14 @@ react-redux@^5.0.0-beta.3:
lodash-es "^4.2.0"
loose-envify "^1.1.0"
+react-responsive:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/react-responsive/-/react-responsive-1.1.5.tgz#a7019a28817dcb601ef31d10d72f798a0d710a17"
+ dependencies:
+ hyphenate-style-name "^1.0.0"
+ matchmedia "^0.1.2"
+ object-assign "^4.0.1"
+
react-router@^2.8.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-2.8.1.tgz#73e9491f6ceb316d0f779829081863e378ee4ed7"