fix: Fix favorites, fix #850

This commit fixes invalid assumption that all timelines are sorted by status id.
Some, like favorites or bookmarks are sorted by private server id. To correctly
paginate we must use the Link header.

To work around the issue, offline for favorites was effectively disabled.
Statuses are still inserted into the database but we can't reproduce correct
timeline order.
This commit is contained in:
charlag 2020-06-13 12:07:27 +02:00 committed by Nolan Lawson
parent 8bbe372fda
commit 5e7c8003db
6 changed files with 12301 additions and 9 deletions

12248
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -116,6 +116,7 @@
"assert": "^2.0.0", "assert": "^2.0.0",
"eslint-plugin-html": "^6.0.0", "eslint-plugin-html": "^6.0.0",
"fake-indexeddb": "^3.0.0", "fake-indexeddb": "^3.0.0",
"http-link-header": "^1.0.2",
"mocha": "^7.1.0", "mocha": "^7.1.0",
"now": "^18.0.0", "now": "^18.0.0",
"standard": "^14.3.1", "standard": "^14.3.1",

View file

@ -14,6 +14,7 @@ import uniqBy from 'lodash-es/uniqBy'
import { addStatusesOrNotifications } from './addStatusOrNotification' import { addStatusesOrNotifications } from './addStatusOrNotification'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask' import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import { sortItemSummariesForThread } from '../_utils/sortItemSummariesForThread' import { sortItemSummariesForThread } from '../_utils/sortItemSummariesForThread'
import LinkHeader from 'http-link-header'
const byId = _ => _.id const byId = _ => _.id
@ -90,12 +91,44 @@ async function fetchTimelineItemsFromNetwork (instanceName, accessToken, timelin
if (timelineName.startsWith('status/')) { // special case - this is a list of descendents and ancestors if (timelineName.startsWith('status/')) { // special case - this is a list of descendents and ancestors
return fetchThreadFromNetwork(instanceName, accessToken, timelineName) return fetchThreadFromNetwork(instanceName, accessToken, timelineName)
} else { // normal timeline } else { // normal timeline
return getTimeline(instanceName, accessToken, timelineName, lastTimelineItemId, null, TIMELINE_BATCH_SIZE) const { items } = await getTimeline(instanceName, accessToken, timelineName, lastTimelineItemId, null, TIMELINE_BATCH_SIZE)
return items
}
}
async function addPagedTimelineItems (instanceName, timelineName, items) {
console.log('addPagedTimelineItems, length:', items.length)
mark('addPagedTimelineItemSummaries')
const newSummaries = items.map(timelineItemToSummary)
addPagedTimelineItemSummaries(instanceName, timelineName, newSummaries)
stop('addPagedTimelineItemSummaries')
}
export async function addPagedTimelineItemSummaries (instanceName, timelineName, newSummaries) {
const oldSummaries = store.getForTimeline(instanceName, timelineName, 'timelineItemSummaries') || []
const mergedSummaries = uniqBy(concat(oldSummaries, newSummaries), byId)
if (!isEqual(oldSummaries, mergedSummaries)) {
store.setForTimeline(instanceName, timelineName, { timelineItemSummaries: mergedSummaries })
} }
} }
async function fetchTimelineItems (instanceName, accessToken, timelineName, lastTimelineItemId, online) { async function fetchPagedItems (instanceName, accessToken, timelineName) {
const { timelineNextPageId } = store.get()
console.log('saved timelineNextPageId', timelineNextPageId)
const { items, headers } = await getTimeline(instanceName, accessToken, timelineName, timelineNextPageId, null, TIMELINE_BATCH_SIZE)
const linkHeader = headers.get('Link')
const next = LinkHeader.parse(linkHeader).rel('next')[0]
const nextId = next && next.uri && (new URL(next.uri)).searchParams.get('max_id')
console.log('new timelineNextPageId', nextId)
store.setForTimeline(instanceName, timelineName, { timelineNextPageId: nextId })
await storeFreshTimelineItemsInDatabase(instanceName, timelineName, items)
await addPagedTimelineItems(instanceName, timelineName, items)
}
async function fetchTimelineItems (instanceName, accessToken, timelineName, online) {
mark('fetchTimelineItems') mark('fetchTimelineItems')
const { lastTimelineItemId } = store.get()
let items let items
let stale = false let stale = false
if (!online) { if (!online) {
@ -146,12 +179,15 @@ async function fetchTimelineItemsAndPossiblyFallBack () {
currentTimeline, currentTimeline,
currentInstance, currentInstance,
accessToken, accessToken,
lastTimelineItemId,
online online
} = store.get() } = store.get()
const { items, stale } = await fetchTimelineItems(currentInstance, accessToken, currentTimeline, lastTimelineItemId, online) if (currentTimeline === 'favorites') {
await fetchPagedItems(currentInstance, accessToken, currentTimeline)
} else {
const { items, stale } = await fetchTimelineItems(currentInstance, accessToken, currentTimeline, online)
addTimelineItems(currentInstance, currentTimeline, items, stale) addTimelineItems(currentInstance, currentTimeline, items, stale)
}
stop('fetchTimelineItemsAndPossiblyFallBack') stop('fetchTimelineItemsAndPossiblyFallBack')
} }

View file

@ -1,4 +1,4 @@
import { get, paramsString, DEFAULT_TIMEOUT } from '../_utils/ajax' import { getWithHeaders, paramsString, DEFAULT_TIMEOUT } from '../_utils/ajax'
import { auth, basename } from './utils' import { auth, basename } from './utils'
function getTimelineUrlPath (timeline) { function getTimelineUrlPath (timeline) {
@ -69,10 +69,10 @@ export async function getTimeline (instanceName, accessToken, timeline, maxId, s
url += '?' + paramsString(params) url += '?' + paramsString(params)
console.log('fetching url', url) console.log('fetching url', url)
const items = await get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT }) const { json: items, headers } = await getWithHeaders(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
if (timeline === 'direct') { if (timeline === 'direct') {
return items.map(item => item.last_status) return items.map(item => item.last_status)
} }
return items return { items, headers }
} }

View file

@ -56,6 +56,7 @@ export function timelineComputations (store) {
computeForTimeline(store, 'showHeader', false) computeForTimeline(store, 'showHeader', false)
computeForTimeline(store, 'shouldShowHeader', false) computeForTimeline(store, 'shouldShowHeader', false)
computeForTimeline(store, 'timelineItemSummariesAreStale', false) computeForTimeline(store, 'timelineItemSummariesAreStale', false)
computeForTimeline(store, 'timelineNextPageId', null)
store.compute('currentTimelineType', ['currentTimeline'], currentTimeline => ( store.compute('currentTimelineType', ['currentTimeline'], currentTimeline => (
currentTimeline && currentTimeline.split('/')[0]) currentTimeline && currentTimeline.split('/')[0])

View file

@ -30,7 +30,7 @@ async function throwErrorIfInvalidResponse (response) {
} }
const json = await response.json() const json = await response.json()
if (response.status >= 200 && response.status < 300) { if (response.status >= 200 && response.status < 300) {
return json return { json, headers: response.headers }
} }
if (json && json.error) { if (json && json.error) {
throw new Error(response.status + ': ' + json.error) throw new Error(response.status + ': ' + json.error)
@ -74,6 +74,12 @@ export async function patch (url, body, headers, options) {
} }
export async function get (url, headers, options) { export async function get (url, headers, options) {
const { json } = await _fetch(url, makeFetchOptions('GET', headers, options), options)
return json
}
/** @returns {json, headers} */
export async function getWithHeaders (url, headers, options) {
return _fetch(url, makeFetchOptions('GET', headers, options), options) return _fetch(url, makeFetchOptions('GET', headers, options), options)
} }