more work on offline capabilities
This commit is contained in:
parent
42fd153364
commit
6cf4a11283
5
package-lock.json
generated
5
package-lock.json
generated
|
@ -3282,6 +3282,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"idb-keyval": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-2.3.0.tgz",
|
||||
"integrity": "sha1-TURLgMP4b8vNUTIbTcvJJHxZSMA="
|
||||
},
|
||||
"ieee754": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz",
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
"fg-loadcss": "^2.0.1",
|
||||
"font-awesome-svg-png": "^1.2.2",
|
||||
"glob": "^7.1.2",
|
||||
"idb-keyval": "^2.3.0",
|
||||
"indexeddb-getall-shim": "^1.3.1",
|
||||
"intersection-observer": "^0.5.0",
|
||||
"intl-relativeformat": "^2.1.0",
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
{{/if}}
|
||||
</div>
|
||||
{{then constructor}}
|
||||
<:Component {constructor} :target />
|
||||
<:Component {constructor} :timeline />
|
||||
{{catch error}}
|
||||
<div>Component failed to load. Please try refreshing! {{error}}</div>
|
||||
{{/await}}
|
||||
|
|
|
@ -11,16 +11,18 @@
|
|||
</style>
|
||||
<script>
|
||||
import { store } from '../_utils/store'
|
||||
import { getHomeTimeline } from '../_utils/mastodon/timelines'
|
||||
import { getTimeline } from '../_utils/mastodon/timelines'
|
||||
import StatusListItem from './StatusListItem.html'
|
||||
import VirtualList from './VirtualList.html'
|
||||
import { splice, push } from 'svelte-extras'
|
||||
import {
|
||||
insertStatuses as insertStatusesIntoDatabase,
|
||||
getTimelineAfter as getTimelineFromDatabaseAfter
|
||||
getTimeline as getTimelineFromDatabase
|
||||
} from '../_utils/database/statuses'
|
||||
import { mergeStatuses } from '../_utils/statuses'
|
||||
import { mark, stop } from '../_utils/marks'
|
||||
import { timelines } from '../_static/timelines'
|
||||
|
||||
|
||||
const FETCH_LIMIT = 20
|
||||
|
||||
|
@ -30,15 +32,14 @@
|
|||
let instanceData = this.store.get('currentInstanceData')
|
||||
let online = this.get('online')
|
||||
let statuses = online ?
|
||||
await getHomeTimeline(instanceName, instanceData.access_token, null, FETCH_LIMIT) :
|
||||
await getTimelineFromDatabaseAfter(null, FETCH_LIMIT)
|
||||
await getTimeline(instanceName, instanceData.access_token, this.get('timeline'), null, FETCH_LIMIT) :
|
||||
await getTimelineFromDatabase(instanceName, this.get('timeline'), null, FETCH_LIMIT)
|
||||
if (online) {
|
||||
insertStatusesIntoDatabase(statuses)
|
||||
insertStatusesIntoDatabase(instanceName, this.get('timeline'), statuses)
|
||||
}
|
||||
this.addStatuses(statuses)
|
||||
},
|
||||
data: () => ({
|
||||
target: 'home',
|
||||
StatusListItem: StatusListItem,
|
||||
statuses: [],
|
||||
runningUpdate: false
|
||||
|
@ -49,7 +50,7 @@
|
|||
key: status.id
|
||||
})),
|
||||
lastStatusId: (statuses) => statuses.length && statuses[statuses.length - 1].id,
|
||||
label: (target, $currentInstance) => `${target} timeline for ${$currentInstance}`
|
||||
label: (timeline, $currentInstance) => `${timelines[timeline].label} timeline for ${$currentInstance}`
|
||||
},
|
||||
store: () => store,
|
||||
components: {
|
||||
|
@ -69,10 +70,10 @@
|
|||
let instanceData = this.store.get('currentInstanceData')
|
||||
let online = this.get('online')
|
||||
let newStatuses = online ?
|
||||
await getHomeTimeline(instanceName, instanceData.access_token, lastStatusId, FETCH_LIMIT) :
|
||||
await getTimelineFromDatabaseAfter(lastStatusId, FETCH_LIMIT)
|
||||
await getTimeline(instanceName, instanceData.access_token, this.get('timeline'), lastStatusId, FETCH_LIMIT) :
|
||||
await getTimelineFromDatabase(instanceName, this.get('timeline'), lastStatusId, FETCH_LIMIT)
|
||||
if (online) {
|
||||
insertStatusesIntoDatabase(newStatuses)
|
||||
insertStatusesIntoDatabase(instanceName, this.get('timeline'), newStatuses)
|
||||
}
|
||||
let statuses = this.get('statuses')
|
||||
if (statuses) {
|
||||
|
|
7
routes/_static/timelines.js
Normal file
7
routes/_static/timelines.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
const timelines = {
|
||||
home: { name: 'home', label: 'Home' },
|
||||
local: { name: 'local', label: 'Local' },
|
||||
federated: { name: 'federated', label: 'Federated' }
|
||||
}
|
||||
|
||||
export { timelines }
|
|
@ -25,15 +25,10 @@ const importIndexedDBGetAllShim = () => import(
|
|||
/* webpackChunkName: 'indexeddb-getall-shim' */ 'indexeddb-getall-shim'
|
||||
)
|
||||
|
||||
const importOfflineNotification = () => import(
|
||||
/* webpackHunkName: 'offlineNotification' */ '../_utils/offlineNotification'
|
||||
)
|
||||
|
||||
export {
|
||||
importURLSearchParams,
|
||||
importTimeline,
|
||||
importIntersectionObserver,
|
||||
importRequestIdleCallback,
|
||||
importIndexedDBGetAllShim,
|
||||
importOfflineNotification
|
||||
importIndexedDBGetAllShim
|
||||
}
|
2
routes/_utils/database/keyval.js
Normal file
2
routes/_utils/database/keyval.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import keyval from 'idb-keyval'
|
||||
export { keyval }
|
|
@ -1,7 +1,9 @@
|
|||
import { keyval } from './keyval'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import padStart from 'lodash/padStart'
|
||||
|
||||
const STORE = 'statuses'
|
||||
const databaseCache = {}
|
||||
|
||||
function toPaddedBigInt(id) {
|
||||
return padStart(id, 30, '0')
|
||||
|
@ -24,26 +26,40 @@ function transformStatusForStorage(status) {
|
|||
return status
|
||||
}
|
||||
|
||||
const dbPromise = new Promise((resolve, reject) => {
|
||||
let req = indexedDB.open(STORE, 1)
|
||||
req.onerror = reject
|
||||
req.onblocked = () => {
|
||||
console.log('idb blocked')
|
||||
function getDatabase(instanceName, timeline) {
|
||||
const key = `${instanceName}_${timeline}`
|
||||
if (databaseCache[key]) {
|
||||
return Promise.resolve(databaseCache[key])
|
||||
}
|
||||
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)
|
||||
})
|
||||
|
||||
export async function getTimelineAfter(max_id = null, limit = 20) {
|
||||
const db = await dbPromise
|
||||
let objectStoreName = `${STORE}_${key}`
|
||||
|
||||
keyval.get('known_dbs').then(knownDbs => {
|
||||
knownDbs = knownDbs || {}
|
||||
knownDbs[objectStoreName] = true
|
||||
keyval.set('known_dbs', knownDbs)
|
||||
})
|
||||
|
||||
databaseCache[key] = new Promise((resolve, reject) => {
|
||||
let req = indexedDB.open(objectStoreName, 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('pinafore_id_as_negative_big_int', 'pinafore_id_as_negative_big_int')
|
||||
}
|
||||
req.onsuccess = () => resolve(req.result)
|
||||
})
|
||||
return databaseCache[key]
|
||||
}
|
||||
|
||||
export async function getTimeline(instanceName, timeline, max_id = null, limit = 20) {
|
||||
const db = await getDatabase(instanceName, timeline)
|
||||
return await new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE, 'readonly')
|
||||
const store = tx.objectStore(STORE)
|
||||
|
@ -61,8 +77,8 @@ export async function getTimelineAfter(max_id = null, limit = 20) {
|
|||
})
|
||||
}
|
||||
|
||||
export async function insertStatuses(statuses) {
|
||||
const db = await dbPromise
|
||||
export async function insertStatuses(instanceName, timeline, statuses) {
|
||||
const db = await getDatabase(instanceName, timeline)
|
||||
return await new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE, 'readwrite')
|
||||
const store = tx.objectStore(STORE)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { get, paramsString } from '../ajax'
|
||||
import { basename } from './utils'
|
||||
|
||||
export function getHomeTimeline(instanceName, accessToken, maxId, since) {
|
||||
let url = `${basename(instanceName)}/api/v1/timelines/home`
|
||||
export function getTimeline(instanceName, accessToken, timeline, maxId, since) {
|
||||
let timelineUrlName = timeline === 'local' || timeline === 'federated' ? 'public' : timeline
|
||||
let url = `${basename(instanceName)}/api/v1/timelines/${timelineUrlName}`
|
||||
|
||||
let params = {}
|
||||
if (since) {
|
||||
|
@ -13,6 +14,10 @@ export function getHomeTimeline(instanceName, accessToken, maxId, since) {
|
|||
params.max_id = maxId
|
||||
}
|
||||
|
||||
if (timeline === 'local') {
|
||||
params.local = true
|
||||
}
|
||||
|
||||
url += '?' + paramsString(params)
|
||||
|
||||
return get(url, {
|
||||
|
|
|
@ -17,5 +17,9 @@ const observe = online => {
|
|||
}
|
||||
}
|
||||
|
||||
if (!navigator.onLine) {
|
||||
observe(false)
|
||||
}
|
||||
|
||||
window.addEventListener('offline', () => observe(false));
|
||||
window.addEventListener('online', () => observe(true));
|
22
routes/_utils/serviceWorkerClient.js
Normal file
22
routes/_utils/serviceWorkerClient.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { toast } from './toast'
|
||||
import { keyval } from './database/keyval'
|
||||
|
||||
function onUpdateFound(registration) {
|
||||
const newWorker = registration.installing
|
||||
|
||||
newWorker.addEventListener('statechange', async () => {
|
||||
if (!(await keyval.get('serviceworker_installed'))) {
|
||||
await keyval.set('serviceworker_installed', true)
|
||||
return
|
||||
}
|
||||
if (newWorker.state === 'activated' && navigator.serviceWorker.controller) {
|
||||
toast.say('App update available. Reload to update.')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!location.origin.match('localhost') && 'serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/service-worker.js').then(registration => {
|
||||
registration.addEventListener('updatefound', () => onUpdateFound(registration))
|
||||
})
|
||||
}
|
|
@ -3,16 +3,29 @@
|
|||
</:Head>
|
||||
|
||||
<Layout page='federated'>
|
||||
<h1>Federated Timeline</h1>
|
||||
{{#if $isUserLoggedIn}}
|
||||
<LazyTimeline timeline='federated' />
|
||||
{{else}}
|
||||
<FreeTextLayout>
|
||||
<h1>Federated</h1>
|
||||
|
||||
<p>Your federated timeline will appear here when logged in.</p>
|
||||
</FreeTextLayout>
|
||||
{{/if}}
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
import Layout from './_components/Layout.html';
|
||||
import Layout from './_components/Layout.html'
|
||||
import LazyTimeline from './_components/LazyTimeline.html'
|
||||
import FreeTextLayout from './_components/FreeTextLayout.html'
|
||||
import { store } from './_utils/store.js'
|
||||
|
||||
export default {
|
||||
store: () => store,
|
||||
components: {
|
||||
Layout
|
||||
Layout,
|
||||
LazyTimeline,
|
||||
FreeTextLayout
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<Layout page='home'>
|
||||
{{#if $isUserLoggedIn}}
|
||||
<LazyTimeline target='home' />
|
||||
<LazyTimeline timeline='home' />
|
||||
{{else}}
|
||||
<NotLoggedInHome/>
|
||||
{{/if}}
|
||||
|
|
|
@ -3,16 +3,29 @@
|
|||
</:Head>
|
||||
|
||||
<Layout page='local'>
|
||||
<h1>Local</h1>
|
||||
{{#if $isUserLoggedIn}}
|
||||
<LazyTimeline timeline='local' />
|
||||
{{else}}
|
||||
<FreeTextLayout>
|
||||
<h1>Local</h1>
|
||||
|
||||
<p>Your local timeline will appear here when logged in.</p>
|
||||
</FreeTextLayout>
|
||||
{{/if}}
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
import Layout from './_components/Layout.html';
|
||||
import Layout from './_components/Layout.html'
|
||||
import LazyTimeline from './_components/LazyTimeline.html'
|
||||
import FreeTextLayout from './_components/FreeTextLayout.html'
|
||||
import { store } from './_utils/store.js'
|
||||
|
||||
export default {
|
||||
store: () => store,
|
||||
components: {
|
||||
Layout
|
||||
Layout,
|
||||
LazyTimeline,
|
||||
FreeTextLayout
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
|
@ -3,16 +3,21 @@
|
|||
</:Head>
|
||||
|
||||
<Layout page='notifications'>
|
||||
<h1>Notifications</h1>
|
||||
<FreeTextLayout>
|
||||
<h1>Notifications</h1>
|
||||
|
||||
<p>Your notifications will appear here when logged in.</p>
|
||||
</FreeTextLayout>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
import Layout from './_components/Layout.html';
|
||||
import FreeTextLayout from './_components/FreeTextLayout.html'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout
|
||||
Layout,
|
||||
FreeTextLayout
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -16,7 +16,7 @@
|
|||
<span class="acct-display-name">{{instanceUserAccount.display_name}}</span>
|
||||
</div>
|
||||
<h2>Theme:</h2>
|
||||
<form class="theme-chooser">
|
||||
<form class="theme-chooser" aria-label="Choose a theme">
|
||||
{{#each themes as theme}}
|
||||
<div class="theme-group">
|
||||
<input type="radio" id="choice-theme-{{theme.name}}"
|
||||
|
@ -27,7 +27,7 @@
|
|||
{{/each}}
|
||||
</form>
|
||||
|
||||
<form class="instance-actions">
|
||||
<form class="instance-actions" aria-label="Switch to or log out of this instance">
|
||||
{{#if $loggedInInstancesInOrder.length > 1}}
|
||||
<button class="primary" disabled="{{$currentInstance === params.instanceName}}"
|
||||
on:click="onSwitchToThisInstance()">
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<Layout page='settings'>
|
||||
<SettingsLayout page='settings/instances/add' label="Add an Instance">
|
||||
<h1>Add an Instance</h1>
|
||||
<h1 id="add-an-instance-h1">Add an Instance</h1>
|
||||
|
||||
<LoadingMask show="{{loading}}"/>
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
|||
<p>Log in to an instance to start using Pinafore.</p>
|
||||
{{/if}}
|
||||
|
||||
<form class="add-new-instance" on:submit='onSubmit(event)'>
|
||||
<form class="add-new-instance" on:submit='onSubmit(event)' aria-labelledby="add-an-instance-h1">
|
||||
<label for="instanceInput">Instance:</label>
|
||||
<input type="text" id="instanceInput" bind:value='$instanceNameInSearch' placeholder=''>
|
||||
<button class="primary" type="submit" id="submitButton" disabled="{{!$instanceNameInSearch}}">Add instance</button>
|
||||
|
|
|
@ -8,12 +8,6 @@
|
|||
<link rel='manifest' href='/manifest.json'>
|
||||
<link rel='icon' type='image/png' href='/favicon.png'>
|
||||
|
||||
<script>
|
||||
if (!location.origin.match('localhost') && 'serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/service-worker.js');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* auto-generated w/ build-sass.js */
|
||||
body{--button-primary-bg:#6081e6;--button-primary-text:#fff;--button-primary-border:#132c76;--button-primary-bg-active:#456ce2;--button-primary-bg-hover:#6988e7;--button-bg:#e6e6e6;--button-text:#333;--button-border:#a7a7a7;--button-bg-active:#bfbfbf;--button-bg-hover:#f2f2f2;--input-border:#dadada;--anchor-text:#4169e1;--main-bg:#fff;--body-bg:#e8edfb;--body-text-color:#333;--main-border:#dadada;--svg-fill:#4169e1;--form-bg:#f7f7f7;--form-border:#c1c1c1;--nav-bg:#4169e1;--nav-border:#214cce;--nav-a-border:#4169e1;--nav-a-selected-border:#fff;--nav-a-selected-bg:#6d8ce8;--nav-svg-fill:#fff;--nav-text-color:#fff;--nav-a-selected-border-hover:#fff;--nav-a-selected-bg-hover:#839deb;--nav-a-bg-hover:#577ae4;--nav-a-border-hover:#4169e1;--nav-svg-fill-hover:#fff;--nav-text-color-hover:#fff;--action-button-fill-color:#839deb;--action-button-fill-color-hover:#99afef;--action-button-fill-color-active:#577ae4;--settings-list-item-bg:#fff;--settings-list-item-text:#4169e1;--settings-list-item-text-hover:#4169e1;--settings-list-item-border:#dadada;--settings-list-item-bg-active:#e6e6e6;--settings-list-item-bg-hover:#fafafa;--toast-bg:#333;--toast-border:#fafafa;--toast-text:#fff;--mask-bg:#333;--mask-svg-fill:#fff;--deemphasized-text-color:#666}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { init } from 'sapper/runtime.js'
|
||||
import { toast } from '../routes/_utils/toast'
|
||||
import { offlineNotifiction } from '../routes/_utils/offlineNotification'
|
||||
import { serviceWorkerClient } from '../routes/_utils/serviceWorkerClient'
|
||||
|
||||
import {
|
||||
importURLSearchParams,
|
||||
importIntersectionObserver,
|
||||
importRequestIdleCallback,
|
||||
importIndexedDBGetAllShim,
|
||||
importOfflineNotification
|
||||
importIndexedDBGetAllShim
|
||||
} from '../routes/_utils/asyncModules'
|
||||
|
||||
// polyfills
|
||||
|
@ -18,14 +18,4 @@ Promise.all([
|
|||
]).then(() => {
|
||||
// `routes` is an array of route objects injected by Sapper
|
||||
init(document.querySelector('#sapper'), __routes__)
|
||||
|
||||
if (navigator.serviceWorker && navigator.serviceWorker.controller) {
|
||||
navigator.serviceWorker.controller.onstatechange = (e) => {
|
||||
if (e.target.state === 'redundant') {
|
||||
toast.say('App update available. Reload to update.');
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
importOfflineNotification()
|
Loading…
Reference in a new issue