first stab at online mode
This commit is contained in:
parent
1c354817a6
commit
90762897db
5
package-lock.json
generated
5
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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')
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
import keyval from './idb-keyval'
|
|
||||||
|
|
|
@ -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
|
|
65
routes/_utils/database/statuses.js
Normal file
65
routes/_utils/database/statuses.js
Normal 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
|
||||||
|
})
|
||||||
|
}
|
|
@ -32,23 +32,4 @@ export function getAccessTokenFromAuthCode(instanceName, clientId, clientSecret,
|
||||||
grant_type: 'authorization_code',
|
grant_type: 'authorization_code',
|
||||||
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}`
|
|
||||||
})
|
|
||||||
}
|
}
|
20
routes/_utils/mastodon/timelines.js
Normal file
20
routes/_utils/mastodon/timelines.js
Normal 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
33
routes/_utils/statuses.js
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue