forked from coracle/flotilla
feat: sync checked read state to Dufflepud for cross-device badges
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
import {derived, get, writable} from "svelte/store"
|
import {derived, get, writable} from "svelte/store"
|
||||||
import {Badge} from "@capawesome/capacitor-badge"
|
import {Badge} from "@capawesome/capacitor-badge"
|
||||||
import {synced, throttled, withGetter} from "@welshman/store"
|
import {synced, throttled, withGetter} from "@welshman/store"
|
||||||
import {pubkey, tracker, repository, relaysByUrl} from "@welshman/app"
|
import {pubkey, signer, tracker, repository, relaysByUrl} from "@welshman/app"
|
||||||
import {assoc, prop, first, identity, groupBy, now} from "@welshman/lib"
|
import {assoc, prop, first, identity, groupBy, now, throttle} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {SignedEvent, TrustedEvent} from "@welshman/util"
|
||||||
import {deriveEventsByIdByUrl} from "@welshman/store"
|
import {deriveEventsByIdByUrl} from "@welshman/store"
|
||||||
import {sortEventsDesc, getTagValue, MESSAGE} from "@welshman/util"
|
import {sortEventsDesc, getTagValue, MESSAGE, makeEvent} from "@welshman/util"
|
||||||
import {makeSpacePath, makeRoomPath, makeSpaceChatPath, makeChatPath} from "@app/util/routes"
|
import {makeSpacePath, makeRoomPath, makeSpaceChatPath, makeChatPath} from "@app/util/routes"
|
||||||
import {
|
import {
|
||||||
CONTENT_KINDS,
|
CONTENT_KINDS,
|
||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
getSpaceUrlsFromGroupList,
|
getSpaceUrlsFromGroupList,
|
||||||
makeCommentFilter,
|
makeCommentFilter,
|
||||||
hasNip29,
|
hasNip29,
|
||||||
|
dufflepud,
|
||||||
|
DUFFLEPUD_URL,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {kv} from "@app/core/storage"
|
import {kv} from "@app/core/storage"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
@@ -76,6 +78,107 @@ export const syncChecked = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HTTP_AUTH = 27235
|
||||||
|
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(
|
||||||
|
makeEvent(HTTP_AUTH, {content: "", tags: [["u", DUFFLEPUD_URL]]}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Nostr ${btoa(JSON.stringify(nip98Auth))}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const pullCheckedRemote = async () => {
|
||||||
|
const authorization = await nip98Header()
|
||||||
|
|
||||||
|
if (!authorization) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(dufflepud(`kv/${CHECKED_KV_KEY}`), {headers: {authorization}})
|
||||||
|
|
||||||
|
if (res.status === 404) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const remote = JSON.parse(await res.text()) as Record<string, number>
|
||||||
|
|
||||||
|
checked.update($checked => {
|
||||||
|
for (const [path, ts] of Object.entries(remote)) {
|
||||||
|
if (ts > ($checked[path] || 0)) {
|
||||||
|
$checked[path] = ts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $checked
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
if (ready && pubkey.get()) {
|
||||||
|
pushCheckedRemote()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribePubkey()
|
||||||
|
unsubscribeChecked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Derived notifications state
|
// Derived notifications state
|
||||||
|
|
||||||
export const allNotifications = derived(
|
export const allNotifications = derived(
|
||||||
|
|||||||
@@ -174,6 +174,9 @@
|
|||||||
// Subscribe to page history to update checked state
|
// Subscribe to page history to update checked state
|
||||||
unsubscribers.push(notifications.syncChecked())
|
unsubscribers.push(notifications.syncChecked())
|
||||||
|
|
||||||
|
// Sync checked state across devices
|
||||||
|
unsubscribers.push(notifications.syncCheckedRemote())
|
||||||
|
|
||||||
// Initialize background notifications
|
// Initialize background notifications
|
||||||
unsubscribers.push(notifications.Push.sync())
|
unsubscribers.push(notifications.Push.sync())
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user