From 905c82917959a5afe24cb85c62c0b0ba13f0da8b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 26 Jan 2017 04:30:40 +0100 Subject: [PATCH] Improve infinite scroll on notifications --- .../components/actions/notifications.jsx | 8 +++++-- .../features/notifications/index.jsx | 9 +++++--- .../components/reducers/notifications.jsx | 23 ++++++++++++++++--- app/assets/stylesheets/about.scss | 2 -- app/assets/stylesheets/application.scss | 1 + app/assets/stylesheets/boost.scss | 8 +++---- .../api/v1/notifications_controller.rb | 6 +++-- 7 files changed, 40 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/components/actions/notifications.jsx b/app/assets/javascripts/components/actions/notifications.jsx index 23bdcb5c7..1731c1857 100644 --- a/app/assets/javascripts/components/actions/notifications.jsx +++ b/app/assets/javascripts/components/actions/notifications.jsx @@ -96,13 +96,17 @@ export function expandNotifications() { return (dispatch, getState) => { const url = getState().getIn(['notifications', 'next'], null); - if (url === null) { + if (url === null || getState().getIn(['notifications', 'isLoading'])) { return; } dispatch(expandNotificationsRequest()); - api(getState).get(url).then(response => { + api(getState).get(url, { + params: { + limit: 5 + } + }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null)); diff --git a/app/assets/javascripts/components/features/notifications/index.jsx b/app/assets/javascripts/components/features/notifications/index.jsx index 366d8f5e9..b4593aaff 100644 --- a/app/assets/javascripts/components/features/notifications/index.jsx +++ b/app/assets/javascripts/components/features/notifications/index.jsx @@ -20,7 +20,8 @@ const getNotifications = createSelector([ ], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type')))); const mapStateToProps = state => ({ - notifications: getNotifications(state) + notifications: getNotifications(state), + isLoading: state.getIn(['notifications', 'isLoading'], true) }); const Notifications = React.createClass({ @@ -29,7 +30,8 @@ const Notifications = React.createClass({ notifications: ImmutablePropTypes.list.isRequired, dispatch: React.PropTypes.func.isRequired, trackScroll: React.PropTypes.bool, - intl: React.PropTypes.object.isRequired + intl: React.PropTypes.object.isRequired, + isLoading: React.PropTypes.bool }, getDefaultProps () { @@ -42,8 +44,9 @@ const Notifications = React.createClass({ handleScroll (e) { const { scrollTop, scrollHeight, clientHeight } = e.target; + const offset = scrollHeight - scrollTop - clientHeight; - if (scrollTop === scrollHeight - clientHeight) { + if (250 > offset && !this.props.isLoading) { this.props.dispatch(expandNotifications()); } }, diff --git a/app/assets/javascripts/components/reducers/notifications.jsx b/app/assets/javascripts/components/reducers/notifications.jsx index c85e7b460..482093c33 100644 --- a/app/assets/javascripts/components/reducers/notifications.jsx +++ b/app/assets/javascripts/components/reducers/notifications.jsx @@ -2,6 +2,10 @@ import { NOTIFICATIONS_UPDATE, NOTIFICATIONS_REFRESH_SUCCESS, NOTIFICATIONS_EXPAND_SUCCESS, + NOTIFICATIONS_REFRESH_REQUEST, + NOTIFICATIONS_EXPAND_REQUEST, + NOTIFICATIONS_REFRESH_FAIL, + NOTIFICATIONS_EXPAND_FAIL } from '../actions/notifications'; import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts'; import Immutable from 'immutable'; @@ -9,7 +13,8 @@ import Immutable from 'immutable'; const initialState = Immutable.Map({ items: Immutable.List(), next: null, - loaded: false + loaded: false, + isLoading: true }); const notificationToMap = notification => Immutable.Map({ @@ -31,7 +36,11 @@ const normalizeNotifications = (state, notifications, next) => { items = items.set(i, notificationToMap(n)); }); - return state.update('items', list => loaded ? list.unshift(...items) : list.push(...items)).set('next', next).set('loaded', true); + return state + .update('items', list => loaded ? list.unshift(...items) : list.push(...items)) + .set('next', next) + .set('loaded', true) + .set('isLoading', false); }; const appendNormalizedNotifications = (state, notifications, next) => { @@ -41,7 +50,10 @@ const appendNormalizedNotifications = (state, notifications, next) => { items = items.set(i, notificationToMap(n)); }); - return state.update('items', list => list.push(...items)).set('next', next); + return state + .update('items', list => list.push(...items)) + .set('next', next) + .set('isLoading', false); }; const filterNotifications = (state, relationship) => { @@ -50,6 +62,11 @@ const filterNotifications = (state, relationship) => { export default function notifications(state = initialState, action) { switch(action.type) { + case NOTIFICATIONS_REFRESH_REQUEST: + case NOTIFICATIONS_EXPAND_REQUEST: + case NOTIFICATIONS_REFRESH_FAIL: + case NOTIFICATIONS_EXPAND_FAIL: + return state.set('isLoading', true); case NOTIFICATIONS_UPDATE: return normalizeNotification(state, action.notification); case NOTIFICATIONS_REFRESH_SUCCESS: diff --git a/app/assets/stylesheets/about.scss b/app/assets/stylesheets/about.scss index d92febced..b7d903ddf 100644 --- a/app/assets/stylesheets/about.scss +++ b/app/assets/stylesheets/about.scss @@ -1,5 +1,3 @@ -@import url(https://fonts.googleapis.com/css?family=Montserrat); - .about-body { .wrapper { max-width: 600px; diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index fc58c7463..649a0148b 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -1,6 +1,7 @@ @import 'variables'; @import url(https://fonts.googleapis.com/css?family=Roboto:400,500,400italic); @import url(https://fonts.googleapis.com/css?family=Roboto+Mono:400,500); +@import url(https://fonts.googleapis.com/css?family=Montserrat); @import 'font-awesome'; /* http://meyerweb.com/eric/tools/css/reset/ diff --git a/app/assets/stylesheets/boost.scss b/app/assets/stylesheets/boost.scss index c45d4ff8e..a2e6421f8 100644 --- a/app/assets/stylesheets/boost.scss +++ b/app/assets/stylesheets/boost.scss @@ -1,9 +1,7 @@ -@import 'variables'; - @function url-friendly-colour($colour) { - @return '%23' + str-slice('#{$colour}', 2, -1) + @return '%23' + str-slice('#{$colour}', 2, -1) } button i.fa-retweet { -background-image: url("data:image/svg+xml;utf8,"); -} \ No newline at end of file + background-image: url("data:image/svg+xml;utf8,"); +} diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 157a88e14..877356a75 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -6,8 +6,10 @@ class Api::V1::NotificationsController < ApiController respond_to :json + DEFAULT_NOTIFICATIONS_LIMIT = 15 + def index - @notifications = Notification.where(account: current_account).browserable.paginate_by_max_id(limit_param(15), params[:max_id], params[:since_id]) + @notifications = Notification.where(account: current_account).browserable.paginate_by_max_id(limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params[:max_id], params[:since_id]) @notifications = cache_collection(@notifications, Notification) statuses = @notifications.select { |n| !n.target_status.nil? }.map(&:target_status) @@ -15,7 +17,7 @@ class Api::V1::NotificationsController < ApiController set_counters_maps(statuses) set_account_counters_maps(@notifications.map(&:from_account)) - next_path = api_v1_notifications_url(max_id: @notifications.last.id) if @notifications.size == limit_param(15) + next_path = api_v1_notifications_url(max_id: @notifications.last.id) if @notifications.size == limit_param(DEFAULT_NOTIFICATIONS_LIMIT) prev_path = api_v1_notifications_url(since_id: @notifications.first.id) unless @notifications.empty? set_pagination_headers(next_path, prev_path)