From 5aff2a6957e861162d67c4610277e9bb2a6f8186 Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 29 May 2020 16:14:16 +0200 Subject: [PATCH] Fix timeline markers not working on Chrome (#13887) * Periodically save timeline markers This saves timeline markers immediately upon message arrival, but not more than once every 5 minutes. This does not change how the markers are saved on closing the window, except that it avoids submitting them if there is no need for it. * Use the Fetch API when possible instead of XHR on window unload --- app/javascript/mastodon/actions/markers.js | 95 ++++++++++++++----- .../mastodon/actions/notifications.js | 4 + app/javascript/mastodon/actions/timelines.js | 9 ++ app/javascript/mastodon/features/ui/index.js | 4 +- app/javascript/mastodon/reducers/index.js | 2 + app/javascript/mastodon/reducers/markers.js | 25 +++++ 6 files changed, 115 insertions(+), 24 deletions(-) create mode 100644 app/javascript/mastodon/reducers/markers.js diff --git a/app/javascript/mastodon/actions/markers.js b/app/javascript/mastodon/actions/markers.js index c3a5fe86f1..9cc061a876 100644 --- a/app/javascript/mastodon/actions/markers.js +++ b/app/javascript/mastodon/actions/markers.js @@ -1,30 +1,81 @@ -export const submitMarkers = () => (dispatch, getState) => { +import api from '../api'; +import { debounce } from 'lodash'; +import compareId from '../compare_id'; +import { showAlertForError } from './alerts'; + +export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS'; + +export const synchronouslySubmitMarkers = () => (dispatch, getState) => { const accessToken = getState().getIn(['meta', 'access_token'], ''); - const params = {}; - - const lastHomeId = getState().getIn(['timelines', 'home', 'items', 0]); - const lastNotificationId = getState().getIn(['notifications', 'items', 0, 'id']); - - if (lastHomeId) { - params.home = { - last_read_id: lastHomeId, - }; - } - - if (lastNotificationId) { - params.notifications = { - last_read_id: lastNotificationId, - }; - } + const params = _buildParams(getState()); if (Object.keys(params).length === 0) { return; } - const client = new XMLHttpRequest(); + 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.send(JSON.stringify(params)); + 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 = {}; + + const lastHomeId = state.getIn(['timelines', 'home', 'items', 0]); + const lastNotificationId = state.getIn(['notifications', 'items', 0, 'id']); + + if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) { + params.home = { + last_read_id: lastHomeId, + }; + } + + if (lastNotificationId && 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; + } + + 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, + }; +}; + +export function submitMarkers() { + return (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); }; diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 8a066b896a..a26844f848 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/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'; @@ -70,6 +71,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) { filtered = regex && regex.test(searchIndex); } + dispatch(submitMarkers()); + if (showInColumn) { dispatch(importFetchedAccount(notification.account)); @@ -157,6 +160,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/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 00a7a67892..de1725acf6 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -1,4 +1,5 @@ import { importFetchedStatus, importFetchedStatuses } from './importer'; +import { submitMarkers } from './markers'; import api, { getLinks } from 'mastodon/api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import compareId from 'mastodon/compare_id'; @@ -36,6 +37,10 @@ export function updateTimeline(timeline, status, accept) { status, usePendingItems: preferPendingItems, }); + + if (timeline === 'home') { + dispatch(submitMarkers()); + } }; }; @@ -98,6 +103,10 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { const next = getLinks(response).refs.find(link => link.rel === 'next'); 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/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 957e807375..81ffad22ed 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -16,7 +16,7 @@ import { expandNotifications } from '../../actions/notifications'; import { fetchFilters } from '../../actions/filters'; import { clearHeight } from '../../actions/height_cache'; import { focusApp, unfocusApp } from 'mastodon/actions/app'; -import { submitMarkers } from 'mastodon/actions/markers'; +import { synchronouslySubmitMarkers } from 'mastodon/actions/markers'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import UploadArea from './components/upload_area'; import ColumnsAreaContainer from './containers/columns_area_container'; @@ -251,7 +251,7 @@ class UI extends React.PureComponent { handleBeforeUnload = e => { const { intl, dispatch, isComposing, hasComposingText, hasMediaAttachments } = this.props; - dispatch(submitMarkers()); + dispatch(synchronouslySubmitMarkers()); if (isComposing && (hasComposingText || hasMediaAttachments)) { // Setting returnValue to any string causes confirmation dialog. diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index b9817cd384..3823bb05e0 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -35,6 +35,7 @@ import identity_proofs from './identity_proofs'; import trends from './trends'; import missed_updates from './missed_updates'; import announcements from './announcements'; +import markers from './markers'; const reducers = { announcements, @@ -73,6 +74,7 @@ const reducers = { polls, trends, missed_updates, + markers, }; export default combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/markers.js b/app/javascript/mastodon/reducers/markers.js new file mode 100644 index 0000000000..2e67be82e7 --- /dev/null +++ b/app/javascript/mastodon/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; + } +};