From a2b7b13b7199b1ed130fa5165471ac8296c4447c Mon Sep 17 00:00:00 2001 From: userAdityaa Date: Fri, 29 May 2026 18:08:23 +0530 Subject: [PATCH] feat: sync checked read state to Dufflepud for cross-device badges --- src/app/util/notifications.ts | 111 ++++++++++++++++++++++++++++++++-- src/routes/+layout.svelte | 3 + 2 files changed, 110 insertions(+), 4 deletions(-) diff --git a/src/app/util/notifications.ts b/src/app/util/notifications.ts index 0bc5cb3c..a3b9b781 100644 --- a/src/app/util/notifications.ts +++ b/src/app/util/notifications.ts @@ -1,11 +1,11 @@ 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} 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, makeEvent} from "@welshman/util" import {makeSpacePath, makeRoomPath, makeSpaceChatPath, makeChatPath} from "@app/util/routes" import { CONTENT_KINDS, @@ -15,6 +15,8 @@ import { getSpaceUrlsFromGroupList, makeCommentFilter, hasNip29, + dufflepud, + DUFFLEPUD_URL, } from "@app/core/state" import {kv} from "@app/core/storage" 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 + + 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 export const allNotifications = derived( diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 42b3f197..a855b2b3 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -174,6 +174,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(notifications.Push.sync())