Improve how account detailed view looks, load account's statuses
This commit is contained in:
		| @ -14,6 +14,10 @@ export const ACCOUNT_UNFOLLOW_REQUEST = 'ACCOUNT_UNFOLLOW_REQUEST'; | ||||
| export const ACCOUNT_UNFOLLOW_SUCCESS = 'ACCOUNT_UNFOLLOW_SUCCESS'; | ||||
| export const ACCOUNT_UNFOLLOW_FAIL    = 'ACCOUNT_UNFOLLOW_FAIL'; | ||||
|  | ||||
| export const ACCOUNT_TIMELINE_FETCH_REQUEST = 'ACCOUNT_TIMELINE_FETCH_REQUEST'; | ||||
| export const ACCOUNT_TIMELINE_FETCH_SUCCESS = 'ACCOUNT_TIMELINE_FETCH_SUCCESS'; | ||||
| export const ACCOUNT_TIMELINE_FETCH_FAIL    = 'ACCOUNT_TIMELINE_FETCH_FAIL'; | ||||
|  | ||||
| export function setAccountSelf(account) { | ||||
|   return { | ||||
|     type: ACCOUNT_SET_SELF, | ||||
| @ -33,6 +37,18 @@ export function fetchAccount(id) { | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function fetchAccountTimeline(id) { | ||||
|   return (dispatch, getState) => { | ||||
|     dispatch(fetchAccountTimelineRequest(id)); | ||||
|  | ||||
|     api(getState).get(`/api/accounts/${id}/statuses`).then(response => { | ||||
|       dispatch(fetchAccountTimelineSuccess(id, response.data)); | ||||
|     }).catch(error => { | ||||
|       dispatch(fetchAccountTimelineFail(id, error)); | ||||
|     }); | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function fetchAccountRequest(id) { | ||||
|   return { | ||||
|     type: ACCOUNT_FETCH_REQUEST, | ||||
| @ -120,3 +136,26 @@ export function unfollowAccountFail(error) { | ||||
|     error: error | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function fetchAccountTimelineRequest(id) { | ||||
|   return { | ||||
|     type: ACCOUNT_TIMELINE_FETCH_REQUEST, | ||||
|     id: id | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function fetchAccountTimelineSuccess(id, statuses) { | ||||
|   return { | ||||
|     type: ACCOUNT_TIMELINE_FETCH_SUCCESS, | ||||
|     id: id, | ||||
|     statuses: statuses | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function fetchAccountTimelineFail(id, error) { | ||||
|   return { | ||||
|     type: ACCOUNT_TIMELINE_FETCH_FAIL, | ||||
|     id: id, | ||||
|     error: error | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| @ -1,19 +1,57 @@ | ||||
| import ColumnHeader    from './column_header'; | ||||
| import PureRenderMixin from 'react-addons-pure-render-mixin'; | ||||
|  | ||||
| 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.scrollTo(0, easingOutQuint(0, elapsed, offset, targetY, duration)); | ||||
|     requestAnimationFrame(step); | ||||
|   }; | ||||
|  | ||||
|   step(); | ||||
|  | ||||
|   return () => { | ||||
|     interrupt = true; | ||||
|   }; | ||||
| }; | ||||
|  | ||||
|  | ||||
| const Column = React.createClass({ | ||||
|  | ||||
|   propTypes: { | ||||
|     heading: React.PropTypes.string, | ||||
|     icon: React.PropTypes.string, | ||||
|     fluid: React.PropTypes.bool | ||||
|     icon: React.PropTypes.string | ||||
|   }, | ||||
|  | ||||
|   mixins: [PureRenderMixin], | ||||
|  | ||||
|   handleHeaderClick () { | ||||
|     let node = ReactDOM.findDOMNode(this); | ||||
|     node.querySelector('.scrollable').scrollTo(0, 0); | ||||
|     this._interruptScrollAnimation = scrollTop(node.querySelector('.scrollable')); | ||||
|   }, | ||||
|  | ||||
|   handleWheel () { | ||||
|     if (typeof this._interruptScrollAnimation !== 'undefined') { | ||||
|       this._interruptScrollAnimation(); | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   handleScroll () { | ||||
|     // todo | ||||
|   }, | ||||
|  | ||||
|   render () { | ||||
| @ -25,14 +63,8 @@ const Column = React.createClass({ | ||||
|  | ||||
|     const style = { width: '350px', flex: '0 0 auto', background: '#282c37', margin: '10px', marginRight: '0', display: 'flex', flexDirection: 'column' }; | ||||
|  | ||||
|     if (this.props.fluid) { | ||||
|       style.width      = 'auto'; | ||||
|       style.flex       = '1 1 auto'; | ||||
|       style.background = '#21242d'; | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       <div style={style}> | ||||
|       <div style={style} onWheel={this.handleWheel} onScroll={this.handleScroll}> | ||||
|         {header} | ||||
|         {this.props.children} | ||||
|       </div> | ||||
|  | ||||
| @ -35,7 +35,7 @@ const Frontend = React.createClass({ | ||||
|             <StatusListContainer type='mentions' /> | ||||
|           </Column> | ||||
|  | ||||
|           <Column fluid={true}> | ||||
|           <Column> | ||||
|             {this.props.children} | ||||
|           </Column> | ||||
|         </ColumnsArea> | ||||
|  | ||||
| @ -2,18 +2,7 @@ import { connect }           from 'react-redux'; | ||||
| import StatusList            from '../components/status_list'; | ||||
| import { replyCompose }      from '../actions/compose'; | ||||
| import { reblog, favourite } from '../actions/interactions'; | ||||
|  | ||||
| function selectStatus(state, id) { | ||||
|   let status = state.getIn(['timelines', 'statuses', id]); | ||||
|  | ||||
|   status = status.set('account', state.getIn(['timelines', 'accounts', status.get('account')])); | ||||
|  | ||||
|   if (status.get('reblog') !== null) { | ||||
|     status = status.set('reblog', selectStatus(state, status.get('reblog'))); | ||||
|   } | ||||
|  | ||||
|   return status; | ||||
| }; | ||||
| import { selectStatus }      from '../reducers/timelines'; | ||||
|  | ||||
| const mapStateToProps = function (state, props) { | ||||
|   return { | ||||
|  | ||||
| @ -0,0 +1,35 @@ | ||||
| import PureRenderMixin    from 'react-addons-pure-render-mixin'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import Button             from '../../../components/button'; | ||||
|  | ||||
| const Header = React.createClass({ | ||||
|  | ||||
|   propTypes: { | ||||
|     account: ImmutablePropTypes.map.isRequired, | ||||
|     onFollow: React.PropTypes.func.isRequired, | ||||
|     onUnfollow: React.PropTypes.func.isRequired | ||||
|   }, | ||||
|  | ||||
|   mixins: [PureRenderMixin], | ||||
|  | ||||
|   render () { | ||||
|     const { account } = this.props; | ||||
|  | ||||
|     return ( | ||||
|       <div style={{ flex: '0 0 auto', background: '#2f3441', textAlign: 'center', backgroundImage: `url(${account.get('header')})`, backgroundSize: 'cover' }}> | ||||
|         <div style={{ background: 'rgba(47, 52, 65, 0.6)', padding: '30px 10px' }}> | ||||
|           <div style={{ width: '90px', margin: '0 auto', marginBottom: '15px', borderRadius: '90px', overflow: 'hidden' }} className='transparent-background'> | ||||
|             <img src={account.get('avatar')} alt='' style={{ display: 'block', width: '90px', height: '90px', borderRadius: '90px' }} /> | ||||
|           </div> | ||||
|  | ||||
|           <span style={{ color: '#fff', fontSize: '20px', lineHeight: '27px', fontWeight: '500', display: 'block' }}>{account.get('display_name')}</span> | ||||
|           <span style={{ fontSize: '14px', fontWeight: '400', display: 'block', color: '#2b90d9', marginBottom: '15px' }}>@{account.get('acct')}</span> | ||||
|           <p style={{ color: '#616b86', fontSize: '14px' }}>{account.get('note')}</p> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| }); | ||||
|  | ||||
| export default Header; | ||||
| @ -1,15 +1,25 @@ | ||||
| import { connect }                                      from 'react-redux'; | ||||
| import PureRenderMixin                                  from 'react-addons-pure-render-mixin'; | ||||
| import ImmutablePropTypes                               from 'react-immutable-proptypes'; | ||||
| import { fetchAccount, followAccount, unfollowAccount } from '../../actions/accounts'; | ||||
| import Button                                           from '../../components/button'; | ||||
| import { connect }                                                            from 'react-redux'; | ||||
| import PureRenderMixin                                                        from 'react-addons-pure-render-mixin'; | ||||
| import ImmutablePropTypes                                                     from 'react-immutable-proptypes'; | ||||
| import { fetchAccount, followAccount, unfollowAccount, fetchAccountTimeline } from '../../actions/accounts'; | ||||
| import { replyCompose }                                                       from '../../actions/compose'; | ||||
| import { favourite, reblog }                                                  from '../../actions/interactions'; | ||||
| import Header                                                                 from './components/header'; | ||||
| import { selectStatus }                                                       from '../../reducers/timelines'; | ||||
| import StatusList                                                             from '../../components/status_list'; | ||||
| import Immutable                                                              from 'immutable'; | ||||
|  | ||||
| function selectAccount(state, id) { | ||||
|   return state.getIn(['timelines', 'accounts', id], null); | ||||
| } | ||||
| }; | ||||
|  | ||||
| function selectStatuses(state, accountId) { | ||||
|   return state.getIn(['timelines', 'accounts_timelines', accountId], Immutable.List()).map(id => selectStatus(state, id)).filterNot(status => status === null); | ||||
| }; | ||||
|  | ||||
| const mapStateToProps = (state, props) => ({ | ||||
|   account: selectAccount(state, Number(props.params.accountId)) | ||||
|   account: selectAccount(state, Number(props.params.accountId)), | ||||
|   statuses: selectStatuses(state, Number(props.params.accountId)) | ||||
| }); | ||||
|  | ||||
| const Account = React.createClass({ | ||||
| @ -17,59 +27,55 @@ const Account = React.createClass({ | ||||
|   propTypes: { | ||||
|     params: React.PropTypes.object.isRequired, | ||||
|     dispatch: React.PropTypes.func.isRequired, | ||||
|     account: ImmutablePropTypes.map | ||||
|     account: ImmutablePropTypes.map, | ||||
|     statuses: ImmutablePropTypes.list | ||||
|   }, | ||||
|  | ||||
|   mixins: [PureRenderMixin], | ||||
|  | ||||
|   componentWillMount () { | ||||
|     this.props.dispatch(fetchAccount(this.props.params.accountId)); | ||||
|     this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); | ||||
|     this.props.dispatch(fetchAccountTimeline(Number(this.props.params.accountId))); | ||||
|   }, | ||||
|  | ||||
|   componentWillReceiveProps(nextProps) { | ||||
|     if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { | ||||
|       this.props.dispatch(fetchAccount(nextProps.params.accountId)); | ||||
|       this.props.dispatch(fetchAccountTimeline(nextProps.params.accountId)); | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   handleFollowClick () { | ||||
|   handleFollow () { | ||||
|     this.props.dispatch(followAccount(this.props.account.get('id'))); | ||||
|   }, | ||||
|  | ||||
|   handleUnfollowClick () { | ||||
|   handleUnfollow () { | ||||
|     this.props.dispatch(unfollowAccount(this.props.account.get('id'))); | ||||
|   }, | ||||
|  | ||||
|   handleReply (status) { | ||||
|     this.props.dispatch(replyCompose(status)); | ||||
|   }, | ||||
|  | ||||
|   handleReblog (status) { | ||||
|     this.props.dispatch(reblog(status)); | ||||
|   }, | ||||
|  | ||||
|   handleFavourite (status) { | ||||
|     this.props.dispatch(favourite(status)); | ||||
|   }, | ||||
|  | ||||
|   render () { | ||||
|     const { account } = this.props; | ||||
|     let action; | ||||
|     const { account, statuses } = this.props; | ||||
|  | ||||
|     if (account === null) { | ||||
|       return <div>Loading {this.props.params.accountId}...</div>; | ||||
|     } | ||||
|  | ||||
|     if (account.get('following')) { | ||||
|       action = <Button text='Unfollow' onClick={this.handleUnfollowClick} />; | ||||
|     } else { | ||||
|       action = <Button text='Follow' onClick={this.handleFollowClick} /> | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       <div> | ||||
|         <p> | ||||
|           {account.get('display_name')} | ||||
|           {account.get('acct')} | ||||
|         </p> | ||||
|  | ||||
|         {account.get('url')} | ||||
|  | ||||
|         <p>{account.get('note')}</p> | ||||
|  | ||||
|         {account.get('followers_count')} followers<br /> | ||||
|         {account.get('following_count')} following<br /> | ||||
|         {account.get('statuses_count')} posts | ||||
|  | ||||
|         <p>{action}</p> | ||||
|       <div style={{ display: 'flex', flexDirection: 'column', 'flex': '0 0 auto', height: '100%' }}> | ||||
|         <Header account={account} onFollow={this.handleFollow} onUnfollow={this.handleUnfollow} /> | ||||
|         <StatusList statuses={statuses} onReply={this.handleReply} onReblog={this.handleReblog} onFavourite={this.handleFavourite} /> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @ -6,21 +6,10 @@ import Immutable                from 'immutable'; | ||||
| import EmbeddedStatus           from '../../components/status'; | ||||
| import { favourite, reblog }    from '../../actions/interactions'; | ||||
| import { replyCompose }         from '../../actions/compose'; | ||||
|  | ||||
| function selectStatus(state, id) { | ||||
|   let status = state.getIn(['timelines', 'statuses', id]); | ||||
|  | ||||
|   status = status.set('account', state.getIn(['timelines', 'accounts', status.get('account')])); | ||||
|  | ||||
|   if (status.get('reblog') !== null) { | ||||
|     status = status.set('reblog', selectStatus(state, status.get('reblog'))); | ||||
|   } | ||||
|  | ||||
|   return status; | ||||
| }; | ||||
| import { selectStatus }         from '../../reducers/timelines'; | ||||
|  | ||||
| function selectStatuses(state, ids) { | ||||
|   return ids.map(id => selectStatus(state, id)); | ||||
|   return ids.map(id => selectStatus(state, id)).filterNot(status => status === null); | ||||
| }; | ||||
|  | ||||
| const mapStateToProps = (state, props) => ({ | ||||
|  | ||||
| @ -1,20 +1,50 @@ | ||||
| import { TIMELINE_SET, TIMELINE_UPDATE, TIMELINE_DELETE }                                            from '../actions/timelines'; | ||||
| import { REBLOG_SUCCESS, FAVOURITE_SUCCESS }                                                         from '../actions/interactions'; | ||||
| import { ACCOUNT_SET_SELF, ACCOUNT_FETCH_SUCCESS, ACCOUNT_FOLLOW_SUCCESS, ACCOUNT_UNFOLLOW_SUCCESS } from '../actions/accounts'; | ||||
| import { STATUS_FETCH_SUCCESS }                                                                      from '../actions/statuses'; | ||||
| import { FOLLOW_SUBMIT_SUCCESS }                                                                     from '../actions/follow'; | ||||
| import Immutable                                                                                     from 'immutable'; | ||||
| import { | ||||
|   TIMELINE_SET, | ||||
|   TIMELINE_UPDATE, | ||||
|   TIMELINE_DELETE | ||||
| }                                from '../actions/timelines'; | ||||
| import { | ||||
|   REBLOG_SUCCESS, | ||||
|   FAVOURITE_SUCCESS | ||||
| }                                from '../actions/interactions'; | ||||
| import { | ||||
|   ACCOUNT_SET_SELF, | ||||
|   ACCOUNT_FETCH_SUCCESS, | ||||
|   ACCOUNT_FOLLOW_SUCCESS, | ||||
|   ACCOUNT_UNFOLLOW_SUCCESS, | ||||
|   ACCOUNT_TIMELINE_FETCH_SUCCESS | ||||
| }                                from '../actions/accounts'; | ||||
| import { STATUS_FETCH_SUCCESS }  from '../actions/statuses'; | ||||
| import { FOLLOW_SUBMIT_SUCCESS } from '../actions/follow'; | ||||
| import Immutable                 from 'immutable'; | ||||
|  | ||||
| const initialState = Immutable.Map({ | ||||
|   home: Immutable.List([]), | ||||
|   mentions: Immutable.List([]), | ||||
|   statuses: Immutable.Map(), | ||||
|   accounts: Immutable.Map(), | ||||
|   accounts_timelines: Immutable.Map(), | ||||
|   me: null, | ||||
|   ancestors: Immutable.Map(), | ||||
|   descendants: Immutable.Map() | ||||
| }); | ||||
|  | ||||
| export function selectStatus(state, id) { | ||||
|   let status = state.getIn(['timelines', 'statuses', id], null); | ||||
|  | ||||
|   if (status === null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   status = status.set('account', state.getIn(['timelines', 'accounts', status.get('account')])); | ||||
|  | ||||
|   if (status.get('reblog') !== null) { | ||||
|     status = status.set('reblog', selectStatus(state, status.get('reblog'))); | ||||
|   } | ||||
|  | ||||
|   return status; | ||||
| }; | ||||
|  | ||||
| function statusToMaps(state, status) { | ||||
|   // Separate account | ||||
|   let account = status.get('account'); | ||||
| @ -59,6 +89,15 @@ function timelineToMaps(state, timeline, statuses) { | ||||
|   return state; | ||||
| }; | ||||
|  | ||||
| function accountTimelineToMaps(state, accountId, statuses) { | ||||
|   statuses.forEach((status, i) => { | ||||
|     state = statusToMaps(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); | ||||
|   state = state.update(timeline, list => list.unshift(status.get('id'))); | ||||
| @ -120,6 +159,8 @@ export default function timelines(state = initialState, action) { | ||||
|       return accountToMaps(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)); | ||||
|     case ACCOUNT_TIMELINE_FETCH_SUCCESS: | ||||
|       return accountTimelineToMaps(state, action.id, Immutable.fromJS(action.statuses)); | ||||
|     default: | ||||
|       return state; | ||||
|   } | ||||
|  | ||||
| @ -4,6 +4,7 @@ attributes :id, :username, :acct, :display_name, :note | ||||
|  | ||||
| node(:url)             { |account| TagManager.instance.url_for(account) } | ||||
| node(:avatar)          { |account| full_asset_url(account.avatar.url(:large, false)) } | ||||
| node(:header)          { |account| full_asset_url(account.header.url(:medium, false)) } | ||||
| node(:followers_count) { |account| account.followers.count } | ||||
| node(:following_count) { |account| account.following.count } | ||||
| node(:statuses_count)  { |account| account.statuses.count  } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user