Skip to content
Extraits de code Groupes Projets
Valider 0967961d rédigé par Eugen Rochko's avatar Eugen Rochko
Parcourir les fichiers

Improve how account detailed view looks, load account's statuses

parent dafcb021
Aucune branche associée trouvée
Aucune étiquette associée trouvée
Aucune requête de fusion associée trouvée
...@@ -14,6 +14,10 @@ export const ACCOUNT_UNFOLLOW_REQUEST = 'ACCOUNT_UNFOLLOW_REQUEST'; ...@@ -14,6 +14,10 @@ export const ACCOUNT_UNFOLLOW_REQUEST = 'ACCOUNT_UNFOLLOW_REQUEST';
export const ACCOUNT_UNFOLLOW_SUCCESS = 'ACCOUNT_UNFOLLOW_SUCCESS'; export const ACCOUNT_UNFOLLOW_SUCCESS = 'ACCOUNT_UNFOLLOW_SUCCESS';
export const ACCOUNT_UNFOLLOW_FAIL = 'ACCOUNT_UNFOLLOW_FAIL'; 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) { export function setAccountSelf(account) {
return { return {
type: ACCOUNT_SET_SELF, type: ACCOUNT_SET_SELF,
...@@ -33,6 +37,18 @@ export function fetchAccount(id) { ...@@ -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) { export function fetchAccountRequest(id) {
return { return {
type: ACCOUNT_FETCH_REQUEST, type: ACCOUNT_FETCH_REQUEST,
...@@ -120,3 +136,26 @@ export function unfollowAccountFail(error) { ...@@ -120,3 +136,26 @@ export function unfollowAccountFail(error) {
error: 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
};
};
import ColumnHeader from './column_header'; import ColumnHeader from './column_header';
import PureRenderMixin from 'react-addons-pure-render-mixin'; 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({ const Column = React.createClass({
propTypes: { propTypes: {
heading: React.PropTypes.string, heading: React.PropTypes.string,
icon: React.PropTypes.string, icon: React.PropTypes.string
fluid: React.PropTypes.bool
}, },
mixins: [PureRenderMixin], mixins: [PureRenderMixin],
handleHeaderClick () { handleHeaderClick () {
let node = ReactDOM.findDOMNode(this); 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 () { render () {
...@@ -25,14 +63,8 @@ const Column = React.createClass({ ...@@ -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' }; 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 ( return (
<div style={style}> <div style={style} onWheel={this.handleWheel} onScroll={this.handleScroll}>
{header} {header}
{this.props.children} {this.props.children}
</div> </div>
......
...@@ -35,7 +35,7 @@ const Frontend = React.createClass({ ...@@ -35,7 +35,7 @@ const Frontend = React.createClass({
<StatusListContainer type='mentions' /> <StatusListContainer type='mentions' />
</Column> </Column>
<Column fluid={true}> <Column>
{this.props.children} {this.props.children}
</Column> </Column>
</ColumnsArea> </ColumnsArea>
......
...@@ -2,18 +2,7 @@ import { connect } from 'react-redux'; ...@@ -2,18 +2,7 @@ import { connect } from 'react-redux';
import StatusList from '../components/status_list'; import StatusList from '../components/status_list';
import { replyCompose } from '../actions/compose'; import { replyCompose } from '../actions/compose';
import { reblog, favourite } from '../actions/interactions'; import { reblog, favourite } from '../actions/interactions';
import { selectStatus } from '../reducers/timelines';
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;
};
const mapStateToProps = function (state, props) { const mapStateToProps = function (state, props) {
return { return {
......
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;
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin'; import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { fetchAccount, followAccount, unfollowAccount } from '../../actions/accounts'; import { fetchAccount, followAccount, unfollowAccount, fetchAccountTimeline } from '../../actions/accounts';
import Button from '../../components/button'; 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) { function selectAccount(state, id) {
return state.getIn(['timelines', 'accounts', id], null); 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) => ({ 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({ const Account = React.createClass({
...@@ -17,59 +27,55 @@ const Account = React.createClass({ ...@@ -17,59 +27,55 @@ const Account = React.createClass({
propTypes: { propTypes: {
params: React.PropTypes.object.isRequired, params: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired, dispatch: React.PropTypes.func.isRequired,
account: ImmutablePropTypes.map account: ImmutablePropTypes.map,
statuses: ImmutablePropTypes.list
}, },
mixins: [PureRenderMixin], mixins: [PureRenderMixin],
componentWillMount () { 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) { componentWillReceiveProps(nextProps) {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
this.props.dispatch(fetchAccount(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'))); this.props.dispatch(followAccount(this.props.account.get('id')));
}, },
handleUnfollowClick () { handleUnfollow () {
this.props.dispatch(unfollowAccount(this.props.account.get('id'))); 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 () { render () {
const { account } = this.props; const { account, statuses } = this.props;
let action;
if (account === null) { if (account === null) {
return <div>Loading {this.props.params.accountId}...</div>; 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 ( return (
<div> <div style={{ display: 'flex', flexDirection: 'column', 'flex': '0 0 auto', height: '100%' }}>
<p> <Header account={account} onFollow={this.handleFollow} onUnfollow={this.handleUnfollow} />
{account.get('display_name')} <StatusList statuses={statuses} onReply={this.handleReply} onReblog={this.handleReblog} onFavourite={this.handleFavourite} />
{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> </div>
); );
} }
......
...@@ -6,21 +6,10 @@ import Immutable from 'immutable'; ...@@ -6,21 +6,10 @@ import Immutable from 'immutable';
import EmbeddedStatus from '../../components/status'; import EmbeddedStatus from '../../components/status';
import { favourite, reblog } from '../../actions/interactions'; import { favourite, reblog } from '../../actions/interactions';
import { replyCompose } from '../../actions/compose'; import { replyCompose } from '../../actions/compose';
import { selectStatus } from '../../reducers/timelines';
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;
};
function selectStatuses(state, ids) { 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) => ({ const mapStateToProps = (state, props) => ({
......
import { TIMELINE_SET, TIMELINE_UPDATE, TIMELINE_DELETE } from '../actions/timelines'; import {
import { REBLOG_SUCCESS, FAVOURITE_SUCCESS } from '../actions/interactions'; TIMELINE_SET,
import { ACCOUNT_SET_SELF, ACCOUNT_FETCH_SUCCESS, ACCOUNT_FOLLOW_SUCCESS, ACCOUNT_UNFOLLOW_SUCCESS } from '../actions/accounts'; TIMELINE_UPDATE,
import { STATUS_FETCH_SUCCESS } from '../actions/statuses'; TIMELINE_DELETE
import { FOLLOW_SUBMIT_SUCCESS } from '../actions/follow'; } from '../actions/timelines';
import Immutable from 'immutable'; 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({ const initialState = Immutable.Map({
home: Immutable.List([]), home: Immutable.List([]),
mentions: Immutable.List([]), mentions: Immutable.List([]),
statuses: Immutable.Map(), statuses: Immutable.Map(),
accounts: Immutable.Map(), accounts: Immutable.Map(),
accounts_timelines: Immutable.Map(),
me: null, me: null,
ancestors: Immutable.Map(), ancestors: Immutable.Map(),
descendants: 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) { function statusToMaps(state, status) {
// Separate account // Separate account
let account = status.get('account'); let account = status.get('account');
...@@ -59,6 +89,15 @@ function timelineToMaps(state, timeline, statuses) { ...@@ -59,6 +89,15 @@ function timelineToMaps(state, timeline, statuses) {
return state; 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) { function updateTimelineWithMaps(state, timeline, status) {
state = statusToMaps(state, status); state = statusToMaps(state, status);
state = state.update(timeline, list => list.unshift(status.get('id'))); state = state.update(timeline, list => list.unshift(status.get('id')));
...@@ -120,6 +159,8 @@ export default function timelines(state = initialState, action) { ...@@ -120,6 +159,8 @@ export default function timelines(state = initialState, action) {
return accountToMaps(state, Immutable.fromJS(action.account)); return accountToMaps(state, Immutable.fromJS(action.account));
case STATUS_FETCH_SUCCESS: case STATUS_FETCH_SUCCESS:
return contextToMaps(state, Immutable.fromJS(action.status), Immutable.fromJS(action.context.ancestors), Immutable.fromJS(action.context.descendants)); 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: default:
return state; return state;
} }
......
...@@ -4,6 +4,7 @@ attributes :id, :username, :acct, :display_name, :note ...@@ -4,6 +4,7 @@ attributes :id, :username, :acct, :display_name, :note
node(:url) { |account| TagManager.instance.url_for(account) } node(:url) { |account| TagManager.instance.url_for(account) }
node(:avatar) { |account| full_asset_url(account.avatar.url(:large, false)) } 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(:followers_count) { |account| account.followers.count }
node(:following_count) { |account| account.following.count } node(:following_count) { |account| account.following.count }
node(:statuses_count) { |account| account.statuses.count } node(:statuses_count) { |account| account.statuses.count }
......
0% Chargement en cours ou .
You are about to add 0 people to the discussion. Proceed with caution.
Terminez d'abord l'édition de ce message.
Veuillez vous inscrire ou vous pour commenter