diff --git a/app/javascript/flavours/glitch/actions/markers.js b/app/javascript/flavours/glitch/actions/markers.js index 7ffab404d..b237d82ec 100644 --- a/app/javascript/flavours/glitch/actions/markers.js +++ b/app/javascript/flavours/glitch/actions/markers.js @@ -1,38 +1,86 @@ import api from 'flavours/glitch/util/api'; +import { debounce } from 'lodash'; +import compareId from 'flavours/glitch/util/compare_id'; +import { showAlertForError } from './alerts'; export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS'; export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL'; +export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS'; -export const submitMarkers = () => (dispatch, getState) => { +export const synchronouslySubmitMarkers = () => (dispatch, getState) => { const accessToken = getState().getIn(['meta', 'access_token'], ''); - const params = {}; + const params = _buildParams(getState()); - const lastHomeId = getState().getIn(['timelines', 'home', 'items', 0]); - const lastNotificationId = getState().getIn(['notifications', 'lastReadId']); + if (Object.keys(params).length === 0) { + return; + } + + if (window.fetch) { + fetch('/api/v1/markers', { + keepalive: true, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}`, + }, + body: JSON.stringify(params), + }); + } else { + const client = new XMLHttpRequest(); + + client.open('POST', '/api/v1/markers', false); + client.setRequestHeader('Content-Type', 'application/json'); + client.setRequestHeader('Authorization', `Bearer ${accessToken}`); + client.SUBMIT(JSON.stringify(params)); + } +}; + +const _buildParams = (state) => { + const params = {}; - if (lastHomeId) { + const lastHomeId = state.getIn(['timelines', 'home', 'items', 0]); + const lastNotificationId = state.getIn(['notifications', 'lastReadId']); + + if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) { params.home = { last_read_id: lastHomeId, }; } - if (lastNotificationId && lastNotificationId !== '0') { + if (lastNotificationId && lastNotificationId !== '0' && compareId(lastNotificationId, state.getIn(['markers', 'notifications'])) > 0) { params.notifications = { last_read_id: lastNotificationId, }; } + return params; +}; + +const debouncedSubmitMarkers = debounce((dispatch, getState) => { + const params = _buildParams(getState()); + if (Object.keys(params).length === 0) { return; } - const client = new XMLHttpRequest(); + api().post('/api/v1/markers', params).then(() => { + dispatch(submitMarkersSuccess(params)); + }).catch(error => { + dispatch(showAlertForError(error)); + }); +}, 300000, { leading: true, trailing: true }); + +export function submitMarkersSuccess({ home, notifications }) { + return { + type: MARKERS_SUBMIT_SUCCESS, + home: (home || {}).last_read_id, + notifications: (notifications || {}).last_read_id, + }; +}; - client.open('POST', '/api/v1/markers', false); - client.setRequestHeader('Content-Type', 'application/json'); - client.setRequestHeader('Authorization', `Bearer ${accessToken}`); - client.send(JSON.stringify(params)); +export function submitMarkers() { + return (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); }; export const fetchMarkers = () => (dispatch, getState) => { diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index b3de7b5bf..ceb1e6df6 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -7,6 +7,7 @@ import { importFetchedStatus, importFetchedStatuses, } from './importer'; +import { submitMarkers } from './markers'; import { saveSettings } from './settings'; import { defineMessages } from 'react-intl'; import { List as ImmutableList } from 'immutable'; @@ -81,6 +82,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) { filtered = regex && regex.test(searchIndex); } + dispatch(submitMarkers()); + if (showInColumn) { dispatch(importFetchedAccount(notification.account)); @@ -168,6 +171,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) { dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems)); fetchRelatedRelationships(dispatch, response.data); + dispatch(submitMarkers()); }).catch(error => { dispatch(expandNotificationsFail(error, isLoadingMore)); }).finally(() => { diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index 46b605e04..9a7f62a08 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -1,4 +1,5 @@ import { importFetchedStatus, importFetchedStatuses } from './importer'; +import { submitMarkers } from './markers'; import api, { getLinks } from 'flavours/glitch/util/api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import compareId from 'flavours/glitch/util/compare_id'; @@ -49,6 +50,10 @@ export function updateTimeline(timeline, status, accept) { usePendingItems: preferPendingItems, filtered }); + + if (timeline === 'home') { + dispatch(submitMarkers()); + } }; }; @@ -112,6 +117,10 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { dispatch(importFetchedStatuses(response.data)); dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); + + if (timelineId === 'home') { + dispatch(submitMarkers()); + } }).catch(error => { dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); }).finally(() => { diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 9f9ef561a..f8f6cff88 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -12,7 +12,7 @@ import { expandHomeTimeline } from 'flavours/glitch/actions/timelines'; import { expandNotifications, notificationsSetVisibility } from 'flavours/glitch/actions/notifications'; import { fetchFilters } from 'flavours/glitch/actions/filters'; import { clearHeight } from 'flavours/glitch/actions/height_cache'; -import { submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers'; +import { synchronouslySubmitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers'; import { WrappedSwitch, WrappedRoute } from 'flavours/glitch/util/react_router_helpers'; import UploadArea from './components/upload_area'; import PermaLink from 'flavours/glitch/components/permalink'; @@ -267,7 +267,7 @@ class UI extends React.Component { handleBeforeUnload = (e) => { const { intl, dispatch, hasComposingText, hasMediaAttachments } = this.props; - dispatch(submitMarkers()); + dispatch(synchronouslySubmitMarkers()); if (hasComposingText || hasMediaAttachments) { // Setting returnValue to any string causes confirmation dialog. diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js index 586b84749..852abe9dd 100644 --- a/app/javascript/flavours/glitch/reducers/index.js +++ b/app/javascript/flavours/glitch/reducers/index.js @@ -36,6 +36,7 @@ import polls from './polls'; import identity_proofs from './identity_proofs'; import trends from './trends'; import announcements from './announcements'; +import markers from './markers'; const reducers = { announcements, @@ -75,6 +76,7 @@ const reducers = { pinnedAccountsEditor, polls, trends, + markers, }; export default combineReducers(reducers); diff --git a/app/javascript/flavours/glitch/reducers/markers.js b/app/javascript/flavours/glitch/reducers/markers.js new file mode 100644 index 000000000..2e67be82e --- /dev/null +++ b/app/javascript/flavours/glitch/reducers/markers.js @@ -0,0 +1,25 @@ +import { + MARKERS_SUBMIT_SUCCESS, +} from '../actions/notifications'; + +const initialState = ImmutableMap({ + home: '0', + notifications: '0', +}); + +import { Map as ImmutableMap } from 'immutable'; + +export default function markers(state = initialState, action) { + switch(action.type) { + case MARKERS_SUBMIT_SUCCESS: + if (action.home) { + state = state.set('home', action.home); + } + if (action.notifications) { + state = state.set('notifications', action.notifications); + } + return state; + default: + return state; + } +};