logo

mastofe

My custom branche(s) on git.pleroma.social/pleroma/mastofe
commit: 337462aa5e68014aa15788e4513e190b2e434d7e
parent: f820edb463109e313e836d8e2f210927a0eba7d1
Author: Eugen Rochko <eugen@zeonfederated.com>
Date:   Mon, 19 Sep 2016 23:25:59 +0200

Re-organizing components to be more modular, adding loading bars

Diffstat:

Mapp/assets/javascripts/components.js2+-
Dapp/assets/javascripts/components/components/compose_form.jsx58----------------------------------------------------------
Dapp/assets/javascripts/components/components/follow_form.jsx40----------------------------------------
Dapp/assets/javascripts/components/components/frontend.jsx50--------------------------------------------------
Dapp/assets/javascripts/components/components/navigation_bar.jsx30------------------------------
Dapp/assets/javascripts/components/components/reply_indicator.jsx41-----------------------------------------
Dapp/assets/javascripts/components/components/upload_button.jsx37-------------------------------------
Dapp/assets/javascripts/components/components/upload_form.jsx43-------------------------------------------
Dapp/assets/javascripts/components/containers/compose_form_container.jsx42------------------------------------------
Dapp/assets/javascripts/components/containers/follow_form_container.jsx24------------------------
Aapp/assets/javascripts/components/containers/mastodon.jsx74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dapp/assets/javascripts/components/containers/notifications_container.jsx25-------------------------
Dapp/assets/javascripts/components/containers/root.jsx74--------------------------------------------------------------------------
Dapp/assets/javascripts/components/containers/status_list_container.jsx29-----------------------------
Dapp/assets/javascripts/components/containers/upload_form_container.jsx25-------------------------
Mapp/assets/javascripts/components/features/status/index.jsx4++--
Rapp/assets/javascripts/components/components/character_counter.jsx -> app/assets/javascripts/components/features/ui/components/character_counter.jsx0
Rapp/assets/javascripts/components/components/column.jsx -> app/assets/javascripts/components/features/ui/components/column.jsx0
Rapp/assets/javascripts/components/components/column_header.jsx -> app/assets/javascripts/components/features/ui/components/column_header.jsx0
Rapp/assets/javascripts/components/components/columns_area.jsx -> app/assets/javascripts/components/features/ui/components/columns_area.jsx0
Aapp/assets/javascripts/components/features/ui/components/compose_form.jsx58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rapp/assets/javascripts/components/components/drawer.jsx -> app/assets/javascripts/components/features/ui/components/drawer.jsx0
Aapp/assets/javascripts/components/features/ui/components/follow_form.jsx40++++++++++++++++++++++++++++++++++++++++
Rapp/assets/javascripts/components/components/media_gallery.jsx -> app/assets/javascripts/components/features/ui/components/media_gallery.jsx0
Aapp/assets/javascripts/components/features/ui/components/navigation_bar.jsx30++++++++++++++++++++++++++++++
Aapp/assets/javascripts/components/features/ui/components/reply_indicator.jsx41+++++++++++++++++++++++++++++++++++++++++
Aapp/assets/javascripts/components/features/ui/components/upload_button.jsx37+++++++++++++++++++++++++++++++++++++
Aapp/assets/javascripts/components/features/ui/components/upload_form.jsx43+++++++++++++++++++++++++++++++++++++++++++
Aapp/assets/javascripts/components/features/ui/containers/compose_form_container.jsx42++++++++++++++++++++++++++++++++++++++++++
Aapp/assets/javascripts/components/features/ui/containers/follow_form_container.jsx24++++++++++++++++++++++++
Rapp/assets/javascripts/components/containers/navigation_container.jsx -> app/assets/javascripts/components/features/ui/containers/navigation_container.jsx0
Aapp/assets/javascripts/components/features/ui/containers/notifications_container.jsx25+++++++++++++++++++++++++
Aapp/assets/javascripts/components/features/ui/containers/status_list_container.jsx29+++++++++++++++++++++++++++++
Aapp/assets/javascripts/components/features/ui/containers/upload_form_container.jsx25+++++++++++++++++++++++++
Aapp/assets/javascripts/components/features/ui/index.jsx56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/assets/javascripts/components/reducers/compose.jsx36++++++++++++++++++++++++------------
Mapp/assets/javascripts/components/reducers/follow.jsx19++++++++++++-------
Mapp/assets/javascripts/components/reducers/index.jsx16+++++++++-------
Mapp/assets/javascripts/components/reducers/notifications.jsx2+-
Mapp/assets/javascripts/components/reducers/timelines.jsx38+++++++++++++++++++-------------------
Mapp/assets/javascripts/components/store/configureStore.jsx11+++++++----
Mapp/views/home/index.html.haml2+-
Mpackage.json1+
43 files changed, 601 insertions(+), 572 deletions(-)

diff --git a/app/assets/javascripts/components.js b/app/assets/javascripts/components.js @@ -6,4 +6,4 @@ window.ReactDOM = require('react-dom'); //= require_tree ./components -window.Root = require('./components/containers/root'); +window.Mastodon = require('./components/containers/mastodon'); diff --git a/app/assets/javascripts/components/components/compose_form.jsx b/app/assets/javascripts/components/components/compose_form.jsx @@ -1,58 +0,0 @@ -import CharacterCounter from './character_counter'; -import Button from './button'; -import PureRenderMixin from 'react-addons-pure-render-mixin'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ReplyIndicator from './reply_indicator'; -import UploadButton from './upload_button'; - -const ComposeForm = React.createClass({ - - propTypes: { - text: React.PropTypes.string.isRequired, - is_submitting: React.PropTypes.bool, - in_reply_to: ImmutablePropTypes.map, - onChange: React.PropTypes.func.isRequired, - onSubmit: React.PropTypes.func.isRequired, - onCancelReply: React.PropTypes.func.isRequired - }, - - mixins: [PureRenderMixin], - - handleChange (e) { - this.props.onChange(e.target.value); - }, - - handleKeyUp (e) { - if (e.keyCode === 13 && e.ctrlKey) { - this.props.onSubmit(); - } - }, - - handleSubmit () { - this.props.onSubmit(); - }, - - render () { - let replyArea = ''; - - if (this.props.in_reply_to) { - replyArea = <ReplyIndicator status={this.props.in_reply_to} onCancel={this.props.onCancelReply} />; - } - - return ( - <div style={{ padding: '10px' }}> - {replyArea} - - <textarea disabled={this.props.is_submitting} placeholder='What is on your mind?' value={this.props.text} onKeyUp={this.handleKeyUp} onChange={this.handleChange} className='compose-form__textarea' style={{ display: 'block', boxSizing: 'border-box', width: '100%', height: '100px', resize: 'none', border: 'none', color: '#282c37', padding: '10px', fontFamily: 'Roboto', fontSize: '14px', margin: '0' }} /> - - <div style={{ marginTop: '10px', overflow: 'hidden' }}> - <div style={{ float: 'right' }}><Button text='Publish' onClick={this.handleSubmit} disabled={this.props.is_submitting} /></div> - <div style={{ float: 'right', marginRight: '16px', lineHeight: '36px' }}><CharacterCounter text={this.props.text} /></div> - </div> - </div> - ); - } - -}); - -export default ComposeForm; diff --git a/app/assets/javascripts/components/components/follow_form.jsx b/app/assets/javascripts/components/components/follow_form.jsx @@ -1,40 +0,0 @@ -import IconButton from './icon_button'; -import PureRenderMixin from 'react-addons-pure-render-mixin'; - -const FollowForm = React.createClass({ - - propTypes: { - text: React.PropTypes.string.isRequired, - is_submitting: React.PropTypes.bool, - onChange: React.PropTypes.func.isRequired, - onSubmit: React.PropTypes.func.isRequired - }, - - mixins: [PureRenderMixin], - - handleChange (e) { - this.props.onChange(e.target.value); - }, - - handleKeyUp (e) { - if (e.keyCode === 13) { - this.props.onSubmit(); - } - }, - - handleSubmit () { - this.props.onSubmit(); - }, - - render () { - return ( - <div style={{ display: 'flex', lineHeight: '20px', padding: '10px', background: '#373b4a' }}> - <input type='text' disabled={this.props.is_submitting} placeholder='username@domain' value={this.props.text} onKeyUp={this.handleKeyUp} onChange={this.handleChange} className='follow-form__input' style={{ flex: '1 1 auto', boxSizing: 'border-box', display: 'block', border: 'none', padding: '10px', fontFamily: 'Roboto', color: '#282c37', fontSize: '14px', margin: '0' }} /> - <div style={{ padding: '10px', paddingRight: '0' }}><IconButton title='Follow' size={20} icon='user-plus' onClick={this.handleSubmit} disabled={this.props.is_submitting} /></div> - </div> - ); - } - -}); - -export default FollowForm; diff --git a/app/assets/javascripts/components/components/frontend.jsx b/app/assets/javascripts/components/components/frontend.jsx @@ -1,50 +0,0 @@ -import ColumnsArea from './columns_area'; -import Column from './column'; -import Drawer from './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'; - -const Frontend = React.createClass({ - - mixins: [PureRenderMixin], - - render () { - 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> - - <Column> - {this.props.children} - </Column> - </ColumnsArea> - - <NotificationsContainer /> - </div> - ); - } - -}); - -export default Frontend; diff --git a/app/assets/javascripts/components/components/navigation_bar.jsx b/app/assets/javascripts/components/components/navigation_bar.jsx @@ -1,30 +0,0 @@ -import PureRenderMixin from 'react-addons-pure-render-mixin'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Avatar from './avatar'; -import IconButton from './icon_button'; -import DisplayName from './display_name'; -import { Link } from 'react-router'; - -const NavigationBar = React.createClass({ - propTypes: { - account: ImmutablePropTypes.map.isRequired - }, - - mixins: [PureRenderMixin], - - render () { - return ( - <div style={{ padding: '10px', display: 'flex', cursor: 'default' }}> - <Link to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} size={40} /></Link> - - <div style={{ flex: '1 1 auto', marginLeft: '8px' }}> - <strong style={{ fontWeight: '500', display: 'block' }}>{this.props.account.get('acct')}</strong> - <Link to='/settings' style={{ color: '#9baec8', textDecoration: 'none' }}>Settings <i className='fa fa fa-cog' /></Link> - </div> - </div> - ); - } - -}); - -export default NavigationBar; diff --git a/app/assets/javascripts/components/components/reply_indicator.jsx b/app/assets/javascripts/components/components/reply_indicator.jsx @@ -1,41 +0,0 @@ -import PureRenderMixin from 'react-addons-pure-render-mixin'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Avatar from './avatar'; -import IconButton from './icon_button'; -import DisplayName from './display_name'; - -const ReplyIndicator = React.createClass({ - - propTypes: { - status: ImmutablePropTypes.map.isRequired, - onCancel: React.PropTypes.func.isRequired - }, - - mixins: [PureRenderMixin], - - handleClick () { - this.props.onCancel(); - }, - - render () { - let content = { __html: this.props.status.get('content') }; - - return ( - <div style={{ background: '#9baec8', padding: '10px' }}> - <div style={{ overflow: 'hidden', marginBottom: '5px' }}> - <div style={{ float: 'right', lineHeight: '24px' }}><IconButton title='Cancel' icon='times' onClick={this.handleClick} /></div> - - <a href={this.props.status.getIn(['account', 'url'])} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', color: '#282c37', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}> - <div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={this.props.status.getIn(['account', 'avatar'])} /></div> - <DisplayName account={this.props.status.get('account')} /> - </a> - </div> - - <div className='reply-indicator__content' dangerouslySetInnerHTML={content} /> - </div> - ); - } - -}); - -export default ReplyIndicator; diff --git a/app/assets/javascripts/components/components/upload_button.jsx b/app/assets/javascripts/components/components/upload_button.jsx @@ -1,37 +0,0 @@ -import PureRenderMixin from 'react-addons-pure-render-mixin'; -import Button from './button'; - -const UploadButton = React.createClass({ - - propTypes: { - disabled: React.PropTypes.bool, - onSelectFile: React.PropTypes.func.isRequired - }, - - mixins: [PureRenderMixin], - - handleChange (e) { - if (e.target.files.length > 0) { - this.props.onSelectFile(e.target.files); - } - }, - - handleClick () { - this.refs.fileElement.click(); - }, - - render () { - return ( - <div> - <Button disabled={this.props.disabled} onClick={this.handleClick} block={true}> - <i className='fa fa-fw fa-photo' /> Add images - </Button> - - <input ref='fileElement' type='file' multiple={false} onChange={this.handleChange} disabled={this.props.disabled} style={{ display: 'none' }} /> - </div> - ); - } - -}); - -export default UploadButton; diff --git a/app/assets/javascripts/components/components/upload_form.jsx b/app/assets/javascripts/components/components/upload_form.jsx @@ -1,43 +0,0 @@ -import PureRenderMixin from 'react-addons-pure-render-mixin'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import UploadButton from './upload_button'; -import IconButton from './icon_button'; - -const UploadForm = React.createClass({ - - propTypes: { - media: ImmutablePropTypes.list.isRequired, - is_uploading: React.PropTypes.bool, - onSelectFile: React.PropTypes.func.isRequired, - onRemoveFile: React.PropTypes.func.isRequired - }, - - mixins: [PureRenderMixin], - - render () { - let uploads = this.props.media.map(function (attachment) { - return ( - <div key={attachment.get('id')} style={{ borderRadius: '4px', marginBottom: '10px' }} className='transparent-background'> - <div style={{ width: '100%', height: '100px', borderRadius: '4px', background: `url(${attachment.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }}> - <IconButton icon='times' title='Undo' size={36} onClick={() => this.props.onRemoveFile(attachment.get('id'))} /> - </div> - </div> - ); - }.bind(this)); - - const noMoreAllowed = (this.props.media.some(m => m.get('type') === 'video')) || (this.props.media.size > 3); - - return ( - <div style={{ marginBottom: '20px', padding: '10px', paddingTop: '0' }}> - <UploadButton onSelectFile={this.props.onSelectFile} disabled={this.props.is_uploading || noMoreAllowed } /> - - <div style={{ marginTop: '10px', overflow: 'hidden' }}> - {uploads} - </div> - </div> - ); - } - -}); - -export default UploadForm; diff --git a/app/assets/javascripts/components/containers/compose_form_container.jsx b/app/assets/javascripts/components/containers/compose_form_container.jsx @@ -1,42 +0,0 @@ -import { connect } from 'react-redux'; -import ComposeForm from '../components/compose_form'; -import { changeCompose, submitCompose, cancelReplyCompose } from '../actions/compose'; - -function selectStatus(state) { - let statusId = state.getIn(['compose', 'in_reply_to'], null); - - if (statusId === null) { - return null; - } - - let status = state.getIn(['timelines', 'statuses', statusId]); - status = status.set('account', state.getIn(['timelines', 'accounts', status.get('account')])); - - return status; -}; - -const mapStateToProps = function (state, props) { - return { - text: state.getIn(['compose', 'text']), - is_submitting: state.getIn(['compose', 'is_submitting']), - in_reply_to: selectStatus(state) - }; -}; - -const mapDispatchToProps = function (dispatch) { - return { - onChange: function (text) { - dispatch(changeCompose(text)); - }, - - onSubmit: function () { - dispatch(submitCompose()); - }, - - onCancelReply: function () { - dispatch(cancelReplyCompose()); - } - } -}; - -export default connect(mapStateToProps, mapDispatchToProps)(ComposeForm); diff --git a/app/assets/javascripts/components/containers/follow_form_container.jsx b/app/assets/javascripts/components/containers/follow_form_container.jsx @@ -1,24 +0,0 @@ -import { connect } from 'react-redux'; -import FollowForm from '../components/follow_form'; -import { changeFollow, submitFollow } from '../actions/follow'; - -const mapStateToProps = function (state, props) { - return { - text: state.getIn(['follow', 'text']), - is_submitting: state.getIn(['follow', 'is_submitting']) - }; -}; - -const mapDispatchToProps = function (dispatch) { - return { - onChange: function (text) { - dispatch(changeFollow(text)); - }, - - onSubmit: function () { - dispatch(submitFollow()); - } - } -}; - -export default connect(mapStateToProps, mapDispatchToProps)(FollowForm); diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx @@ -0,0 +1,74 @@ +import { Provider } from 'react-redux'; +import configureStore from '../store/configureStore'; +import { setTimeline, updateTimeline, deleteFromTimelines, refreshTimeline } from '../actions/timelines'; +import { setAccessToken } from '../actions/meta'; +import { setAccountSelf } from '../actions/accounts'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import { Router, Route, hashHistory } from 'react-router'; +import Account from '../features/account'; +import Settings from '../features/settings'; +import Status from '../features/status'; +import Subscriptions from '../features/subscriptions'; +import UI from '../features/ui'; + +const store = configureStore(); + +const Mastodon = React.createClass({ + + propTypes: { + token: React.PropTypes.string.isRequired, + timelines: React.PropTypes.object, + account: React.PropTypes.string + }, + + mixins: [PureRenderMixin], + + componentWillMount() { + store.dispatch(setAccessToken(this.props.token)); + store.dispatch(setAccountSelf(JSON.parse(this.props.account))); + + for (var timelineType in this.props.timelines) { + if (this.props.timelines.hasOwnProperty(timelineType)) { + store.dispatch(setTimeline(timelineType, JSON.parse(this.props.timelines[timelineType]))); + } + } + + if (typeof App !== 'undefined') { + App.timeline = App.cable.subscriptions.create("TimelineChannel", { + connected: function() {}, + + disconnected: function() {}, + + received: function(data) { + switch(data.type) { + case 'update': + return store.dispatch(updateTimeline(data.timeline, JSON.parse(data.message))); + case 'delete': + return store.dispatch(deleteFromTimelines(data.id)); + case 'merge': + case 'unmerge': + return store.dispatch(refreshTimeline('home')); + } + } + }); + } + }, + + render () { + return ( + <Provider store={store}> + <Router history={hashHistory}> + <Route path='/' component={UI}> + <Route path='/settings' component={Settings} /> + <Route path='/subscriptions' component={Subscriptions} /> + <Route path='/statuses/:statusId' component={Status} /> + <Route path='/accounts/:accountId' component={Account} /> + </Route> + </Router> + </Provider> + ); + } + +}); + +export default Mastodon; diff --git a/app/assets/javascripts/components/containers/notifications_container.jsx b/app/assets/javascripts/components/containers/notifications_container.jsx @@ -1,25 +0,0 @@ -import { connect } from 'react-redux'; -import { NotificationStack } from 'react-notification'; -import { dismissNotification } from '../actions/notifications'; - -const mapStateToProps = (state, props) => { - return { - notifications: state.get('notifications').map((item, i) => ({ - message: item.get('message'), - title: item.get('title'), - key: i, - action: 'Dismiss', - dismissAfter: 5000 - })).toJS() - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - onDismiss: notifiction => { - dispatch(dismissNotification(notifiction)); - } - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(NotificationStack); diff --git a/app/assets/javascripts/components/containers/root.jsx b/app/assets/javascripts/components/containers/root.jsx @@ -1,74 +0,0 @@ -import { Provider } from 'react-redux'; -import configureStore from '../store/configureStore'; -import Frontend from '../components/frontend'; -import { setTimeline, updateTimeline, deleteFromTimelines, refreshTimeline } from '../actions/timelines'; -import { setAccessToken } from '../actions/meta'; -import { setAccountSelf } from '../actions/accounts'; -import PureRenderMixin from 'react-addons-pure-render-mixin'; -import { Router, Route, hashHistory } from 'react-router'; -import Account from '../features/account'; -import Settings from '../features/settings'; -import Status from '../features/status'; -import Subscriptions from '../features/subscriptions'; - -const store = configureStore(); - -const Root = React.createClass({ - - propTypes: { - token: React.PropTypes.string.isRequired, - timelines: React.PropTypes.object, - account: React.PropTypes.string - }, - - mixins: [PureRenderMixin], - - componentWillMount() { - store.dispatch(setAccessToken(this.props.token)); - store.dispatch(setAccountSelf(JSON.parse(this.props.account))); - - for (var timelineType in this.props.timelines) { - if (this.props.timelines.hasOwnProperty(timelineType)) { - store.dispatch(setTimeline(timelineType, JSON.parse(this.props.timelines[timelineType]))); - } - } - - if (typeof App !== 'undefined') { - App.timeline = App.cable.subscriptions.create("TimelineChannel", { - connected: function() {}, - - disconnected: function() {}, - - received: function(data) { - switch(data.type) { - case 'update': - return store.dispatch(updateTimeline(data.timeline, JSON.parse(data.message))); - case 'delete': - return store.dispatch(deleteFromTimelines(data.id)); - case 'merge': - case 'unmerge': - return store.dispatch(refreshTimeline('home')); - } - } - }); - } - }, - - render () { - return ( - <Provider store={store}> - <Router history={hashHistory}> - <Route path='/' component={Frontend}> - <Route path='/settings' component={Settings} /> - <Route path='/subscriptions' component={Subscriptions} /> - <Route path='/statuses/:statusId' component={Status} /> - <Route path='/accounts/:accountId' component={Account} /> - </Route> - </Router> - </Provider> - ); - } - -}); - -export default Root; diff --git a/app/assets/javascripts/components/containers/status_list_container.jsx b/app/assets/javascripts/components/containers/status_list_container.jsx @@ -1,29 +0,0 @@ -import { connect } from 'react-redux'; -import StatusList from '../components/status_list'; -import { replyCompose } from '../actions/compose'; -import { reblog, favourite } from '../actions/interactions'; -import { selectStatus } from '../reducers/timelines'; - -const mapStateToProps = function (state, props) { - return { - statuses: state.getIn(['timelines', props.type]).map(id => selectStatus(state, id)) - }; -}; - -const mapDispatchToProps = function (dispatch) { - return { - onReply: function (status) { - dispatch(replyCompose(status)); - }, - - onFavourite: function (status) { - dispatch(favourite(status)); - }, - - onReblog: function (status) { - dispatch(reblog(status)); - } - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(StatusList); diff --git a/app/assets/javascripts/components/containers/upload_form_container.jsx b/app/assets/javascripts/components/containers/upload_form_container.jsx @@ -1,25 +0,0 @@ -import { connect } from 'react-redux'; -import UploadForm from '../components/upload_form'; -import { uploadCompose, undoUploadCompose } from '../actions/compose'; - -const mapStateToProps = function (state, props) { - return { - media: state.getIn(['compose', 'media_attachments']), - progress: state.getIn(['compose', 'progress']), - is_uploading: state.getIn(['compose', 'is_uploading']) - }; -}; - -const mapDispatchToProps = function (dispatch) { - return { - onSelectFile: function (files) { - dispatch(uploadCompose(files)); - }, - - onRemoveFile: function (media_id) { - dispatch(undoUploadCompose(media_id)); - } - } -}; - -export default connect(mapStateToProps, mapDispatchToProps)(UploadForm); diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx @@ -31,12 +31,12 @@ const Status = React.createClass({ mixins: [PureRenderMixin], componentWillMount () { - this.props.dispatch(fetchStatus(this.props.params.statusId)); + this.props.dispatch(fetchStatus(Number(this.props.params.statusId))); }, componentWillReceiveProps (nextProps) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { - this.props.dispatch(fetchStatus(nextProps.params.statusId)); + this.props.dispatch(fetchStatus(Number(nextProps.params.statusId))); } }, diff --git a/app/assets/javascripts/components/components/character_counter.jsx b/app/assets/javascripts/components/features/ui/components/character_counter.jsx diff --git a/app/assets/javascripts/components/components/column.jsx b/app/assets/javascripts/components/features/ui/components/column.jsx diff --git a/app/assets/javascripts/components/components/column_header.jsx b/app/assets/javascripts/components/features/ui/components/column_header.jsx diff --git a/app/assets/javascripts/components/components/columns_area.jsx b/app/assets/javascripts/components/features/ui/components/columns_area.jsx diff --git a/app/assets/javascripts/components/features/ui/components/compose_form.jsx b/app/assets/javascripts/components/features/ui/components/compose_form.jsx @@ -0,0 +1,58 @@ +import CharacterCounter from './character_counter'; +import Button from '../../../components/button'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ReplyIndicator from './reply_indicator'; +import UploadButton from './upload_button'; + +const ComposeForm = React.createClass({ + + propTypes: { + text: React.PropTypes.string.isRequired, + is_submitting: React.PropTypes.bool, + in_reply_to: ImmutablePropTypes.map, + onChange: React.PropTypes.func.isRequired, + onSubmit: React.PropTypes.func.isRequired, + onCancelReply: React.PropTypes.func.isRequired + }, + + mixins: [PureRenderMixin], + + handleChange (e) { + this.props.onChange(e.target.value); + }, + + handleKeyUp (e) { + if (e.keyCode === 13 && e.ctrlKey) { + this.props.onSubmit(); + } + }, + + handleSubmit () { + this.props.onSubmit(); + }, + + render () { + let replyArea = ''; + + if (this.props.in_reply_to) { + replyArea = <ReplyIndicator status={this.props.in_reply_to} onCancel={this.props.onCancelReply} />; + } + + return ( + <div style={{ padding: '10px' }}> + {replyArea} + + <textarea disabled={this.props.is_submitting} placeholder='What is on your mind?' value={this.props.text} onKeyUp={this.handleKeyUp} onChange={this.handleChange} className='compose-form__textarea' style={{ display: 'block', boxSizing: 'border-box', width: '100%', height: '100px', resize: 'none', border: 'none', color: '#282c37', padding: '10px', fontFamily: 'Roboto', fontSize: '14px', margin: '0' }} /> + + <div style={{ marginTop: '10px', overflow: 'hidden' }}> + <div style={{ float: 'right' }}><Button text='Publish' onClick={this.handleSubmit} disabled={this.props.is_submitting} /></div> + <div style={{ float: 'right', marginRight: '16px', lineHeight: '36px' }}><CharacterCounter text={this.props.text} /></div> + </div> + </div> + ); + } + +}); + +export default ComposeForm; diff --git a/app/assets/javascripts/components/components/drawer.jsx b/app/assets/javascripts/components/features/ui/components/drawer.jsx diff --git a/app/assets/javascripts/components/features/ui/components/follow_form.jsx b/app/assets/javascripts/components/features/ui/components/follow_form.jsx @@ -0,0 +1,40 @@ +import IconButton from '../../../components/icon_button'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; + +const FollowForm = React.createClass({ + + propTypes: { + text: React.PropTypes.string.isRequired, + is_submitting: React.PropTypes.bool, + onChange: React.PropTypes.func.isRequired, + onSubmit: React.PropTypes.func.isRequired + }, + + mixins: [PureRenderMixin], + + handleChange (e) { + this.props.onChange(e.target.value); + }, + + handleKeyUp (e) { + if (e.keyCode === 13) { + this.props.onSubmit(); + } + }, + + handleSubmit () { + this.props.onSubmit(); + }, + + render () { + return ( + <div style={{ display: 'flex', lineHeight: '20px', padding: '10px', background: '#373b4a' }}> + <input type='text' disabled={this.props.is_submitting} placeholder='username@domain' value={this.props.text} onKeyUp={this.handleKeyUp} onChange={this.handleChange} className='follow-form__input' style={{ flex: '1 1 auto', boxSizing: 'border-box', display: 'block', border: 'none', padding: '10px', fontFamily: 'Roboto', color: '#282c37', fontSize: '14px', margin: '0' }} /> + <div style={{ padding: '10px', paddingRight: '0' }}><IconButton title='Follow' size={20} icon='user-plus' onClick={this.handleSubmit} disabled={this.props.is_submitting} /></div> + </div> + ); + } + +}); + +export default FollowForm; diff --git a/app/assets/javascripts/components/components/media_gallery.jsx b/app/assets/javascripts/components/features/ui/components/media_gallery.jsx diff --git a/app/assets/javascripts/components/features/ui/components/navigation_bar.jsx b/app/assets/javascripts/components/features/ui/components/navigation_bar.jsx @@ -0,0 +1,30 @@ +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Avatar from '../../../components/avatar'; +import IconButton from '../../../components/icon_button'; +import DisplayName from '../../../components/display_name'; +import { Link } from 'react-router'; + +const NavigationBar = React.createClass({ + propTypes: { + account: ImmutablePropTypes.map.isRequired + }, + + mixins: [PureRenderMixin], + + render () { + return ( + <div style={{ padding: '10px', display: 'flex', cursor: 'default' }}> + <Link to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} size={40} /></Link> + + <div style={{ flex: '1 1 auto', marginLeft: '8px' }}> + <strong style={{ fontWeight: '500', display: 'block' }}>{this.props.account.get('acct')}</strong> + <Link to='/settings' style={{ color: '#9baec8', textDecoration: 'none' }}>Settings <i className='fa fa fa-cog' /></Link> + </div> + </div> + ); + } + +}); + +export default NavigationBar; diff --git a/app/assets/javascripts/components/features/ui/components/reply_indicator.jsx b/app/assets/javascripts/components/features/ui/components/reply_indicator.jsx @@ -0,0 +1,41 @@ +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Avatar from '../../../components/avatar'; +import IconButton from '../../../components/icon_button'; +import DisplayName from '../../../components/display_name'; + +const ReplyIndicator = React.createClass({ + + propTypes: { + status: ImmutablePropTypes.map.isRequired, + onCancel: React.PropTypes.func.isRequired + }, + + mixins: [PureRenderMixin], + + handleClick () { + this.props.onCancel(); + }, + + render () { + let content = { __html: this.props.status.get('content') }; + + return ( + <div style={{ background: '#9baec8', padding: '10px' }}> + <div style={{ overflow: 'hidden', marginBottom: '5px' }}> + <div style={{ float: 'right', lineHeight: '24px' }}><IconButton title='Cancel' icon='times' onClick={this.handleClick} /></div> + + <a href={this.props.status.getIn(['account', 'url'])} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', color: '#282c37', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}> + <div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={this.props.status.getIn(['account', 'avatar'])} /></div> + <DisplayName account={this.props.status.get('account')} /> + </a> + </div> + + <div className='reply-indicator__content' dangerouslySetInnerHTML={content} /> + </div> + ); + } + +}); + +export default ReplyIndicator; diff --git a/app/assets/javascripts/components/features/ui/components/upload_button.jsx b/app/assets/javascripts/components/features/ui/components/upload_button.jsx @@ -0,0 +1,37 @@ +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import Button from '../../../components/button'; + +const UploadButton = React.createClass({ + + propTypes: { + disabled: React.PropTypes.bool, + onSelectFile: React.PropTypes.func.isRequired + }, + + mixins: [PureRenderMixin], + + handleChange (e) { + if (e.target.files.length > 0) { + this.props.onSelectFile(e.target.files); + } + }, + + handleClick () { + this.refs.fileElement.click(); + }, + + render () { + return ( + <div> + <Button disabled={this.props.disabled} onClick={this.handleClick} block={true}> + <i className='fa fa-fw fa-photo' /> Add images + </Button> + + <input ref='fileElement' type='file' multiple={false} onChange={this.handleChange} disabled={this.props.disabled} style={{ display: 'none' }} /> + </div> + ); + } + +}); + +export default UploadButton; diff --git a/app/assets/javascripts/components/features/ui/components/upload_form.jsx b/app/assets/javascripts/components/features/ui/components/upload_form.jsx @@ -0,0 +1,43 @@ +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import UploadButton from './upload_button'; +import IconButton from '../../../components/icon_button'; + +const UploadForm = React.createClass({ + + propTypes: { + media: ImmutablePropTypes.list.isRequired, + is_uploading: React.PropTypes.bool, + onSelectFile: React.PropTypes.func.isRequired, + onRemoveFile: React.PropTypes.func.isRequired + }, + + mixins: [PureRenderMixin], + + render () { + let uploads = this.props.media.map(function (attachment) { + return ( + <div key={attachment.get('id')} style={{ borderRadius: '4px', marginBottom: '10px' }} className='transparent-background'> + <div style={{ width: '100%', height: '100px', borderRadius: '4px', background: `url(${attachment.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }}> + <IconButton icon='times' title='Undo' size={36} onClick={() => this.props.onRemoveFile(attachment.get('id'))} /> + </div> + </div> + ); + }.bind(this)); + + const noMoreAllowed = (this.props.media.some(m => m.get('type') === 'video')) || (this.props.media.size > 3); + + return ( + <div style={{ marginBottom: '20px', padding: '10px', paddingTop: '0' }}> + <UploadButton onSelectFile={this.props.onSelectFile} disabled={this.props.is_uploading || noMoreAllowed } /> + + <div style={{ marginTop: '10px', overflow: 'hidden' }}> + {uploads} + </div> + </div> + ); + } + +}); + +export default UploadForm; diff --git a/app/assets/javascripts/components/features/ui/containers/compose_form_container.jsx b/app/assets/javascripts/components/features/ui/containers/compose_form_container.jsx @@ -0,0 +1,42 @@ +import { connect } from 'react-redux'; +import ComposeForm from '../components/compose_form'; +import { changeCompose, submitCompose, cancelReplyCompose } from '../../../actions/compose'; + +function selectStatus(state) { + let statusId = state.getIn(['compose', 'in_reply_to'], null); + + if (statusId === null) { + return null; + } + + let status = state.getIn(['timelines', 'statuses', statusId]); + status = status.set('account', state.getIn(['timelines', 'accounts', status.get('account')])); + + return status; +}; + +const mapStateToProps = function (state, props) { + return { + text: state.getIn(['compose', 'text']), + is_submitting: state.getIn(['compose', 'is_submitting']), + in_reply_to: selectStatus(state) + }; +}; + +const mapDispatchToProps = function (dispatch) { + return { + onChange: function (text) { + dispatch(changeCompose(text)); + }, + + onSubmit: function () { + dispatch(submitCompose()); + }, + + onCancelReply: function () { + dispatch(cancelReplyCompose()); + } + } +}; + +export default connect(mapStateToProps, mapDispatchToProps)(ComposeForm); diff --git a/app/assets/javascripts/components/features/ui/containers/follow_form_container.jsx b/app/assets/javascripts/components/features/ui/containers/follow_form_container.jsx @@ -0,0 +1,24 @@ +import { connect } from 'react-redux'; +import FollowForm from '../components/follow_form'; +import { changeFollow, submitFollow } from '../../../actions/follow'; + +const mapStateToProps = function (state, props) { + return { + text: state.getIn(['follow', 'text']), + is_submitting: state.getIn(['follow', 'is_submitting']) + }; +}; + +const mapDispatchToProps = function (dispatch) { + return { + onChange: function (text) { + dispatch(changeFollow(text)); + }, + + onSubmit: function () { + dispatch(submitFollow()); + } + } +}; + +export default connect(mapStateToProps, mapDispatchToProps)(FollowForm); diff --git a/app/assets/javascripts/components/containers/navigation_container.jsx b/app/assets/javascripts/components/features/ui/containers/navigation_container.jsx diff --git a/app/assets/javascripts/components/features/ui/containers/notifications_container.jsx b/app/assets/javascripts/components/features/ui/containers/notifications_container.jsx @@ -0,0 +1,25 @@ +import { connect } from 'react-redux'; +import { NotificationStack } from 'react-notification'; +import { dismissNotification } from '../../../actions/notifications'; + +const mapStateToProps = (state, props) => { + return { + notifications: state.get('notifications').map((item, i) => ({ + message: item.get('message'), + title: item.get('title'), + key: i, + action: 'Dismiss', + dismissAfter: 5000 + })).toJS() + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + onDismiss: notifiction => { + dispatch(dismissNotification(notifiction)); + } + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(NotificationStack); diff --git a/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx b/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx @@ -0,0 +1,29 @@ +import { connect } from 'react-redux'; +import StatusList from '../../../components/status_list'; +import { replyCompose } from '../../../actions/compose'; +import { reblog, favourite } from '../../../actions/interactions'; +import { selectStatus } from '../../../reducers/timelines'; + +const mapStateToProps = function (state, props) { + return { + statuses: state.getIn(['timelines', props.type]).map(id => selectStatus(state, id)) + }; +}; + +const mapDispatchToProps = function (dispatch) { + return { + onReply: function (status) { + dispatch(replyCompose(status)); + }, + + onFavourite: function (status) { + dispatch(favourite(status)); + }, + + onReblog: function (status) { + dispatch(reblog(status)); + } + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(StatusList); diff --git a/app/assets/javascripts/components/features/ui/containers/upload_form_container.jsx b/app/assets/javascripts/components/features/ui/containers/upload_form_container.jsx @@ -0,0 +1,25 @@ +import { connect } from 'react-redux'; +import UploadForm from '../components/upload_form'; +import { uploadCompose, undoUploadCompose } from '../../../actions/compose'; + +const mapStateToProps = function (state, props) { + return { + media: state.getIn(['compose', 'media_attachments']), + progress: state.getIn(['compose', 'progress']), + is_uploading: state.getIn(['compose', 'is_uploading']) + }; +}; + +const mapDispatchToProps = function (dispatch) { + return { + onSelectFile: function (files) { + dispatch(uploadCompose(files)); + }, + + onRemoveFile: function (media_id) { + dispatch(undoUploadCompose(media_id)); + } + } +}; + +export default connect(mapStateToProps, mapDispatchToProps)(UploadForm); diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx @@ -0,0 +1,56 @@ +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 LoadingBar from 'react-redux-loading-bar'; + +const UI = React.createClass({ + + propTypes: { + router: React.PropTypes.object + }, + + mixins: [PureRenderMixin], + + render () { + 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> + + <Column> + {this.props.children} + </Column> + </ColumnsArea> + + <NotificationsContainer /> + <LoadingBar style={{ backgroundColor: '#2b90d9', left: '0', top: '0' }} /> + </div> + ); + } + +}); + +export default UI; diff --git a/app/assets/javascripts/components/reducers/compose.jsx b/app/assets/javascripts/components/reducers/compose.jsx @@ -1,4 +1,16 @@ -import * as constants from '../actions/compose'; +import { + COMPOSE_CHANGE, + COMPOSE_REPLY, + COMPOSE_REPLY_CANCEL, + COMPOSE_SUBMIT_REQUEST, + COMPOSE_SUBMIT_SUCCESS, + COMPOSE_SUBMIT_FAIL, + COMPOSE_UPLOAD_REQUEST, + COMPOSE_UPLOAD_SUCCESS, + COMPOSE_UPLOAD_FAIL, + COMPOSE_UPLOAD_UNDO, + COMPOSE_UPLOAD_PROGRESS +} from '../actions/compose'; import { TIMELINE_DELETE } from '../actions/timelines'; import Immutable from 'immutable'; @@ -13,41 +25,41 @@ const initialState = Immutable.Map({ export default function compose(state = initialState, action) { switch(action.type) { - case constants.COMPOSE_CHANGE: + case COMPOSE_CHANGE: return state.set('text', action.text); - case constants.COMPOSE_REPLY: + case COMPOSE_REPLY: return state.withMutations(map => { map.set('in_reply_to', action.status.get('id')); map.set('text', `@${action.status.getIn(['account', 'acct'])} `); }); - case constants.COMPOSE_REPLY_CANCEL: + case COMPOSE_REPLY_CANCEL: return state.withMutations(map => { map.set('in_reply_to', null); map.set('text', ''); }); - case constants.COMPOSE_SUBMIT_REQUEST: + case COMPOSE_SUBMIT_REQUEST: return state.set('is_submitting', true); - case constants.COMPOSE_SUBMIT_SUCCESS: + case COMPOSE_SUBMIT_SUCCESS: return state.withMutations(map => { map.set('text', ''); map.set('is_submitting', false); map.set('in_reply_to', null); map.update('media_attachments', list => list.clear()); }); - case constants.COMPOSE_SUBMIT_FAIL: + case COMPOSE_SUBMIT_FAIL: return state.set('is_submitting', false); - case constants.COMPOSE_UPLOAD_REQUEST: + case COMPOSE_UPLOAD_REQUEST: return state.set('is_uploading', true); - case constants.COMPOSE_UPLOAD_SUCCESS: + case COMPOSE_UPLOAD_SUCCESS: return state.withMutations(map => { map.update('media_attachments', list => list.push(Immutable.fromJS(action.media))); map.set('is_uploading', false); }); - case constants.COMPOSE_UPLOAD_FAIL: + case COMPOSE_UPLOAD_FAIL: return state.set('is_uploading', false); - case constants.COMPOSE_UPLOAD_UNDO: + case COMPOSE_UPLOAD_UNDO: return state.update('media_attachments', list => list.filterNot(item => item.get('id') === action.media_id)); - case constants.COMPOSE_UPLOAD_PROGRESS: + case COMPOSE_UPLOAD_PROGRESS: return state.set('progress', Math.round((action.loaded / action.total) * 100)); case TIMELINE_DELETE: if (action.id === state.get('in_reply_to')) { diff --git a/app/assets/javascripts/components/reducers/follow.jsx b/app/assets/javascripts/components/reducers/follow.jsx @@ -1,22 +1,27 @@ -import * as constants from '../actions/follow'; -import Immutable from 'immutable'; +import { + FOLLOW_CHANGE, + FOLLOW_SUBMIT_REQUEST, + FOLLOW_SUBMIT_SUCCESS, + FOLLOW_SUBMIT_FAIL +} from '../actions/follow'; +import Immutable from 'immutable'; const initialState = Immutable.Map({ text: '', is_submitting: false }); -export default function compose(state = initialState, action) { +export default function follow(state = initialState, action) { switch(action.type) { - case constants.FOLLOW_CHANGE: + case FOLLOW_CHANGE: return state.set('text', action.text); - case constants.FOLLOW_SUBMIT_REQUEST: + case FOLLOW_SUBMIT_REQUEST: return state.set('is_submitting', true); - case constants.FOLLOW_SUBMIT_SUCCESS: + case FOLLOW_SUBMIT_SUCCESS: return state.withMutations(map => { map.set('text', '').set('is_submitting', false); }); - case constants.FOLLOW_SUBMIT_FAIL: + case FOLLOW_SUBMIT_FAIL: return state.set('is_submitting', false); default: return state; diff --git a/app/assets/javascripts/components/reducers/index.jsx b/app/assets/javascripts/components/reducers/index.jsx @@ -1,14 +1,16 @@ -import { combineReducers } from 'redux-immutable'; -import timelines from './timelines'; -import meta from './meta'; -import compose from './compose'; -import follow from './follow'; -import notifications from './notifications'; +import { combineReducers } from 'redux-immutable'; +import timelines from './timelines'; +import meta from './meta'; +import compose from './compose'; +import follow from './follow'; +import notifications from './notifications'; +import { loadingBarReducer } from 'react-redux-loading-bar'; export default combineReducers({ timelines, meta, compose, follow, - notifications + notifications, + loadingBar: loadingBarReducer, }); diff --git a/app/assets/javascripts/components/reducers/notifications.jsx b/app/assets/javascripts/components/reducers/notifications.jsx @@ -24,7 +24,7 @@ function notificationFromError(state, error) { return state.push(n); }; -export default function meta(state = initialState, action) { +export default function notifications(state = initialState, action) { switch(action.type) { case COMPOSE_SUBMIT_FAIL: case COMPOSE_UPLOAD_FAIL: diff --git a/app/assets/javascripts/components/reducers/timelines.jsx b/app/assets/javascripts/components/reducers/timelines.jsx @@ -45,7 +45,7 @@ export function selectStatus(state, id) { return status; }; -function statusToMaps(state, status) { +function normalizeStatus(state, status) { // Separate account let account = status.get('account'); status = status.set('account', account.get('id')); @@ -55,7 +55,7 @@ function statusToMaps(state, status) { if (reblog !== null) { status = status.set('reblog', reblog.get('id')); - state = statusToMaps(state, reblog); + state = normalizeStatus(state, reblog); } // Replies @@ -80,26 +80,26 @@ function statusToMaps(state, status) { }); }; -function timelineToMaps(state, timeline, statuses) { +function normalizeTimeline(state, timeline, statuses) { statuses.forEach((status, i) => { - state = statusToMaps(state, status); + state = normalizeStatus(state, status); state = state.setIn([timeline, i], status.get('id')); }); return state; }; -function accountTimelineToMaps(state, accountId, statuses) { +function normalizeAccountTimeline(state, accountId, statuses) { statuses.forEach((status, i) => { - state = statusToMaps(state, status); + state = normalizeStatus(state, status); state = state.updateIn(['accounts_timelines', accountId], Immutable.List(), list => list.set(i, status.get('id'))); }); return state; }; -function updateTimelineWithMaps(state, timeline, status) { - state = statusToMaps(state, status); +function updateTimeline(state, timeline, status) { + state = normalizeStatus(state, status); state = state.update(timeline, list => list.unshift(status.get('id'))); state = state.updateIn(['accounts_timelines', status.getIn(['account', 'id'])], Immutable.List(), list => list.unshift(status.get('id'))); @@ -114,20 +114,20 @@ function deleteStatus(state, id) { return state.deleteIn(['statuses', id]); }; -function accountToMaps(state, account) { +function normalizeAccount(state, account) { return state.setIn(['accounts', account.get('id')], account); }; -function contextToMaps(state, status, ancestors, descendants) { - state = statusToMaps(state, status); +function normalizeContext(state, status, ancestors, descendants) { + state = normalizeStatus(state, status); let ancestorsIds = ancestors.map(ancestor => { - state = statusToMaps(state, ancestor); + state = normalizeStatus(state, ancestor); return ancestor.get('id'); }).toOrderedSet(); let descendantsIds = descendants.map(descendant => { - state = statusToMaps(state, descendant); + state = normalizeStatus(state, descendant); return descendant.get('id'); }).toOrderedSet(); @@ -140,14 +140,14 @@ function contextToMaps(state, status, ancestors, descendants) { export default function timelines(state = initialState, action) { switch(action.type) { case TIMELINE_SET: - return timelineToMaps(state, action.timeline, Immutable.fromJS(action.statuses)); + return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses)); case TIMELINE_UPDATE: - return updateTimelineWithMaps(state, action.timeline, Immutable.fromJS(action.status)); + return updateTimeline(state, action.timeline, Immutable.fromJS(action.status)); case TIMELINE_DELETE: return deleteStatus(state, action.id); case REBLOG_SUCCESS: case FAVOURITE_SUCCESS: - return statusToMaps(state, Immutable.fromJS(action.response)); + return normalizeStatus(state, Immutable.fromJS(action.response)); case ACCOUNT_SET_SELF: return state.withMutations(map => { map.setIn(['accounts', action.account.id], Immutable.fromJS(action.account)); @@ -157,11 +157,11 @@ export default function timelines(state = initialState, action) { case FOLLOW_SUBMIT_SUCCESS: case ACCOUNT_FOLLOW_SUCCESS: case ACCOUNT_UNFOLLOW_SUCCESS: - return accountToMaps(state, Immutable.fromJS(action.account)); + return normalizeAccount(state, Immutable.fromJS(action.account)); case STATUS_FETCH_SUCCESS: - return contextToMaps(state, Immutable.fromJS(action.status), Immutable.fromJS(action.context.ancestors), Immutable.fromJS(action.context.descendants)); + return normalizeContext(state, Immutable.fromJS(action.status), Immutable.fromJS(action.context.ancestors), Immutable.fromJS(action.context.descendants)); case ACCOUNT_TIMELINE_FETCH_SUCCESS: - return accountTimelineToMaps(state, action.id, Immutable.fromJS(action.statuses)); + return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses)); default: return state; } diff --git a/app/assets/javascripts/components/store/configureStore.jsx b/app/assets/javascripts/components/store/configureStore.jsx @@ -1,7 +1,10 @@ import { createStore, applyMiddleware, compose } from 'redux'; -import thunk from 'redux-thunk'; -import appReducer from '../reducers'; +import thunk from 'redux-thunk'; +import appReducer from '../reducers'; +import { loadingBarMiddleware } from 'react-redux-loading-bar'; export default function configureStore(initialState) { - return createStore(appReducer, initialState, compose(applyMiddleware(thunk), window.devToolsExtension ? window.devToolsExtension() : f => f)); -} + return createStore(appReducer, initialState, compose(applyMiddleware(thunk, loadingBarMiddleware({ + promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'], + })), window.devToolsExtension ? window.devToolsExtension() : f => f)); +}; diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml @@ -1 +1 @@ -= react_component 'Root', default_props, class: 'app-holder', prerender: false += react_component 'Mastodon', default_props, class: 'app-holder', prerender: false diff --git a/package.json b/package.json @@ -21,6 +21,7 @@ "react-immutable-proptypes": "^2.1.0", "react-notification": "^6.1.1", "react-redux": "^4.4.5", + "react-redux-loading-bar": "^2.3.3", "react-router": "^2.8.0", "redux": "^3.5.2", "redux-immutable": "^3.0.8",