From 646b8f873641d9a59b83f24776b6048e283d3f72 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Fri, 23 Jan 2026 16:51:02 -0800 Subject: [PATCH] Rework subscription storage --- capacitor.config.ts | 8 +- src/app/core/state.ts | 38 +++-- src/app/util/notifications.ts | 217 ++++++++++++++---------- src/lib/components/Drawer.svelte | 2 +- src/routes/+layout.svelte | 18 +- src/routes/settings/alerts/+page.svelte | 106 ++++++------ 6 files changed, 219 insertions(+), 170 deletions(-) diff --git a/capacitor.config.ts b/capacitor.config.ts index 42540485..8875f8a0 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -27,10 +27,10 @@ const config: CapacitorConfig = { }, }, // Use this for live reload https://capacitorjs.com/docs/guides/live-reload - // server: { - // url: "http://192.168.1.65:1847", - // cleartext: true - // }, + server: { + url: "http://192.168.1.65:1847", + cleartext: true + }, }; export default config; diff --git a/src/app/core/state.ts b/src/app/core/state.ts index c65080a4..ab7646e1 100644 --- a/src/app/core/state.ts +++ b/src/app/core/state.ts @@ -42,6 +42,7 @@ import { import { getter, throttled, + withGetter, deriveArray, makeDeriveEvent, makeLoadItem, @@ -278,12 +279,6 @@ export type SettingsValues = { relay_auth: RelayAuthMode send_delay: number font_size: number - alerts_push: boolean - alerts_sound: boolean - alerts_badge: boolean - alerts_spaces: boolean - alerts_mentions: boolean - alerts_messages: boolean muted_rooms: string[] } @@ -301,12 +296,6 @@ export const defaultSettings: SettingsValues = { relay_auth: RelayAuthMode.Conservative, send_delay: 0, font_size: 1.1, - alerts_push: false, - alerts_sound: false, - alerts_badge: false, - alerts_spaces: true, - alerts_mentions: true, - alerts_messages: true, muted_rooms: [], } @@ -348,9 +337,28 @@ export const relaysMostlyRestricted = writable>({}) // Push notifications -export const pushToken = writable() - -export const pushSecret = writable() +export const notificationSettings = withGetter( + writable<{ + push: boolean + sound: boolean + badge: boolean + spaces: boolean, + mentions: boolean, + messages: boolean, + token?: string + subscription?: { + key: string + callback: string + } + }>({ + push: false, + sound: false, + badge: false, + spaces: true, + mentions: true, + messages: true, + }) +) // Chats diff --git a/src/app/util/notifications.ts b/src/app/util/notifications.ts index 0a09835c..6701c0a0 100644 --- a/src/app/util/notifications.ts +++ b/src/app/util/notifications.ts @@ -69,8 +69,7 @@ import { MESSAGE_KINDS, PUSH_BRIDGE, PUSH_SERVER, - pushToken, - pushSecret, + notificationSettings, chatsById, hasNip29, getSettings, @@ -335,56 +334,86 @@ export const clearBadges = async () => { } } -// Local notifications +// Push notifications interface IPushAdapter { - request: (prompt: boolean) => Promise - cancel: () => Promise + request: (prompt?: boolean) => Promise + enable: () => Promise + disable: () => Promise start: () => Unsubscriber } -class CapacitorNotifications implements IPushAdapter { - async sync(signal: AbortSignal) { - const token = get(pushToken) +PushNotifications.addListener( + "pushNotificationActionPerformed", + async (action: ActionPerformed) => { + console.log('====== action', JSON.stringify(action)) + const event = parseJson(action.notification.data.event) + const relays = [action.notification.data.relay] - if (!token) { - return + goto(await getEventPath(event, relays)) + }, +) + +class CapacitorNotifications implements IPushAdapter { + async request(prompt = true) { + let status = await PushNotifications.checkPermissions() + + if (prompt && status.receive === "prompt") { + status = await PushNotifications.requestPermissions() } - const info: Maybe<{ - secret: string - callback: string - }> = await tryCatch(async () => { - const secret = get(pushSecret) + if (status.receive !== "granted") { + return status.receive + } - if (secret) { - const res = await fetch(buildUrl(PUSH_SERVER, "subscription", secret), {signal}) + let {token} = notificationSettings.get() - if (res.ok) { - const {callback} = await res.json() + if (!token) { + PushNotifications.addListener("registration", ({value}: Token) => { + token = value + }) - if (callback) { - return {secret, callback} - } else { - pushSecret.set(undefined) - } - } - } + PushNotifications.addListener("registrationError", (error: RegistrationError) => { + console.error(error) + }) - const ios = Capacitor.getPlatform() === "ios" - const channel = ios ? "apns" : "fcm" - const url = buildUrl(PUSH_SERVER, "subscription", channel) - const json = await postJson(url, {token}, {signal}) + await Promise.all([ + PushNotifications.register(), + poll({ + condition: () => Boolean(token), + signal: AbortSignal.timeout(5000), + }), + ]) - if (json) { - pushSecret.set(json.sk) + notificationSettings.update(assoc('token', token)) + } - return {secret: json.sk, callback: json.callback} - } - }) + return token ? "granted" : "denied" + } - if (!info) { - return + async syncServer(signal: AbortSignal) { + const {token} = notificationSettings.get() + + if (!token) { + throw new Error("Attempted to sync push server without a token") + } + + const channel = Capacitor.getPlatform() === "ios" ? "apns" : "fcm" + const url = buildUrl(PUSH_SERVER, "subscription", channel) + const json = await postJson(url, {token}, {signal}) + + if (json?.callback && json?.id) { + notificationSettings.update(assoc('subscription', json)) + } else { + console.warn("Failed to register with push server") + } + } + + async syncRelays(signal: AbortSignal) { + const {subscription} = notificationSettings.get() + + if (!subscription) { + throw new Error("Attempted to sync relays without a subscription") } const canPush = (relay?: RelayProfile) => @@ -402,19 +431,23 @@ class CapacitorNotifications implements IPushAdapter { } } - const syncSubscription = async ( + const syncRelay = async ( key: string, relay: string, filters: Filter[], ignore: Filter[] = [], ) => { + if (signal.aborted) { + return + } + const stuff = await getPushStuff(relay) if (!stuff) { console.warn(`Failed to subscribe ${relay} to ${key} notifications: unsupported`) } else { const {url, pubkey} = stuff - const identifier = String(hash(info.callback + relay + key)) + const identifier = String(hash(subscription.callback + relay + key)) const thunk = publishThunk({ signal, @@ -426,7 +459,7 @@ class CapacitorNotifications implements IPushAdapter { pubkey, JSON.stringify([ ["relay", relay], - ["callback", info.callback], + ["callback", subscription.callback], ...ignore.map(filter => ["ignore", JSON.stringify(filter)]), ...filters.map(filter => ["filter", JSON.stringify(filter)]), ]), @@ -451,7 +484,7 @@ class CapacitorNotifications implements IPushAdapter { const filters = [{kinds: MESSAGE_KINDS}, makeCommentFilter(CONTENT_KINDS)] const ignore = [{"#h": [muted_rooms]}] - syncSubscription("spaces", relay, filters, ignore) + syncRelay("spaces", relay, filters, ignore) } const $pubkey = pubkey.get()! @@ -459,59 +492,56 @@ class CapacitorNotifications implements IPushAdapter { for (const relay of getPubkeyRelays($pubkey, RelayMode.Messaging)) { const filters = [{kinds: DM_KINDS, "#p": [$pubkey]}] - syncSubscription("messages", relay, filters) + syncRelay("messages", relay, filters) } } - async request(prompt = true) { - let status = await PushNotifications.checkPermissions() - - if (prompt && status.receive === "prompt") { - status = await PushNotifications.requestPermissions() - } - - if (status.receive !== "granted") { - return status.receive - } - - let token = get(pushToken) - - if (!token) { - PushNotifications.addListener("registration", ({value}: Token) => { - token = value - }) - - PushNotifications.addListener("registrationError", (error: RegistrationError) => { - console.error(error) - }) - - await Promise.all([ - PushNotifications.register(), - poll({ - condition: () => Boolean(token), - signal: AbortSignal.timeout(5000), - }), - ]) - - pushToken.set(token) - } - - return token ? "granted" : "denied" - } - start() { + const {token} = notificationSettings.get() const controller = new AbortController() + const {signal} = controller - this.sync(controller.signal) + if (!token) { + console.warn("Attempted to start push notifications without a callback") + } else { + call(async () => { + try { + if (!notificationSettings.get().subscription) { + await this.syncServer(signal) + } + + if (notificationSettings.get().subscription) { + await this.syncRelays(signal) + } + } catch (e) { + console.error(e) + } + }) + } return () => controller.abort() } - async cancel() { + async enable() { + notificationSettings.update(assoc('push', true)) + } + + async disable() { + const {token, subscription, ...settings} = notificationSettings.get() + await PushNotifications.unregister() - pushSecret.set(undefined) - pushToken.set(undefined) + if (subscription) { + const res = await fetch(buildUrl(PUSH_SERVER, 'subscription', subscription.key), { + method: 'delete', + }) + + if (!res.ok) { + console.warn("Failed to delete push subscription") + } + } + + notificationSettings.set({...settings, push: false}) } } @@ -550,9 +580,9 @@ class WebNotifications implements IPushAdapter { start() { return onNotification(event => { - const {alerts_messages, alerts_mentions, alerts_spaces} = getSettings() + const {alerts_push, alerts_messages, alerts_mentions, alerts_spaces} = getSettings() - if (document.hidden && Notification?.permission === "granted") { + if (alerts_push && document.hidden && Notification?.permission === "granted") { if (alerts_messages && matchFilters(dmFilters, event)) { this.notify(event, "New direct message", "Someone sent you a direct message.") } else if ( @@ -568,14 +598,17 @@ class WebNotifications implements IPushAdapter { }) } - async cancel() { - // pass + async enable() { + notificationSettings.update(assoc('push', true)) + } + + async disable() { + notificationSettings.update(assoc('push', false)) } } export class Push { static _adapter: IPushAdapter | undefined - static _unsubscriber: Unsubscriber | undefined static _getAdapter() { if (!Push._adapter) { @@ -600,14 +633,18 @@ export class Push { } }) - Push._unsubscriber = () => controller.abort() + return () => controller.abort() } static request() { return Push._getAdapter().request() } - static cancel() { - return Push._getAdapter().cancel() + static enable() { + return Push._getAdapter().enable() + } + + static disable() { + return Push._getAdapter().disable() } } diff --git a/src/lib/components/Drawer.svelte b/src/lib/components/Drawer.svelte index 07127488..ce85ecfe 100644 --- a/src/lib/components/Drawer.svelte +++ b/src/lib/components/Drawer.svelte @@ -12,7 +12,7 @@ onclick={onClose}>
{@render children?.()}
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index bf667449..8661fbac 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -28,7 +28,7 @@ import {setupAnalytics} from "@app/util/analytics" import {authPolicy, blockPolicy, trustPolicy, mostlyRestrictedPolicy} from "@app/util/policies" import {kv, db} from "@app/core/storage" - import {userSettingsValues} from "@app/core/state" + import {userSettingsValues, notificationSettings} from "@app/core/state" import {syncApplicationData} from "@app/core/sync" import * as commands from "@app/core/commands" import * as requests from "@app/core/requests" @@ -44,17 +44,6 @@ const policies = [authPolicy, blockPolicy, trustPolicy, mostlyRestrictedPolicy] - PushNotifications.addListener( - "pushNotificationActionPerformed", - async (action: ActionPerformed) => { - console.log('====== action', JSON.stringify(action)) - const event = parseJson(action.notification.data.event) - const relays = [action.notification.data.relay] - - goto(await getEventPath(event, relays)) - }, - ) - // Add stuff to window for convenience Object.assign(window, { get, @@ -114,6 +103,11 @@ store: shouldUnwrap, storage: kv, }), + sync({ + key: "notificationSettings", + store: notificationSettings, + storage: kv, + }), ]) // Set up our storage adapters diff --git a/src/routes/settings/alerts/+page.svelte b/src/routes/settings/alerts/+page.svelte index 29022fce..2535ac3e 100644 --- a/src/routes/settings/alerts/+page.svelte +++ b/src/routes/settings/alerts/+page.svelte @@ -1,63 +1,73 @@
@@ -72,15 +82,14 @@ + bind:checked={settings.badge} /> {/if} {/await} {#if !Capacitor.isNativePlatform()}

Play sound for new activity

- +
{/if}
@@ -88,39 +97,38 @@ + bind:checked={settings.push} />
Alert Types

Notify me about new activity

- +

Always notify me when mentioned

- +

Notify me about new messages

+ bind:checked={settings.messages} />
Muted Rooms - {#each settings.muted_rooms as id (id)} + {#each muted_rooms as id (id)} {@const [url, h] = splitRoomId(id)}
- - + +