Keep timelines in the UI trimmed when possible
This commit is contained in:
		| @@ -12,12 +12,13 @@ export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; | ||||
| export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS'; | ||||
| 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 { | ||||
|     type: TIMELINE_REFRESH_SUCCESS, | ||||
|     timeline: timeline, | ||||
|     statuses: statuses, | ||||
|     replace: replace | ||||
|     statuses: statuses | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| @@ -48,24 +49,25 @@ export function deleteFromTimelines(id) { | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function refreshTimelineRequest(timeline) { | ||||
| export function refreshTimelineRequest(timeline, id) { | ||||
|   return { | ||||
|     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) { | ||||
|     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; | ||||
|  | ||||
|     let params = ''; | ||||
|     let path   = timeline; | ||||
|  | ||||
|     if (newestId !== null && !replace) { | ||||
|     if (newestId !== null) { | ||||
|       params = `?since_id=${newestId}`; | ||||
|     } | ||||
|  | ||||
| @@ -74,7 +76,7 @@ export function refreshTimeline(timeline, replace = false, id = null) { | ||||
|     } | ||||
|  | ||||
|     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) { | ||||
|       dispatch(refreshTimelineFail(timeline, error)); | ||||
|     }); | ||||
| @@ -84,14 +86,14 @@ export function refreshTimeline(timeline, replace = false, id = null) { | ||||
| export function refreshTimelineFail(timeline, error) { | ||||
|   return { | ||||
|     type: TIMELINE_REFRESH_FAIL, | ||||
|     timeline: timeline, | ||||
|     error: error | ||||
|     timeline, | ||||
|     error | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function expandTimeline(timeline, id = null) { | ||||
|   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)); | ||||
|  | ||||
| @@ -112,22 +114,30 @@ export function expandTimeline(timeline, id = null) { | ||||
| export function expandTimelineRequest(timeline) { | ||||
|   return { | ||||
|     type: TIMELINE_EXPAND_REQUEST, | ||||
|     timeline: timeline | ||||
|     timeline | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function expandTimelineSuccess(timeline, statuses) { | ||||
|   return { | ||||
|     type: TIMELINE_EXPAND_SUCCESS, | ||||
|     timeline: timeline, | ||||
|     statuses: statuses | ||||
|     timeline, | ||||
|     statuses | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function expandTimelineFail(timeline, error) { | ||||
|   return { | ||||
|     type: TIMELINE_EXPAND_FAIL, | ||||
|     timeline: timeline, | ||||
|     error: error | ||||
|     timeline, | ||||
|     error | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function scrollTopTimeline(timeline, top) { | ||||
|   return { | ||||
|     type: TIMELINE_SCROLL_TOP, | ||||
|     timeline, | ||||
|     top | ||||
|   }; | ||||
| }; | ||||
|   | ||||
| @@ -1,14 +1,16 @@ | ||||
| import Status              from './status'; | ||||
| import ImmutablePropTypes  from 'react-immutable-proptypes'; | ||||
| import PureRenderMixin     from 'react-addons-pure-render-mixin'; | ||||
| import Status from './status'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import PureRenderMixin from 'react-addons-pure-render-mixin'; | ||||
| import { ScrollContainer } from 'react-router-scroll'; | ||||
| import StatusContainer     from '../containers/status_container'; | ||||
| import StatusContainer from '../containers/status_container'; | ||||
|  | ||||
| const StatusList = React.createClass({ | ||||
|  | ||||
|   propTypes: { | ||||
|     statusIds: ImmutablePropTypes.list.isRequired, | ||||
|     onScrollToBottom: React.PropTypes.func, | ||||
|     onScrollToTop: React.PropTypes.func, | ||||
|     onScroll: React.PropTypes.func, | ||||
|     trackScroll: React.PropTypes.bool | ||||
|   }, | ||||
|  | ||||
| @@ -27,6 +29,10 @@ const StatusList = React.createClass({ | ||||
|  | ||||
|     if (scrollTop === scrollHeight - clientHeight) { | ||||
|       this.props.onScrollToBottom(); | ||||
|     } else if (scrollTop < 100) { | ||||
|       this.props.onScrollToTop(); | ||||
|     } else { | ||||
|       this.props.onScroll(); | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   | ||||
| @@ -47,13 +47,13 @@ const HashtagTimeline = React.createClass({ | ||||
|     const { dispatch } = this.props; | ||||
|     const { id } = this.props.params; | ||||
|  | ||||
|     dispatch(refreshTimeline('tag', true, id)); | ||||
|     dispatch(refreshTimeline('tag', id)); | ||||
|     this._subscribe(dispatch, id); | ||||
|   }, | ||||
|  | ||||
|   componentWillReceiveProps (nextProps) { | ||||
|     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._subscribe(this.props.dispatch, nextProps.params.id); | ||||
|     } | ||||
|   | ||||
| @@ -1,16 +1,25 @@ | ||||
| import { connect } from 'react-redux'; | ||||
| import StatusList from '../../../components/status_list'; | ||||
| import { expandTimeline } from '../../../actions/timelines'; | ||||
| import { expandTimeline, scrollTopTimeline } from '../../../actions/timelines'; | ||||
| import Immutable from 'immutable'; | ||||
|  | ||||
| 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) { | ||||
|   return { | ||||
|     onScrollToBottom () { | ||||
|       dispatch(scrollTopTimeline(props.type, false)); | ||||
|       dispatch(expandTimeline(props.type, props.id)); | ||||
|     }, | ||||
|  | ||||
|     onScrollToTop () { | ||||
|       dispatch(scrollTopTimeline(props.type, true)); | ||||
|     }, | ||||
|  | ||||
|     onScroll () { | ||||
|       dispatch(scrollTopTimeline(props.type, false)); | ||||
|     } | ||||
|   }; | ||||
| }; | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| import { | ||||
|   TIMELINE_REFRESH_REQUEST, | ||||
|   TIMELINE_REFRESH_SUCCESS, | ||||
|   TIMELINE_UPDATE, | ||||
|   TIMELINE_DELETE, | ||||
|   TIMELINE_EXPAND_SUCCESS | ||||
|   TIMELINE_EXPAND_SUCCESS, | ||||
|   TIMELINE_SCROLL_TOP | ||||
| } from '../actions/timelines'; | ||||
| import { | ||||
|   REBLOG_SUCCESS, | ||||
| @@ -23,10 +25,31 @@ import { | ||||
| import Immutable from 'immutable'; | ||||
|  | ||||
| const initialState = Immutable.Map({ | ||||
|   home: Immutable.List(), | ||||
|   mentions: Immutable.List(), | ||||
|   public: Immutable.List(), | ||||
|   tag: Immutable.List(), | ||||
|   home: Immutable.Map({ | ||||
|     loaded: false, | ||||
|     top: true, | ||||
|     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(), | ||||
|   ancestors: Immutable.Map(), | ||||
|   descendants: Immutable.Map() | ||||
| @@ -50,14 +73,17 @@ const normalizeStatus = (state, status) => { | ||||
| }; | ||||
|  | ||||
| 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) => { | ||||
|     state = normalizeStatus(state, status); | ||||
|     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) => { | ||||
| @@ -68,7 +94,7 @@ const appendNormalizedTimeline = (state, timeline, statuses) => { | ||||
|     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) => { | ||||
| @@ -94,9 +120,15 @@ const appendNormalizedAccountTimeline = (state, accountId, statuses) => { | ||||
| }; | ||||
|  | ||||
| const updateTimeline = (state, timeline, status, references) => { | ||||
|   const top = state.getIn([timeline, 'top']); | ||||
|  | ||||
|   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'))) { | ||||
|       return list; | ||||
|     } | ||||
| @@ -116,7 +148,7 @@ const updateTimeline = (state, timeline, status, references) => { | ||||
| const deleteStatus = (state, id, accountId, references) => { | ||||
|   // Remove references from timelines | ||||
|   ['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 | ||||
| @@ -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) { | ||||
|   switch(action.type) { | ||||
|     case TIMELINE_REFRESH_REQUEST: | ||||
|       return resetTimeline(state, action.timeline, action.id); | ||||
|     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: | ||||
|       return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses)); | ||||
|     case TIMELINE_UPDATE: | ||||
| @@ -184,6 +229,8 @@ export default function timelines(state = initialState, action) { | ||||
|       return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses)); | ||||
|     case ACCOUNT_BLOCK_SUCCESS: | ||||
|       return filterTimelines(state, action.relationship, action.statuses); | ||||
|     case TIMELINE_SCROLL_TOP: | ||||
|       return state.setIn([action.timeline, 'top'], action.top); | ||||
|     default: | ||||
|       return state; | ||||
|   } | ||||
|   | ||||
| @@ -2,10 +2,10 @@ class AddFromAccountIdToNotifications < ActiveRecord::Migration[5.0] | ||||
|   def up | ||||
|     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(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(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: '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: '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: '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: '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 | ||||
|  | ||||
|   def down | ||||
|   | ||||
		Reference in New Issue
	
	Block a user