first stab at online mode

This commit is contained in:
Nolan Lawson 2018-01-18 20:25:34 -08:00
parent 1c354817a6
commit 90762897db
10 changed files with 159 additions and 97 deletions

5
package-lock.json generated
View file

@ -3282,11 +3282,6 @@
} }
} }
}, },
"idb": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/idb/-/idb-2.0.4.tgz",
"integrity": "sha512-Nw4ykKrrVje6YODRiRm/k2ucFEQeoY+zrkszfOuzVmxx8yyBMtZh2KLaRCKk9r5GzhuF0QlNCVjBewP2n5OZ7Q=="
},
"ieee754": { "ieee754": {
"version": "1.1.8", "version": "1.1.8",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz",

View file

@ -23,7 +23,6 @@
"fg-loadcss": "^2.0.1", "fg-loadcss": "^2.0.1",
"font-awesome-svg-png": "^1.2.2", "font-awesome-svg-png": "^1.2.2",
"glob": "^7.1.2", "glob": "^7.1.2",
"idb": "^2.0.4",
"intersection-observer": "^0.5.0", "intersection-observer": "^0.5.0",
"intl-relativeformat": "^2.1.0", "intl-relativeformat": "^2.1.0",
"lodash": "^4.17.4", "lodash": "^4.17.4",

View file

@ -20,16 +20,17 @@
export default { export default {
oncreate() { oncreate() {
mark('onCreate Layout') mark('onCreate Layout')
let node = this.refs.node
this.observe('innerHeight', debounce(() => { this.observe('innerHeight', debounce(() => {
// respond to window resize events // respond to window resize events
this.store.set({ this.store.set({
offsetHeight: this.refs.node.offsetHeight offsetHeight: node.offsetHeight
}) })
}, RESIZE_EVENT_DELAY)) }, RESIZE_EVENT_DELAY))
this.store.set({ this.store.set({
scrollTop: this.refs.node.scrollTop, scrollTop: node.scrollTop,
scrollHeight: this.refs.node.scrollHeight, scrollHeight: node.scrollHeight,
offsetHeight: this.refs.node.offsetHeight offsetHeight: node.offsetHeight
}) })
stop('onCreate Layout') stop('onCreate Layout')
}, },

View file

@ -1,3 +1,4 @@
<:Window bind:online />
<div class="timeline" role="feed" aria-label="{{label}}"> <div class="timeline" role="feed" aria-label="{{label}}">
<VirtualList component="{{StatusListItem}}" <VirtualList component="{{StatusListItem}}"
items="{{keyedStatuses}}" items="{{keyedStatuses}}"
@ -10,20 +11,32 @@
</style> </style>
<script> <script>
import { store } from '../_utils/store' import { store } from '../_utils/store'
import { getHomeTimeline } from '../_utils/mastodon/oauth' import { getHomeTimeline } from '../_utils/mastodon/timelines'
import StatusListItem from './StatusListItem.html' import StatusListItem from './StatusListItem.html'
import VirtualList from './VirtualList.html' import VirtualList from './VirtualList.html'
import { splice, push } from 'svelte-extras' import { splice, push } from 'svelte-extras'
import {
insertStatuses as insertStatusesIntoDatabase,
getTimelineAfter as getTimelineFromDatabaseAfter
} from '../_utils/database/statuses'
import { mergeStatuses } from '../_utils/statuses'
import { mark, stop } from '../_utils/marks' import { mark, stop } from '../_utils/marks'
const FETCH_LIMIT = 20
export default { export default {
async oncreate() { async oncreate() {
this.observe('online', e => console.log(e))
let instanceName = this.store.get('currentInstance') let instanceName = this.store.get('currentInstance')
let instanceData = this.store.get('currentInstanceData') let instanceData = this.store.get('currentInstanceData')
let statuses = await getHomeTimeline(instanceName, instanceData.access_token, null, 10) let online = this.get('online')
this.set({ let statuses = online ?
statuses: statuses, await getHomeTimeline(instanceName, instanceData.access_token, null, FETCH_LIMIT) :
}) await getTimelineFromDatabaseAfter(null, FETCH_LIMIT)
if (!online) {
insertStatusesIntoDatabase(statuses)
}
this.addStatuses(statuses)
}, },
data: () => ({ data: () => ({
target: 'home', target: 'home',
@ -55,16 +68,30 @@
let lastStatusId = this.get('lastStatusId') let lastStatusId = this.get('lastStatusId')
let instanceName = this.store.get('currentInstance') let instanceName = this.store.get('currentInstance')
let instanceData = this.store.get('currentInstanceData') let instanceData = this.store.get('currentInstanceData')
let newStatuses = await getHomeTimeline(instanceName, instanceData.access_token, lastStatusId, 10) let online = this.get('online')
let newStatuses = online ?
await getHomeTimeline(instanceName, instanceData.access_token, lastStatusId, FETCH_LIMIT) :
await getTimelineFromDatabaseAfter(lastStatusId, FETCH_LIMIT)
if (online) {
insertStatusesIntoDatabase(newStatuses)
}
let statuses = this.get('statuses') let statuses = this.get('statuses')
if (statuses) { if (statuses) {
this.addItems(newStatuses) this.addStatuses(newStatuses)
} }
this.set({ runningUpdate: false }) this.set({ runningUpdate: false })
stop('onScrollToBottom') stop('onScrollToBottom')
}, },
addItems(items) { addStatuses(newStatuses) {
this.splice('statuses', this.get('statuses').length, 0, ...items) if (process.env.NODE_ENV !== 'production') {
console.log('addStatuses()')
}
let statuses = this.get('statuses')
if (!statuses) {
return
}
let merged = mergeStatuses(statuses, newStatuses)
this.set({ statuses: merged })
} }
} }
} }

View file

@ -1,2 +0,0 @@
import keyval from './idb-keyval'

View file

@ -1,57 +0,0 @@
import idb from 'idb'
// copypasta'd from https://github.com/jakearchibald/idb#keyval-store
const dbPromise = idb.open('keyval-store', 1, upgradeDB => {
upgradeDB.createObjectStore('keyval')
})
const idbKeyval = {
get(key) {
return dbPromise.then(db => {
return db.transaction('keyval').objectStore('keyval').get(key)
})
},
set(key, val) {
return dbPromise.then(db => {
const tx = db.transaction('keyval', 'readwrite')
tx.objectStore('keyval').put(val, key)
return tx.complete
})
},
delete(key) {
return dbPromise.then(db => {
const tx = db.transaction('keyval', 'readwrite')
tx.objectStore('keyval').delete(key)
return tx.complete
})
},
clear() {
return dbPromise.then(db => {
const tx = db.transaction('keyval', 'readwrite')
tx.objectStore('keyval').clear()
return tx.complete
})
},
keys() {
return dbPromise.then(db => {
const tx = db.transaction('keyval')
const keys = []
const store = tx.objectStore('keyval')
// This would be store.getAllKeys(), but it isn't supported by Edge or Safari.
// openKeyCursor isn't supported by Safari, so we fall back
const iterate = store.iterateKeyCursor || store.iterateCursor
iterate.call(store, cursor => {
if (!cursor) {
return
}
keys.push(cursor.key)
cursor.continue()
})
return tx.complete.then(() => keys)
})
}
}
export default idbKeyval

View file

@ -0,0 +1,65 @@
import cloneDeep from 'lodash/cloneDeep'
const STORE = 'statuses'
const dbPromise = new Promise((resolve, reject) => {
let req = indexedDB.open(STORE, 1)
req.onerror = reject
req.onblocked = () => {
console.log('idb blocked')
}
req.onupgradeneeded = () => {
let db = req.result;
let oStore = db.createObjectStore(STORE, {
keyPath: 'id'
})
oStore.createIndex('created_at', 'created_at')
oStore.createIndex('pinafore_id_as_negative_big_int', 'pinafore_id_as_negative_big_int')
oStore.createIndex('pinafore_id_as_big_int', 'pinafore_id_as_big_int')
}
req.onsuccess = () => resolve(req.result)
})
function transformStatusForStorage(status) {
status = cloneDeep(status)
status.pinafore_id_as_big_int = parseInt(status, 10)
status.pinafore_id_as_negative_big_int = -parseInt(status, 10)
status.pinafore_stale = true
return status
}
export async function getTimelineAfter(max_id = null, limit = 20) {
const db = await dbPromise
return await new Promise((resolve, reject) => {
const tx = db.transaction(STORE, 'readonly')
const store = tx.objectStore(STORE)
const index = store.index('pinafore_id_as_negative_big_int')
let res
let sinceAsNegativeBigInt = max_id === null ? null : -parseInt(max_id, 10)
let query = sinceAsNegativeBigInt === null ? null : IDBKeyRange.lowerBound(sinceAsNegativeBigInt, false)
index.getAll(query, limit).onsuccess = (e) => {
console.log('done calling getAll()')
res = e.target.result
}
tx.oncomplete = () => {
console.log('complete')
resolve(res)
}
tx.onerror = reject
})
}
export async function insertStatuses(statuses) {
const db = await dbPromise
return await new Promise((resolve, reject) => {
const tx = db.transaction(STORE, 'readwrite')
const store = tx.objectStore(STORE)
for (let status of statuses) {
store.put(transformStatusForStorage(status))
}
tx.oncomplete = resolve
tx.onerror = reject
})
}

View file

@ -33,22 +33,3 @@ export function getAccessTokenFromAuthCode(instanceName, clientId, clientSecret,
code: code code: code
}) })
} }
export function getHomeTimeline(instanceName, accessToken, since, limit) {
let url = `https://${instanceName}/api/v1/timelines/home`
let params = {}
if (since) {
params[since] = since
}
if (limit) {
params[limit] = limit
}
url += '?' + paramsString(params)
return get(url, {
'Authorization': `Bearer ${accessToken}`
})
}

View file

@ -0,0 +1,20 @@
import { get, paramsString } from '../ajax'
export function getHomeTimeline(instanceName, accessToken, maxId, since) {
let url = `https://${instanceName}/api/v1/timelines/home`
let params = {}
if (since) {
params.since = since
}
if (maxId) {
params.max_id = maxId
}
url += '?' + paramsString(params)
return get(url, {
'Authorization': `Bearer ${accessToken}`
})
}

33
routes/_utils/statuses.js Normal file
View file

@ -0,0 +1,33 @@
// Merge two lists of statuses for the same timeline, e.g. one from IDB
// and another from the network. In case of duplicates, prefer the fresh.
export function mergeStatuses(leftStatuses, rightStatuses) {
let leftIndex = 0
let rightIndex = 0
let merged = []
while (leftIndex < leftStatuses.length || rightIndex < rightStatuses.length) {
if (leftIndex === leftStatuses.length) {
merged.push(rightStatuses[rightIndex])
rightIndex++
continue
}
if (rightIndex === rightStatuses.length) {
merged.push(leftStatuses[leftIndex])
leftIndex++
continue
}
let left = leftStatuses[leftIndex]
let right = rightStatuses[rightIndex]
if (right.id === left.id) {
merged.push(right.pinafore_stale ? left : right)
rightIndex++
leftIndex++
} else if (parseInt(right.id, 10) > parseInt(left.id, 10)) {
merged.push(right)
rightIndex++
} else {
merged.push(left)
leftIndex++
}
}
return merged
}