Skip to content
Snippets Groups Projects
Commit 5b0cef97 authored by Eugen Rochko's avatar Eugen Rochko
Browse files

Setting up preliminary "detailed" routes in the UI, new API end-point for fetching status context

parent 2e7aac79
No related branches found
No related tags found
No related merge requests found
Showing
with 400 additions and 12 deletions
import api from '../api'
export const ACCOUNT_SET_SELF = 'ACCOUNT_SET_SELF';
export const ACCOUNT_SET_SELF = 'ACCOUNT_SET_SELF';
export const ACCOUNT_FETCH = 'ACCOUNT_FETCH';
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
export const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL';
export const ACCOUNT_FOLLOW = 'ACCOUNT_FOLLOW';
export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST';
export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS';
export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL';
export const ACCOUNT_UNFOLLOW = 'ACCOUNT_UNFOLLOW';
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 function setAccountSelf(account) {
return {
type: ACCOUNT_SET_SELF,
......@@ -46,3 +57,69 @@ export function fetchAccountFail(id, error) {
error: error
};
};
export function followAccount(id) {
return (dispatch, getState) => {
dispatch(followAccountRequest(id));
api(getState).post(`/api/accounts/${id}/follow`).then(response => {
dispatch(followAccountSuccess(response.data));
}).catch(error => {
dispatch(followAccountFail(error));
});
};
};
export function unfollowAccount(id) {
return (dispatch, getState) => {
dispatch(unfollowAccountRequest(id));
api(getState).post(`/api/accounts/${id}/unfollow`).then(response => {
dispatch(unfollowAccountSuccess(response.data));
}).catch(error => {
dispatch(unfollowAccountFail(error));
});
}
};
export function followAccountRequest(id) {
return {
type: ACCOUNT_FOLLOW_REQUEST,
id: id
};
};
export function followAccountSuccess(account) {
return {
type: ACCOUNT_FOLLOW_SUCCESS,
account: account
};
};
export function followAccountFail(error) {
return {
type: ACCOUNT_FOLLOW_FAIL,
error: error
};
};
export function unfollowAccountRequest(id) {
return {
type: ACCOUNT_UNFOLLOW_REQUEST,
id: id
};
};
export function unfollowAccountSuccess(account) {
return {
type: ACCOUNT_UNFOLLOW_SUCCESS,
account: account
};
};
export function unfollowAccountFail(error) {
return {
type: ACCOUNT_UNFOLLOW_FAIL,
error: error
};
};
import api from '../api';
import api from '../api';
import axios from 'axios';
export const STATUS_FETCH = 'STATUS_FETCH';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
export const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL';
export function fetchStatusRequest(id) {
return {
type: STATUS_FETCH_REQUEST,
id: id
};
};
export function fetchStatus(id) {
return (dispatch, getState) => {
const boundApi = api(getState);
dispatch(fetchStatusRequest(id));
axios.all([boundApi.get(`/api/statuses/${id}`), boundApi.get(`/api/statuses/${id}/context`)]).then(values => {
dispatch(fetchStatusSuccess(values[0].data, values[1].data));
}).catch(error => {
dispatch(fetchStatusFail(id, error));
});
};
};
export function fetchStatusSuccess(status, context) {
return {
type: STATUS_FETCH_SUCCESS,
status: status,
context: context
};
};
export function fetchStatusFail(id, error) {
return {
type: STATUS_FETCH_FAIL,
id: id,
error: error
};
};
......@@ -15,7 +15,7 @@ const NavigationBar = React.createClass({
render () {
return (
<div style={{ padding: '10px', display: 'flex', cursor: 'default' }}>
<Avatar src={this.props.account.get('avatar')} size={40} />
<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>
......
......@@ -32,7 +32,16 @@ const Status = React.createClass({
},
handleClick () {
hashHistory.push(`/statuses/${this.props.status.get('id')}`);
const { status } = this.props;
hashHistory.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
},
handleAccountClick (id, e) {
if (e.button === 0) {
e.preventDefault();
hashHistory.push(`/accounts/${id}`);
e.stopPropagation();
}
},
render () {
......@@ -46,7 +55,7 @@ const Status = React.createClass({
<div style={{ cursor: 'pointer' }} onClick={this.handleClick}>
<div style={{ marginLeft: '68px', color: '#616b86', padding: '8px 0', paddingBottom: '2px', fontSize: '14px', position: 'relative' }}>
<div style={{ position: 'absolute', 'left': '-26px'}}><i className='fa fa-fw fa-retweet'></i></div>
<a href={status.getIn(['account', 'url'])} className='status__display-name'><strong style={{ color: '#616b86'}}>{status.getIn(['account', 'display_name'])}</strong></a> reblogged
<a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name'><strong style={{ color: '#616b86'}}>{status.getIn(['account', 'display_name'])}</strong></a> reblogged
</div>
<Status {...other} wrapped={true} status={status.get('reblog')} />
......@@ -65,7 +74,7 @@ const Status = React.createClass({
<a href={status.get('url')} className='status__relative-time' style={{ color: '#616b86' }}><RelativeTimestamp timestamp={status.get('created_at')} /></a>
</div>
<a href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', color: '#616b86' }}>
<a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', color: '#616b86' }}>
<div style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}>
<Avatar src={status.getIn(['account', 'avatar'])} size={48} />
</div>
......
......@@ -6,6 +6,10 @@ import { setAccessToken } fro
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();
......@@ -55,10 +59,10 @@ const Root = React.createClass({
<Provider store={store}>
<Router history={hashHistory}>
<Route path='/' component={Frontend}>
<Route path='/settings' component={null} />
<Route path='/subscriptions' component={null} />
<Route path='/statuses/:statusId' component={null} />
<Route path='/accounts/:accountId' component={null} />
<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>
......
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';
function selectAccount(state, id) {
return state.getIn(['timelines', 'accounts', id], null);
}
const mapStateToProps = (state, props) => ({
account: selectAccount(state, Number(props.params.accountId))
});
const Account = React.createClass({
propTypes: {
params: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired,
account: ImmutablePropTypes.map
},
mixins: [PureRenderMixin],
componentWillMount () {
this.props.dispatch(fetchAccount(this.props.params.accountId));
},
componentWillReceiveProps(nextProps) {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
this.props.dispatch(fetchAccount(nextProps.params.accountId));
}
},
handleFollowClick () {
this.props.dispatch(followAccount(this.props.account.get('id')));
},
handleUnfollowClick () {
this.props.dispatch(unfollowAccount(this.props.account.get('id')));
},
render () {
const { account } = this.props;
let action;
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>
);
}
});
export default connect(mapStateToProps)(Account);
import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
const mapStateToProps = (state, props) => ({
});
const Settings = React.createClass({
propTypes: {
params: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired
},
mixins: [PureRenderMixin],
componentWillMount () {
//
},
render () {
return <div>Settings</div>;
}
});
export default connect(mapStateToProps)(Settings);
import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { fetchStatus } from '../../actions/statuses';
import Immutable from 'immutable';
import EmbeddedStatus from '../../components/status';
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) {
return ids.map(id => selectStatus(state, id));
};
const mapStateToProps = (state, props) => ({
status: selectStatus(state, Number(props.params.statusId)),
ancestors: selectStatuses(state, state.getIn(['timelines', 'ancestors', Number(props.params.statusId)], Immutable.List())),
descendants: selectStatuses(state, state.getIn(['timelines', 'descendants', Number(props.params.statusId)], Immutable.List()))
});
const Status = React.createClass({
propTypes: {
params: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired,
status: ImmutablePropTypes.map,
ancestors: ImmutablePropTypes.list.isRequired,
descendants: ImmutablePropTypes.list.isRequired
},
mixins: [PureRenderMixin],
componentWillMount () {
this.props.dispatch(fetchStatus(this.props.params.statusId));
},
componentWillReceiveProps (nextProps) {
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
this.props.dispatch(fetchStatus(nextProps.params.statusId));
}
},
renderChildren (list) {
return list.map(s => <EmbeddedStatus status={s} key={s.get('id')} />);
},
render () {
const { status, ancestors, descendants } = this.props;
if (status === null) {
return <div>Loading {this.props.params.statusId}...</div>;
}
return (
<div>
{this.renderChildren(ancestors)}
<EmbeddedStatus status={status} />
{this.renderChildren(descendants)}
</div>
);
}
});
export default connect(mapStateToProps)(Status);
import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
const mapStateToProps = (state, props) => ({
});
const Subscriptions = React.createClass({
propTypes: {
params: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired
},
mixins: [PureRenderMixin],
componentWillMount () {
//
},
render () {
return <div>Subscriptions</div>;
}
});
export default connect(mapStateToProps)(Subscriptions);
import { TIMELINE_SET, TIMELINE_UPDATE, TIMELINE_DELETE } from '../actions/timelines';
import { REBLOG_SUCCESS, FAVOURITE_SUCCESS } from '../actions/interactions';
import { ACCOUNT_SET_SELF } from '../actions/accounts';
import { ACCOUNT_SET_SELF, ACCOUNT_FETCH_SUCCESS } from '../actions/accounts';
import { STATUS_FETCH_SUCCESS } from '../actions/statuses';
import Immutable from 'immutable';
const initialState = Immutable.Map({
......@@ -8,7 +9,9 @@ const initialState = Immutable.Map({
mentions: Immutable.List([]),
statuses: Immutable.Map(),
accounts: Immutable.Map(),
me: null
me: null,
ancestors: Immutable.Map(),
descendants: Immutable.Map()
});
function statusToMaps(state, status) {
......@@ -54,6 +57,29 @@ function deleteStatus(state, id) {
return state.deleteIn(['statuses', id]);
};
function accountToMaps(state, account) {
return state.setIn(['accounts', account.get('id')], account);
};
function contextToMaps(state, status, ancestors, descendants) {
state = statusToMaps(state, status);
let ancestorsIds = ancestors.map(ancestor => {
state = statusToMaps(state, ancestor);
return ancestor.get('id');
});
let descendantsIds = descendants.map(descendant => {
state = statusToMaps(state, descendant);
return descendant.get('id');
});
return state.withMutations(map => {
map.setIn(['ancestors', status.get('id')], ancestorsIds);
map.setIn(['descendants', status.get('id')], descendantsIds);
});
};
export default function timelines(state = initialState, action) {
switch(action.type) {
case TIMELINE_SET:
......@@ -70,6 +96,10 @@ export default function timelines(state = initialState, action) {
map.setIn(['accounts', action.account.id], Immutable.fromJS(action.account));
map.set('me', action.account.id);
});
case ACCOUNT_FETCH_SUCCESS:
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));
default:
return state;
}
......
......@@ -6,6 +6,12 @@ class Api::StatusesController < ApiController
@status = Status.find(params[:id])
end
def context
@status = Status.find(params[:id])
@ancestors = @status.ancestors.with_includes.with_counters
@descendants = @status.descendants.with_includes.with_counters
end
def create
@status = PostStatusService.new.(current_user.account, params[:status], params[:in_reply_to_id].blank? ? nil : Status.find(params[:in_reply_to_id]), params[:media_ids])
render action: :show
......
object false
node :ancestors do
@ancestors.map do |status|
partial('api/statuses/show', object: status)
end
end
node :descendants do
@descendants.map do |status|
partial('api/statuses/show', object: status)
end
end
......@@ -47,6 +47,8 @@ Rails.application.routes.draw do
end
member do
get :context
post :reblog
post :favourite
end
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment