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

Keep timelines in the UI trimmed when possible

parent b14b5e3b
Aucune branche associée trouvée
Aucune étiquette associée trouvée
Aucune requête de fusion associée trouvée
...@@ -12,12 +12,13 @@ export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; ...@@ -12,12 +12,13 @@ export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS'; export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
export function refreshTimelineSuccess(timeline, statuses, replace) { export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
export function refreshTimelineSuccess(timeline, statuses) {
return { return {
type: TIMELINE_REFRESH_SUCCESS, type: TIMELINE_REFRESH_SUCCESS,
timeline: timeline, timeline: timeline,
statuses: statuses, statuses: statuses
replace: replace
}; };
}; };
...@@ -48,24 +49,25 @@ export function deleteFromTimelines(id) { ...@@ -48,24 +49,25 @@ export function deleteFromTimelines(id) {
}; };
}; };
export function refreshTimelineRequest(timeline) { export function refreshTimelineRequest(timeline, id) {
return { return {
type: TIMELINE_REFRESH_REQUEST, type: TIMELINE_REFRESH_REQUEST,
timeline: timeline timeline,
id
}; };
}; };
export function refreshTimeline(timeline, replace = false, id = null) { export function refreshTimeline(timeline, id = null) {
return function (dispatch, getState) { return function (dispatch, getState) {
dispatch(refreshTimelineRequest(timeline)); dispatch(refreshTimelineRequest(timeline, id));
const ids = getState().getIn(['timelines', timeline], Immutable.List()); const ids = getState().getIn(['timelines', timeline, 'items'], Immutable.List());
const newestId = ids.size > 0 ? ids.first() : null; const newestId = ids.size > 0 ? ids.first() : null;
let params = ''; let params = '';
let path = timeline; let path = timeline;
if (newestId !== null && !replace) { if (newestId !== null) {
params = `?since_id=${newestId}`; params = `?since_id=${newestId}`;
} }
...@@ -74,7 +76,7 @@ export function refreshTimeline(timeline, replace = false, id = null) { ...@@ -74,7 +76,7 @@ export function refreshTimeline(timeline, replace = false, id = null) {
} }
api(getState).get(`/api/v1/timelines/${path}${params}`).then(function (response) { api(getState).get(`/api/v1/timelines/${path}${params}`).then(function (response) {
dispatch(refreshTimelineSuccess(timeline, response.data, replace)); dispatch(refreshTimelineSuccess(timeline, response.data));
}).catch(function (error) { }).catch(function (error) {
dispatch(refreshTimelineFail(timeline, error)); dispatch(refreshTimelineFail(timeline, error));
}); });
...@@ -84,14 +86,14 @@ export function refreshTimeline(timeline, replace = false, id = null) { ...@@ -84,14 +86,14 @@ export function refreshTimeline(timeline, replace = false, id = null) {
export function refreshTimelineFail(timeline, error) { export function refreshTimelineFail(timeline, error) {
return { return {
type: TIMELINE_REFRESH_FAIL, type: TIMELINE_REFRESH_FAIL,
timeline: timeline, timeline,
error: error error
}; };
}; };
export function expandTimeline(timeline, id = null) { export function expandTimeline(timeline, id = null) {
return (dispatch, getState) => { return (dispatch, getState) => {
const lastId = getState().getIn(['timelines', timeline], Immutable.List()).last(); const lastId = getState().getIn(['timelines', timeline, 'items'], Immutable.List()).last();
dispatch(expandTimelineRequest(timeline)); dispatch(expandTimelineRequest(timeline));
...@@ -112,22 +114,30 @@ export function expandTimeline(timeline, id = null) { ...@@ -112,22 +114,30 @@ export function expandTimeline(timeline, id = null) {
export function expandTimelineRequest(timeline) { export function expandTimelineRequest(timeline) {
return { return {
type: TIMELINE_EXPAND_REQUEST, type: TIMELINE_EXPAND_REQUEST,
timeline: timeline timeline
}; };
}; };
export function expandTimelineSuccess(timeline, statuses) { export function expandTimelineSuccess(timeline, statuses) {
return { return {
type: TIMELINE_EXPAND_SUCCESS, type: TIMELINE_EXPAND_SUCCESS,
timeline: timeline, timeline,
statuses: statuses statuses
}; };
}; };
export function expandTimelineFail(timeline, error) { export function expandTimelineFail(timeline, error) {
return { return {
type: TIMELINE_EXPAND_FAIL, type: TIMELINE_EXPAND_FAIL,
timeline: timeline, timeline,
error: error error
};
};
export function scrollTopTimeline(timeline, top) {
return {
type: TIMELINE_SCROLL_TOP,
timeline,
top
}; };
}; };
import Status from './status'; import Status from './status';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PureRenderMixin from 'react-addons-pure-render-mixin'; import PureRenderMixin from 'react-addons-pure-render-mixin';
import { ScrollContainer } from 'react-router-scroll'; import { ScrollContainer } from 'react-router-scroll';
import StatusContainer from '../containers/status_container'; import StatusContainer from '../containers/status_container';
const StatusList = React.createClass({ const StatusList = React.createClass({
propTypes: { propTypes: {
statusIds: ImmutablePropTypes.list.isRequired, statusIds: ImmutablePropTypes.list.isRequired,
onScrollToBottom: React.PropTypes.func, onScrollToBottom: React.PropTypes.func,
onScrollToTop: React.PropTypes.func,
onScroll: React.PropTypes.func,
trackScroll: React.PropTypes.bool trackScroll: React.PropTypes.bool
}, },
...@@ -27,6 +29,10 @@ const StatusList = React.createClass({ ...@@ -27,6 +29,10 @@ const StatusList = React.createClass({
if (scrollTop === scrollHeight - clientHeight) { if (scrollTop === scrollHeight - clientHeight) {
this.props.onScrollToBottom(); this.props.onScrollToBottom();
} else if (scrollTop < 100) {
this.props.onScrollToTop();
} else {
this.props.onScroll();
} }
}, },
......
...@@ -47,13 +47,13 @@ const HashtagTimeline = React.createClass({ ...@@ -47,13 +47,13 @@ const HashtagTimeline = React.createClass({
const { dispatch } = this.props; const { dispatch } = this.props;
const { id } = this.props.params; const { id } = this.props.params;
dispatch(refreshTimeline('tag', true, id)); dispatch(refreshTimeline('tag', id));
this._subscribe(dispatch, id); this._subscribe(dispatch, id);
}, },
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.params.id !== this.props.params.id) { if (nextProps.params.id !== this.props.params.id) {
this.props.dispatch(refreshTimeline('tag', true, nextProps.params.id)); this.props.dispatch(refreshTimeline('tag', nextProps.params.id));
this._unsubscribe(); this._unsubscribe();
this._subscribe(this.props.dispatch, nextProps.params.id); this._subscribe(this.props.dispatch, nextProps.params.id);
} }
......
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import StatusList from '../../../components/status_list'; import StatusList from '../../../components/status_list';
import { expandTimeline } from '../../../actions/timelines'; import { expandTimeline, scrollTopTimeline } from '../../../actions/timelines';
import Immutable from 'immutable'; import Immutable from 'immutable';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
statusIds: state.getIn(['timelines', props.type], Immutable.List()) statusIds: state.getIn(['timelines', props.type, 'items'], Immutable.List())
}); });
const mapDispatchToProps = function (dispatch, props) { const mapDispatchToProps = function (dispatch, props) {
return { return {
onScrollToBottom () { onScrollToBottom () {
dispatch(scrollTopTimeline(props.type, false));
dispatch(expandTimeline(props.type, props.id)); dispatch(expandTimeline(props.type, props.id));
},
onScrollToTop () {
dispatch(scrollTopTimeline(props.type, true));
},
onScroll () {
dispatch(scrollTopTimeline(props.type, false));
} }
}; };
}; };
......
import { import {
TIMELINE_REFRESH_REQUEST,
TIMELINE_REFRESH_SUCCESS, TIMELINE_REFRESH_SUCCESS,
TIMELINE_UPDATE, TIMELINE_UPDATE,
TIMELINE_DELETE, TIMELINE_DELETE,
TIMELINE_EXPAND_SUCCESS TIMELINE_EXPAND_SUCCESS,
TIMELINE_SCROLL_TOP
} from '../actions/timelines'; } from '../actions/timelines';
import { import {
REBLOG_SUCCESS, REBLOG_SUCCESS,
...@@ -23,10 +25,31 @@ import { ...@@ -23,10 +25,31 @@ import {
import Immutable from 'immutable'; import Immutable from 'immutable';
const initialState = Immutable.Map({ const initialState = Immutable.Map({
home: Immutable.List(), home: Immutable.Map({
mentions: Immutable.List(), loaded: false,
public: Immutable.List(), top: true,
tag: Immutable.List(), items: Immutable.List()
}),
mentions: Immutable.Map({
loaded: false,
top: true,
items: Immutable.List()
}),
public: Immutable.Map({
loaded: false,
top: true,
items: Immutable.List()
}),
tag: Immutable.Map({
id: null,
loaded: false,
top: true,
items: Immutable.List()
}),
accounts_timelines: Immutable.Map(), accounts_timelines: Immutable.Map(),
ancestors: Immutable.Map(), ancestors: Immutable.Map(),
descendants: Immutable.Map() descendants: Immutable.Map()
...@@ -50,14 +73,17 @@ const normalizeStatus = (state, status) => { ...@@ -50,14 +73,17 @@ const normalizeStatus = (state, status) => {
}; };
const normalizeTimeline = (state, timeline, statuses, replace = false) => { const normalizeTimeline = (state, timeline, statuses, replace = false) => {
let ids = Immutable.List(); let ids = Immutable.List();
const loaded = state.getIn([timeline, 'loaded']);
statuses.forEach((status, i) => { statuses.forEach((status, i) => {
state = normalizeStatus(state, status); state = normalizeStatus(state, status);
ids = ids.set(i, status.get('id')); ids = ids.set(i, status.get('id'));
}); });
return state.update(timeline, Immutable.List(), list => (replace ? ids : list.unshift(...ids))); state = state.setIn([timeline, 'loaded'], true);
return state.updateIn([timeline, 'items'], Immutable.List(), list => (loaded ? list.unshift(...ids) : list.push(...ids)));
}; };
const appendNormalizedTimeline = (state, timeline, statuses) => { const appendNormalizedTimeline = (state, timeline, statuses) => {
...@@ -68,7 +94,7 @@ const appendNormalizedTimeline = (state, timeline, statuses) => { ...@@ -68,7 +94,7 @@ const appendNormalizedTimeline = (state, timeline, statuses) => {
moreIds = moreIds.set(i, status.get('id')); moreIds = moreIds.set(i, status.get('id'));
}); });
return state.update(timeline, Immutable.List(), list => list.push(...moreIds)); return state.updateIn([timeline, 'items'], Immutable.List(), list => list.push(...moreIds));
}; };
const normalizeAccountTimeline = (state, accountId, statuses, replace = false) => { const normalizeAccountTimeline = (state, accountId, statuses, replace = false) => {
...@@ -94,9 +120,15 @@ const appendNormalizedAccountTimeline = (state, accountId, statuses) => { ...@@ -94,9 +120,15 @@ const appendNormalizedAccountTimeline = (state, accountId, statuses) => {
}; };
const updateTimeline = (state, timeline, status, references) => { const updateTimeline = (state, timeline, status, references) => {
const top = state.getIn([timeline, 'top']);
state = normalizeStatus(state, status); state = normalizeStatus(state, status);
state = state.update(timeline, Immutable.List(), list => { state = state.updateIn([timeline, 'items'], Immutable.List(), list => {
if (top && list.size > 40) {
list = list.take(20);
}
if (list.includes(status.get('id'))) { if (list.includes(status.get('id'))) {
return list; return list;
} }
...@@ -116,7 +148,7 @@ const updateTimeline = (state, timeline, status, references) => { ...@@ -116,7 +148,7 @@ const updateTimeline = (state, timeline, status, references) => {
const deleteStatus = (state, id, accountId, references) => { const deleteStatus = (state, id, accountId, references) => {
// Remove references from timelines // Remove references from timelines
['home', 'mentions', 'public', 'tag'].forEach(function (timeline) { ['home', 'mentions', 'public', 'tag'].forEach(function (timeline) {
state = state.update(timeline, list => list.filterNot(item => item === id)); state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id));
}); });
// Remove references from account timelines // Remove references from account timelines
...@@ -166,10 +198,23 @@ const normalizeContext = (state, id, ancestors, descendants) => { ...@@ -166,10 +198,23 @@ const normalizeContext = (state, id, ancestors, descendants) => {
}); });
}; };
const resetTimeline = (state, timeline, id) => {
if (timeline === 'tag' && state.getIn([timeline, 'id']) !== id) {
state = state.update(timeline, map => map
.set('id', id)
.set('loaded', false)
.update('items', list => list.clear()));
}
return state;
};
export default function timelines(state = initialState, action) { export default function timelines(state = initialState, action) {
switch(action.type) { switch(action.type) {
case TIMELINE_REFRESH_REQUEST:
return resetTimeline(state, action.timeline, action.id);
case TIMELINE_REFRESH_SUCCESS: case TIMELINE_REFRESH_SUCCESS:
return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.replace); return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses));
case TIMELINE_EXPAND_SUCCESS: case TIMELINE_EXPAND_SUCCESS:
return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses)); return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses));
case TIMELINE_UPDATE: case TIMELINE_UPDATE:
...@@ -184,6 +229,8 @@ export default function timelines(state = initialState, action) { ...@@ -184,6 +229,8 @@ export default function timelines(state = initialState, action) {
return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses)); return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses));
case ACCOUNT_BLOCK_SUCCESS: case ACCOUNT_BLOCK_SUCCESS:
return filterTimelines(state, action.relationship, action.statuses); return filterTimelines(state, action.relationship, action.statuses);
case TIMELINE_SCROLL_TOP:
return state.setIn([action.timeline, 'top'], action.top);
default: default:
return state; return state;
} }
......
...@@ -2,10 +2,10 @@ class AddFromAccountIdToNotifications < ActiveRecord::Migration[5.0] ...@@ -2,10 +2,10 @@ class AddFromAccountIdToNotifications < ActiveRecord::Migration[5.0]
def up def up
add_column :notifications, :from_account_id, :integer add_column :notifications, :from_account_id, :integer
Notification.where(activity_type: 'Status').update_all('from_account_id = (SELECT statuses.account_id FROM notifications AS notifications1 INNER JOIN statuses ON notifications1.activity_id = statuses.id WHERE notifications1.activity_type = \'Status\' AND notifications1.id = notifications.id)') Notification.where(from_account_id: nil).where(activity_type: 'Status').update_all('from_account_id = (SELECT statuses.account_id FROM notifications AS notifications1 INNER JOIN statuses ON notifications1.activity_id = statuses.id WHERE notifications1.activity_type = \'Status\' AND notifications1.id = notifications.id)')
Notification.where(activity_type: 'Mention').update_all('from_account_id = (SELECT statuses.account_id FROM notifications AS notifications1 INNER JOIN mentions ON notifications1.activity_id = mentions.id INNER JOIN statuses ON mentions.status_id = statuses.id WHERE notifications1.activity_type = \'Mention\' AND notifications1.id = notifications.id)') Notification.where(from_account_id: nil).where(activity_type: 'Mention').update_all('from_account_id = (SELECT statuses.account_id FROM notifications AS notifications1 INNER JOIN mentions ON notifications1.activity_id = mentions.id INNER JOIN statuses ON mentions.status_id = statuses.id WHERE notifications1.activity_type = \'Mention\' AND notifications1.id = notifications.id)')
Notification.where(activity_type: 'Favourite').update_all('from_account_id = (SELECT favourites.account_id FROM notifications AS notifications1 INNER JOIN favourites ON notifications1.activity_id = favourites.id WHERE notifications1.activity_type = \'Favourite\' AND notifications1.id = notifications.id)') Notification.where(from_account_id: nil).where(activity_type: 'Favourite').update_all('from_account_id = (SELECT favourites.account_id FROM notifications AS notifications1 INNER JOIN favourites ON notifications1.activity_id = favourites.id WHERE notifications1.activity_type = \'Favourite\' AND notifications1.id = notifications.id)')
Notification.where(activity_type: 'Follow').update_all('from_account_id = (SELECT follows.account_id FROM notifications AS notifications1 INNER JOIN follows ON notifications1.activity_id = follows.id WHERE notifications1.activity_type = \'Follow\' AND notifications1.id = notifications.id)') Notification.where(from_account_id: nil).where(activity_type: 'Follow').update_all('from_account_id = (SELECT follows.account_id FROM notifications AS notifications1 INNER JOIN follows ON notifications1.activity_id = follows.id WHERE notifications1.activity_type = \'Follow\' AND notifications1.id = notifications.id)')
end end
def down def down
......
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