Allow clients to fetch notifications made while they were offline (#6886)
This commit is contained in:
		
					parent
					
						
							
								9a1a55ce52
							
						
					
				
			
			
				commit
				
					
						cbf97c03bb
					
				
			
		
					 5 changed files with 82 additions and 114 deletions
				
			
		|  | @ -1,5 +1,4 @@ | |||
| import api, { getLinks } from '../api'; | ||||
| import { List as ImmutableList } from 'immutable'; | ||||
| import IntlMessageFormat from 'intl-messageformat'; | ||||
| import { fetchRelationships } from './accounts'; | ||||
| import { | ||||
|  | @ -12,10 +11,6 @@ import { defineMessages } from 'react-intl'; | |||
| 
 | ||||
| export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; | ||||
| 
 | ||||
| export const NOTIFICATIONS_REFRESH_REQUEST = 'NOTIFICATIONS_REFRESH_REQUEST'; | ||||
| export const NOTIFICATIONS_REFRESH_SUCCESS = 'NOTIFICATIONS_REFRESH_SUCCESS'; | ||||
| export const NOTIFICATIONS_REFRESH_FAIL    = 'NOTIFICATIONS_REFRESH_FAIL'; | ||||
| 
 | ||||
| export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST'; | ||||
| export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS'; | ||||
| export const NOTIFICATIONS_EXPAND_FAIL    = 'NOTIFICATIONS_EXPAND_FAIL'; | ||||
|  | @ -74,74 +69,14 @@ export function updateNotifications(notification, intlMessages, intlLocale) { | |||
| 
 | ||||
| const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); | ||||
| 
 | ||||
| export function refreshNotifications() { | ||||
| export function expandNotifications({ maxId } = {}) { | ||||
|   return (dispatch, getState) => { | ||||
|     const params = {}; | ||||
|     const ids    = getState().getIn(['notifications', 'items']); | ||||
| 
 | ||||
|     let skipLoading = false; | ||||
| 
 | ||||
|     if (ids.size > 0) { | ||||
|       params.since_id = ids.first().get('id'); | ||||
|     } | ||||
| 
 | ||||
|     if (getState().getIn(['notifications', 'loaded'])) { | ||||
|       skipLoading = true; | ||||
|     } | ||||
| 
 | ||||
|     params.exclude_types = excludeTypesFromSettings(getState()); | ||||
| 
 | ||||
|     dispatch(refreshNotificationsRequest(skipLoading)); | ||||
| 
 | ||||
|     api(getState).get('/api/v1/notifications', { params }).then(response => { | ||||
|       const next = getLinks(response).refs.find(link => link.rel === 'next'); | ||||
| 
 | ||||
|       dispatch(importFetchedAccounts(response.data.map(item => item.account))); | ||||
|       dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); | ||||
| 
 | ||||
|       dispatch(refreshNotificationsSuccess(response.data, skipLoading, next ? next.uri : null)); | ||||
|       fetchRelatedRelationships(dispatch, response.data); | ||||
|     }).catch(error => { | ||||
|       dispatch(refreshNotificationsFail(error, skipLoading)); | ||||
|     }); | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function refreshNotificationsRequest(skipLoading) { | ||||
|   return { | ||||
|     type: NOTIFICATIONS_REFRESH_REQUEST, | ||||
|     skipLoading, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function refreshNotificationsSuccess(notifications, skipLoading, next) { | ||||
|   return { | ||||
|     type: NOTIFICATIONS_REFRESH_SUCCESS, | ||||
|     notifications, | ||||
|     skipLoading, | ||||
|     next, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function refreshNotificationsFail(error, skipLoading) { | ||||
|   return { | ||||
|     type: NOTIFICATIONS_REFRESH_FAIL, | ||||
|     error, | ||||
|     skipLoading, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function expandNotifications() { | ||||
|   return (dispatch, getState) => { | ||||
|     const items  = getState().getIn(['notifications', 'items'], ImmutableList()); | ||||
| 
 | ||||
|     if (getState().getIn(['notifications', 'isLoading']) || items.size === 0) { | ||||
|     if (getState().getIn(['notifications', 'isLoading'])) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const params = { | ||||
|       max_id: items.last().get('id'), | ||||
|       limit: 20, | ||||
|       max_id: maxId, | ||||
|       exclude_types: excludeTypesFromSettings(getState()), | ||||
|     }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ import { | |||
|   expandHomeTimeline, | ||||
|   disconnectTimeline, | ||||
| } from './timelines'; | ||||
| import { updateNotifications, refreshNotifications } from './notifications'; | ||||
| import { updateNotifications, expandNotifications } from './notifications'; | ||||
| import { getLocale } from '../locales'; | ||||
| 
 | ||||
| const { messages } = getLocale(); | ||||
|  | @ -38,7 +38,7 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null) | |||
| 
 | ||||
| function refreshHomeTimelineAndNotification (dispatch) { | ||||
|   dispatch(expandHomeTimeline()); | ||||
|   dispatch(refreshNotifications()); | ||||
|   dispatch(expandNotifications()); | ||||
| } | ||||
| 
 | ||||
| export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ import { createSelector } from 'reselect'; | |||
| import { List as ImmutableList } from 'immutable'; | ||||
| import { debounce } from 'lodash'; | ||||
| import ScrollableList from '../../components/scrollable_list'; | ||||
| import LoadMore from '../../components/load_more'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   title: { id: 'column.notifications', defaultMessage: 'Notifications' }, | ||||
|  | @ -21,13 +22,31 @@ const messages = defineMessages({ | |||
| const getNotifications = createSelector([ | ||||
|   state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()), | ||||
|   state => state.getIn(['notifications', 'items']), | ||||
| ], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type')))); | ||||
| ], (excludedTypes, notifications) => notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')))); | ||||
| 
 | ||||
| class LoadGap extends React.PureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     disabled: PropTypes.bool, | ||||
|     maxId: PropTypes.string, | ||||
|     onClick: PropTypes.func.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   handleClick = () => { | ||||
|     this.props.onClick(this.props.maxId); | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     return <LoadMore onClick={this.handleClick} disabled={this.props.disabled} />; | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| const mapStateToProps = state => ({ | ||||
|   notifications: getNotifications(state), | ||||
|   isLoading: state.getIn(['notifications', 'isLoading'], true), | ||||
|   isUnread: state.getIn(['notifications', 'unread']) > 0, | ||||
|   hasMore: !!state.getIn(['notifications', 'next']), | ||||
|   hasMore: state.getIn(['notifications', 'hasMore']), | ||||
| }); | ||||
| 
 | ||||
| @connect(mapStateToProps) | ||||
|  | @ -51,14 +70,19 @@ export default class Notifications extends React.PureComponent { | |||
|   }; | ||||
| 
 | ||||
|   componentWillUnmount () { | ||||
|     this.handleLoadMore.cancel(); | ||||
|     this.handleLoadOlder.cancel(); | ||||
|     this.handleScrollToTop.cancel(); | ||||
|     this.handleScroll.cancel(); | ||||
|     this.props.dispatch(scrollTopNotifications(false)); | ||||
|   } | ||||
| 
 | ||||
|   handleLoadMore = debounce(() => { | ||||
|     this.props.dispatch(expandNotifications()); | ||||
|   handleLoadGap = (maxId) => { | ||||
|     this.props.dispatch(expandNotifications({ maxId })); | ||||
|   }; | ||||
| 
 | ||||
|   handleLoadOlder = debounce(() => { | ||||
|     const last = this.props.notifications.last(); | ||||
|     this.props.dispatch(expandNotifications({ maxId: last && last.get('id') })); | ||||
|   }, 300, { leading: true }); | ||||
| 
 | ||||
|   handleScrollToTop = debounce(() => { | ||||
|  | @ -93,12 +117,12 @@ export default class Notifications extends React.PureComponent { | |||
|   } | ||||
| 
 | ||||
|   handleMoveUp = id => { | ||||
|     const elementIndex = this.props.notifications.findIndex(item => item.get('id') === id) - 1; | ||||
|     const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1; | ||||
|     this._selectChild(elementIndex); | ||||
|   } | ||||
| 
 | ||||
|   handleMoveDown = id => { | ||||
|     const elementIndex = this.props.notifications.findIndex(item => item.get('id') === id) + 1; | ||||
|     const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1; | ||||
|     this._selectChild(elementIndex); | ||||
|   } | ||||
| 
 | ||||
|  | @ -120,7 +144,14 @@ export default class Notifications extends React.PureComponent { | |||
|     if (isLoading && this.scrollableContent) { | ||||
|       scrollableContent = this.scrollableContent; | ||||
|     } else if (notifications.size > 0 || hasMore) { | ||||
|       scrollableContent = notifications.map((item) => ( | ||||
|       scrollableContent = notifications.map((item, index) => item === null ? ( | ||||
|         <LoadGap | ||||
|           key={'gap:' + notifications.getIn([index + 1, 'id'])} | ||||
|           disabled={isLoading} | ||||
|           maxId={index > 0 ? notifications.getIn([index - 1, 'id']) : null} | ||||
|           onClick={this.handleLoadGap} | ||||
|         /> | ||||
|       ) : ( | ||||
|         <NotificationContainer | ||||
|           key={item.get('id')} | ||||
|           notification={item} | ||||
|  | @ -142,7 +173,7 @@ export default class Notifications extends React.PureComponent { | |||
|         isLoading={isLoading} | ||||
|         hasMore={hasMore} | ||||
|         emptyMessage={emptyMessage} | ||||
|         onLoadMore={this.handleLoadMore} | ||||
|         onLoadMore={this.handleLoadOlder} | ||||
|         onScrollToTop={this.handleScrollToTop} | ||||
|         onScroll={this.handleScroll} | ||||
|         shouldUpdateScroll={shouldUpdateScroll} | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ import { isMobile } from '../../is_mobile'; | |||
| import { debounce } from 'lodash'; | ||||
| import { uploadCompose, resetCompose } from '../../actions/compose'; | ||||
| import { expandHomeTimeline } from '../../actions/timelines'; | ||||
| import { refreshNotifications } from '../../actions/notifications'; | ||||
| import { expandNotifications } from '../../actions/notifications'; | ||||
| import { clearHeight } from '../../actions/height_cache'; | ||||
| import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; | ||||
| import UploadArea from './components/upload_area'; | ||||
|  | @ -285,7 +285,7 @@ export default class UI extends React.PureComponent { | |||
|     } | ||||
| 
 | ||||
|     this.props.dispatch(expandHomeTimeline()); | ||||
|     this.props.dispatch(refreshNotifications()); | ||||
|     this.props.dispatch(expandNotifications()); | ||||
|   } | ||||
| 
 | ||||
|   componentDidMount () { | ||||
|  |  | |||
|  | @ -1,10 +1,7 @@ | |||
| import { | ||||
|   NOTIFICATIONS_UPDATE, | ||||
|   NOTIFICATIONS_REFRESH_SUCCESS, | ||||
|   NOTIFICATIONS_EXPAND_SUCCESS, | ||||
|   NOTIFICATIONS_REFRESH_REQUEST, | ||||
|   NOTIFICATIONS_EXPAND_REQUEST, | ||||
|   NOTIFICATIONS_REFRESH_FAIL, | ||||
|   NOTIFICATIONS_EXPAND_FAIL, | ||||
|   NOTIFICATIONS_CLEAR, | ||||
|   NOTIFICATIONS_SCROLL_TOP, | ||||
|  | @ -13,16 +10,15 @@ import { | |||
|   ACCOUNT_BLOCK_SUCCESS, | ||||
|   ACCOUNT_MUTE_SUCCESS, | ||||
| } from '../actions/accounts'; | ||||
| import { TIMELINE_DELETE } from '../actions/timelines'; | ||||
| import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines'; | ||||
| import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; | ||||
| 
 | ||||
| const initialState = ImmutableMap({ | ||||
|   items: ImmutableList(), | ||||
|   next: null, | ||||
|   hasMore: true, | ||||
|   top: true, | ||||
|   unread: 0, | ||||
|   loaded: false, | ||||
|   isLoading: true, | ||||
|   isLoading: false, | ||||
| }); | ||||
| 
 | ||||
| const notificationToMap = notification => ImmutableMap({ | ||||
|  | @ -48,35 +44,41 @@ const normalizeNotification = (state, notification) => { | |||
|   }); | ||||
| }; | ||||
| 
 | ||||
| const normalizeNotifications = (state, notifications, next) => { | ||||
|   let newItems    = ImmutableList(); | ||||
|   const loaded = state.get('loaded'); | ||||
| const newer = (m, n) => { | ||||
|   const mId = m.get('id'); | ||||
|   const nId = n.get('id'); | ||||
| 
 | ||||
|   notifications.forEach((n, i) => { | ||||
|     newItems = newItems.set(i, notificationToMap(n)); | ||||
|   }); | ||||
| 
 | ||||
|   if (state.get('next') === null) { | ||||
|     state = state.set('next', next); | ||||
|   } | ||||
| 
 | ||||
|   return state | ||||
|     .update('items', oldItems => loaded ? newItems.concat(oldItems) : oldItems.concat(newItems)) | ||||
|     .set('loaded', true) | ||||
|     .set('isLoading', false); | ||||
|   return mId.length === nId.length ? mId > nId : mId.length > nId.length; | ||||
| }; | ||||
| 
 | ||||
| const appendNormalizedNotifications = (state, notifications, next) => { | ||||
| const expandNormalizedNotifications = (state, notifications, next) => { | ||||
|   let items = ImmutableList(); | ||||
| 
 | ||||
|   notifications.forEach((n, i) => { | ||||
|     items = items.set(i, notificationToMap(n)); | ||||
|   }); | ||||
| 
 | ||||
|   return state | ||||
|     .update('items', list => list.concat(items)) | ||||
|     .set('next', next) | ||||
|     .set('isLoading', false); | ||||
|   return state.withMutations(mutable => { | ||||
|     if (!items.isEmpty()) { | ||||
|       mutable.update('items', list => { | ||||
|         const lastIndex = 1 + list.findLastIndex( | ||||
|           item => item !== null && (newer(item, items.last()) || item.get('id') === items.last().get('id')) | ||||
|         ); | ||||
| 
 | ||||
|         const firstIndex = 1 + list.take(lastIndex).findLastIndex( | ||||
|           item => item !== null && newer(item, items.first()) | ||||
|         ); | ||||
| 
 | ||||
|         return list.take(firstIndex).concat(items, list.skip(lastIndex)); | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     if (!next) { | ||||
|       mutable.set('hasMore', true); | ||||
|     } | ||||
| 
 | ||||
|     mutable.set('isLoading', false); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| const filterNotifications = (state, relationship) => { | ||||
|  | @ -97,27 +99,27 @@ const deleteByStatus = (state, statusId) => { | |||
| 
 | ||||
| export default function notifications(state = initialState, action) { | ||||
|   switch(action.type) { | ||||
|   case NOTIFICATIONS_REFRESH_REQUEST: | ||||
|   case NOTIFICATIONS_EXPAND_REQUEST: | ||||
|     return state.set('isLoading', true); | ||||
|   case NOTIFICATIONS_REFRESH_FAIL: | ||||
|   case NOTIFICATIONS_EXPAND_FAIL: | ||||
|     return state.set('isLoading', false); | ||||
|   case NOTIFICATIONS_SCROLL_TOP: | ||||
|     return updateTop(state, action.top); | ||||
|   case NOTIFICATIONS_UPDATE: | ||||
|     return normalizeNotification(state, action.notification); | ||||
|   case NOTIFICATIONS_REFRESH_SUCCESS: | ||||
|     return normalizeNotifications(state, action.notifications, action.next); | ||||
|   case NOTIFICATIONS_EXPAND_SUCCESS: | ||||
|     return appendNormalizedNotifications(state, action.notifications, action.next); | ||||
|     return expandNormalizedNotifications(state, action.notifications, action.next); | ||||
|   case ACCOUNT_BLOCK_SUCCESS: | ||||
|   case ACCOUNT_MUTE_SUCCESS: | ||||
|     return filterNotifications(state, action.relationship); | ||||
|   case NOTIFICATIONS_CLEAR: | ||||
|     return state.set('items', ImmutableList()).set('next', null); | ||||
|     return state.set('items', ImmutableList()).set('hasMore', false); | ||||
|   case TIMELINE_DELETE: | ||||
|     return deleteByStatus(state, action.id); | ||||
|   case TIMELINE_DISCONNECT: | ||||
|     return action.timeline === 'home' ? | ||||
|       state.update('items', items => items.first() ? items.unshift(null) : items) : | ||||
|       state; | ||||
|   default: | ||||
|     return state; | ||||
|   } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue