feat: sync checked read state to Dufflepud for cross-device badges #288

Merged
hodlbod merged 1 commits from userAdityaa/flotilla:fix/252-sync-checked-dufflepud into dev 2026-05-29 21:15:57 +00:00
2 changed files with 109 additions and 4 deletions
+106 -4
View File
@@ -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"
1
@@ -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
Outdated
Review

Use the utils in welshman/lib

Use the utils in welshman/lib
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
Outdated
Review

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
Outdated
Review

Use parseJson to avoid errors

Use `parseJson` to avoid errors
}
checked.update($checked => {
for (const [path, ts] of Object.entries(remote)) {
hodlbod marked this conversation as resolved Outdated
Outdated
Review

Use gt from welshman/lib to avoid nan/undefined related errors

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
Outdated
Review

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
Outdated
Review

Use once from welshman/lib

Use `once` from welshman/lib
Outdated
Review

Sorry, I wrote this before I realized what ready does, you should not actually use once here.

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(
+3
View File
@@ -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())