feat: sync checked read state to Dufflepud for cross-device badges #288
@@ -1,11 +1,17 @@
|
||||
import {derived, get, writable} from "svelte/store"
|
||||
import {Badge} from "@capawesome/capacitor-badge"
|
||||
import {synced, throttled, withGetter} from "@welshman/store"
|
||||
import {pubkey, tracker, repository, relaysByUrl} from "@welshman/app"
|
||||
import {assoc, prop, first, identity, groupBy, now} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {pubkey, signer, tracker, repository, relaysByUrl} from "@welshman/app"
|
||||
import {assoc, prop, first, identity, groupBy, now, throttle, parseJson, gt} from "@welshman/lib"
|
||||
import type {SignedEvent, TrustedEvent} from "@welshman/util"
|
||||
import {deriveEventsByIdByUrl} from "@welshman/store"
|
||||
import {sortEventsDesc, getTagValue, MESSAGE} from "@welshman/util"
|
||||
import {
|
||||
sortEventsDesc,
|
||||
getTagValue,
|
||||
MESSAGE,
|
||||
makeHttpAuth,
|
||||
makeHttpAuthHeader,
|
||||
} from "@welshman/util"
|
||||
import {makeSpacePath, makeRoomPath, makeSpaceChatPath, makeChatPath} from "@app/util/routes"
|
||||
import {
|
||||
CONTENT_KINDS,
|
||||
@@ -15,6 +21,8 @@ import {
|
||||
getSpaceUrlsFromGroupList,
|
||||
makeCommentFilter,
|
||||
hasNip29,
|
||||
dufflepud,
|
||||
DUFFLEPUD_URL,
|
||||
} from "@app/core/state"
|
||||
import {kv} from "@app/core/storage"
|
||||
import {page} from "$app/stores"
|
||||
@@ -75,6 +83,100 @@ export const syncChecked = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const CHECKED_KV_KEY = "checked"
|
||||
const NIP98_MAX_AGE = 23 * 60 * 60
|
||||
|
||||
let nip98Auth: SignedEvent | undefined
|
||||
|
||||
const nip98Header = async () => {
|
||||
const $signer = signer.get()
|
||||
|
||||
if (!$signer) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!nip98Auth || now() - nip98Auth.created_at > NIP98_MAX_AGE) {
|
||||
nip98Auth = await $signer.sign(await makeHttpAuth(DUFFLEPUD_URL, "GET"))
|
||||
}
|
||||
|
||||
|
hodlbod marked this conversation as resolved
Outdated
|
||||
return makeHttpAuthHeader(nip98Auth)
|
||||
}
|
||||
|
||||
const pullCheckedRemote = async () => {
|
||||
const authorization = await nip98Header()
|
||||
|
||||
if (!authorization) {
|
||||
return
|
||||
}
|
||||
|
||||
const res = await fetch(dufflepud(`kv/${CHECKED_KV_KEY}`), {headers: {authorization}})
|
||||
|
||||
if (!res.ok) {
|
||||
return
|
||||
}
|
||||
|
||||
const remote = parseJson<Record<string, number>>(await res.text())
|
||||
|
||||
|
hodlbod marked this conversation as resolved
Outdated
hodlbod
commented
These two checks are redundant, just check res.ok These two checks are redundant, just check res.ok
|
||||
if (!remote) {
|
||||
return
|
||||
|
hodlbod marked this conversation as resolved
Outdated
hodlbod
commented
Use Use `parseJson` to avoid errors
|
||||
}
|
||||
|
||||
checked.update($checked => {
|
||||
for (const [path, ts] of Object.entries(remote)) {
|
||||
|
hodlbod marked this conversation as resolved
Outdated
hodlbod
commented
Use Use `gt` from welshman/lib to avoid nan/undefined related errors
|
||||
if (gt(ts, $checked[path])) {
|
||||
$checked[path] = ts
|
||||
}
|
||||
}
|
||||
|
||||
return $checked
|
||||
})
|
||||
}
|
||||
|
||||
|
hodlbod marked this conversation as resolved
Outdated
hodlbod
commented
Don't wrap large blocks of code in a big try catch unless it's likely to fail. Scope this down to wrap the json parse or whatever is likely to actually fail. Don't wrap large blocks of code in a big try catch unless it's likely to fail. Scope this down to wrap the json parse or whatever is likely to actually fail.
|
||||
const pushCheckedRemote = throttle(3000, async () => {
|
||||
const authorization = await nip98Header()
|
||||
|
||||
if (!authorization) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await fetch(dufflepud(`kv/${CHECKED_KV_KEY}`), {
|
||||
method: "POST",
|
||||
headers: {authorization},
|
||||
body: JSON.stringify(checked.get()),
|
||||
})
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
})
|
||||
|
||||
export const syncCheckedRemote = () => {
|
||||
let ready = false
|
||||
|
||||
const unsubscribePubkey = pubkey.subscribe($pubkey => {
|
||||
ready = false
|
||||
nip98Auth = undefined
|
||||
|
||||
if ($pubkey) {
|
||||
pullCheckedRemote().then(() => {
|
||||
ready = true
|
||||
pushCheckedRemote()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const unsubscribeChecked = checked.subscribe(() => {
|
||||
|
hodlbod marked this conversation as resolved
Outdated
hodlbod
commented
Use Use `once` from welshman/lib
hodlbod
commented
Sorry, I wrote this before I realized what Sorry, I wrote this before I realized what `ready` does, you should not actually use once here.
|
||||
if (ready && pubkey.get()) {
|
||||
pushCheckedRemote()
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubscribePubkey()
|
||||
unsubscribeChecked()
|
||||
}
|
||||
}
|
||||
|
||||
// Derived notifications state
|
||||
|
||||
export const allNotifications = derived(
|
||||
|
||||
@@ -175,6 +175,9 @@
|
||||
// Subscribe to page history to update checked state
|
||||
unsubscribers.push(notifications.syncChecked())
|
||||
|
||||
// Sync checked state across devices
|
||||
unsubscribers.push(notifications.syncCheckedRemote())
|
||||
|
||||
// Initialize background notifications
|
||||
unsubscribers.push(Push.sync())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user
Use the utils in welshman/lib