From 2528e4acad14c238366f971fe277e5c70ec95c74 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Fri, 23 Jan 2026 15:35:54 -0800 Subject: [PATCH] Clean up and fix push notifications implementation --- .env.template | 4 +- capacitor.config.ts | 9 +- src/app/core/state.ts | 2 +- src/app/util/notifications.ts | 147 ++++++++++++------------ src/lib/util.ts | 8 ++ src/routes/+layout.svelte | 14 ++- src/routes/settings/alerts/+page.svelte | 6 + 7 files changed, 107 insertions(+), 83 deletions(-) diff --git a/.env.template b/.env.template index 4e78a141..bac512e9 100644 --- a/.env.template +++ b/.env.template @@ -10,8 +10,8 @@ VITE_PLATFORM_RELAYS= VITE_PLATFORM_ACCENT="#7161FF" VITE_PLATFORM_SECONDARY="#EB5E28" VITE_PLATFORM_DESCRIPTION="Flotilla is nostr — for communities." -VITE_PUSH_SERVER= -VITE_PUSH_BRIDGE= +VITE_PUSH_SERVER=https://nps.flotilla.social/ +VITE_PUSH_BRIDGE=wss://npb.coracle.social/ VITE_BLOCKED_RELAYS=brb.io,relay.nostr.band,nostr.mutinywallet.com,feeds.nostr.band,nostr.zbd.gg,wot.utxo.one VITE_INDEXER_RELAYS=purplepag.es,relay.damus.io,indexer.coracle.social VITE_DEFAULT_RELAYS=relay.damus.io,relay.primal.net,nostr.mom diff --git a/capacitor.config.ts b/capacitor.config.ts index 647c0a16..42540485 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -11,12 +11,15 @@ const config: CapacitorConfig = { adjustMarginsForEdgeToEdge: false, }, plugins: { + SystemBars: { + insetsHandling: "disable", + }, SplashScreen: { - androidSplashResourceName: "splash" + androidSplashResourceName: "splash", }, Keyboard: { style: "DARK", - resizeOnFullScreen: true, + // resizeOnFullScreen: true, }, Badge: { persist: true, @@ -25,7 +28,7 @@ const config: CapacitorConfig = { }, // Use this for live reload https://capacitorjs.com/docs/guides/live-reload // server: { - // url: "http://192.168.1.115:1847", + // url: "http://192.168.1.65:1847", // cleartext: true // }, }; diff --git a/src/app/core/state.ts b/src/app/core/state.ts index ea3ed80a..c65080a4 100644 --- a/src/app/core/state.ts +++ b/src/app/core/state.ts @@ -125,7 +125,7 @@ export const ENABLE_ZAPS = Capacitor.getPlatform() != "ios" export const PUSH_SERVER = import.meta.env.VITE_PUSH_SERVER -export const PUSH_BRIDGE = import.meta.env.VITE_PUSH_BRIDGE +export const PUSH_BRIDGE = normalizeRelayUrl(import.meta.env.VITE_PUSH_BRIDGE) export const VAPID_PUBLIC_KEY = import.meta.env.VITE_VAPID_PUBLIC_KEY diff --git a/src/app/util/notifications.ts b/src/app/util/notifications.ts index 9cc7c595..0a09835c 100644 --- a/src/app/util/notifications.ts +++ b/src/app/util/notifications.ts @@ -23,7 +23,7 @@ import { assoc, poll, prop, - sha256, + hash, textEncoder, parseJson, flatten, @@ -37,7 +37,7 @@ import { postJson, fetchJson, } from "@welshman/lib" -import type {TrustedEvent, Filter} from "@welshman/util" +import type {TrustedEvent, RelayProfile, Filter} from "@welshman/util" import {deriveEventsByIdByUrl} from "@welshman/store" import { ZAP_GOAL, @@ -51,6 +51,7 @@ import { makeEvent, RelayMode, } from "@welshman/util" +import {buildUrl} from '@lib/util' import { makeSpacePath, makeChatPath, @@ -73,6 +74,7 @@ import { chatsById, hasNip29, getSettings, + userSettings, userSettingsValues, userGroupList, getSpaceUrlsFromGroupList, @@ -336,12 +338,13 @@ export const clearBadges = async () => { // Local notifications interface IPushAdapter { - request: () => Promise + request: (prompt: boolean) => Promise + cancel: () => Promise start: () => Unsubscriber } class CapacitorNotifications implements IPushAdapter { - async sync() { + async sync(signal: AbortSignal) { const token = get(pushToken) if (!token) { @@ -355,21 +358,23 @@ class CapacitorNotifications implements IPushAdapter { const secret = get(pushSecret) if (secret) { - const {callback} = await fetchJson(`${PUSH_SERVER}/subscription/${secret}`) + const res = await fetch(buildUrl(PUSH_SERVER, "subscription", secret), {signal}) - if (callback) { - return {secret, callback} - } else { - pushSecret.set(undefined) + if (res.ok) { + const {callback} = await res.json() + + if (callback) { + return {secret, callback} + } else { + pushSecret.set(undefined) + } } } const ios = Capacitor.getPlatform() === "ios" const channel = ios ? "apns" : "fcm" - const topic = "social.flotilla" - const data = ios ? {token, topic} : {token} - - const json = await postJson(`${PUSH_SERVER}/subscription/${channel}`, data) + const url = buildUrl(PUSH_SERVER, "subscription", channel) + const json = await postJson(url, {token}, {signal}) if (json) { pushSecret.set(json.sk) @@ -382,10 +387,13 @@ class CapacitorNotifications implements IPushAdapter { return } + const canPush = (relay?: RelayProfile) => + Boolean(relay?.self && relay?.supported_nips?.map(String)?.includes("9a")) + const getPushStuff = async (url: string) => { let relay = await loadRelay(url) - if (!relay?.self || !relay?.supported_nips?.includes("9a")) { + if (!canPush(relay)) { relay = await loadRelay(PUSH_BRIDGE) } @@ -406,9 +414,10 @@ class CapacitorNotifications implements IPushAdapter { console.warn(`Failed to subscribe ${relay} to ${key} notifications: unsupported`) } else { const {url, pubkey} = stuff - const identifier = await sha256(textEncoder.encode(info.callback + relay + key)) + const identifier = String(hash(info.callback + relay + key)) const thunk = publishThunk({ + signal, relays: [url], event: makeEvent(30390, { content: await signer @@ -454,10 +463,10 @@ class CapacitorNotifications implements IPushAdapter { } } - async request() { + async request(prompt = true) { let status = await PushNotifications.checkPermissions() - if (status.receive === "prompt") { + if (prompt && status.receive === "prompt") { status = await PushNotifications.requestPermissions() } @@ -465,53 +474,50 @@ class CapacitorNotifications implements IPushAdapter { return status.receive } - let token = "" + let token = get(pushToken) - PushNotifications.addListener("registration", ({value}: Token) => { - token = value - }) + if (!token) { + PushNotifications.addListener("registration", ({value}: Token) => { + token = value + }) - PushNotifications.addListener("registrationError", (error: RegistrationError) => { - console.error(error) - }) + PushNotifications.addListener("registrationError", (error: RegistrationError) => { + console.error(error) + }) - await PushNotifications.register() - await poll({ - condition: () => Boolean(token), - signal: AbortSignal.timeout(5000), - }) + await Promise.all([ + PushNotifications.register(), + poll({ + condition: () => Boolean(token), + signal: AbortSignal.timeout(5000), + }), + ]) - if (token) { pushToken.set(token) - - return "granted" } - pushToken.set(undefined) - - return "denied" + return token ? "granted" : "denied" } start() { - this.sync().then(() => { - PushNotifications.addListener( - "pushNotificationActionPerformed", - async (action: ActionPerformed) => { - const event = parseJson(action.notification.data.event) - const relays = [action.notification.data.relay] + const controller = new AbortController() - goto(await getEventPath(event, relays)) - }, - ) - }) + this.sync(controller.signal) - return () => PushNotifications.removeAllListeners() + return () => controller.abort() + } + + async cancel() { + await PushNotifications.unregister() + + pushSecret.set(undefined) + pushToken.set(undefined) } } class WebNotifications implements IPushAdapter { - async request() { - if (Notification?.permission === "default") { + async request(prompt = true) { + if (prompt && Notification?.permission === "default") { await Notification.requestPermission() } @@ -561,6 +567,10 @@ class WebNotifications implements IPushAdapter { } }) } + + async cancel() { + // pass + } } export class Push { @@ -580,39 +590,24 @@ export class Push { } static start() { - return Push._getAdapter().start() + const adapter = Push._getAdapter() + const promise = adapter.request(false) + const controller = new AbortController() + + promise.then(permissions => { + if (permissions === "granted" && !controller.signal.aborted) { + controller.signal.addEventListener("abort", adapter.start()) + } + }) + + Push._unsubscriber = () => controller.abort() } static request() { return Push._getAdapter().request() } - static resume() { - const unsubscribe = userSettingsValues.subscribe(({alerts_push}) => { - if (alerts_push) { - const promise = Push.request() - const controller = new AbortController() - - promise.then(permissions => { - if (permissions === "granted" && !controller.signal.aborted) { - controller.signal.addEventListener("abort", Push.start()) - } - }) - - Push._unsubscriber = () => controller.abort() - } else { - Push.stop() - } - }) - - return () => { - unsubscribe() - Push.stop() - } - } - - static stop() { - Push._unsubscriber?.() - Push._unsubscriber = undefined + static cancel() { + return Push._getAdapter().cancel() } } diff --git a/src/lib/util.ts b/src/lib/util.ts index f35952f6..75e971be 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -16,3 +16,11 @@ export const day = (seconds: number) => Math.floor(seconds / DAY) export const daysBetween = (start: number, end: number) => [...range(start, end, DAY)].map(day) export const ucFirst = (s: string) => s.slice(0, 1).toUpperCase() + s.slice(1) + +export const buildUrl = (base: string | URL, ...pathname: string[]) => { + const url = new URL(base) + + url.pathname = '/' + pathname.join('/') + + return url.toString() +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index f85f92aa..bf667449 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,6 +1,7 @@