From ca3d66183097512ed8eab452a8f5a573244b7f8e Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Thu, 22 Jan 2026 14:23:46 -0800 Subject: [PATCH] Generally just refactor alerts, upgrade some deps --- .ackrc | 1 + android/capacitor.settings.gradle | 20 +- android/variables.gradle | 4 +- ios/App/Podfile | 22 +- package.json | 24 +- pnpm-lock.yaml | Bin 348627 -> 352247 bytes .../components/NewNotificationSound.svelte | 26 +- src/app/components/SignerStatus.svelte | 2 +- src/app/components/SpaceMenu.svelte | 6 +- src/app/core/requests.ts | 5 - src/app/core/state.ts | 16 +- src/app/util/notifications.ts | 329 ++++++++++-------- src/lib/components/Link.svelte | 2 +- src/routes/+layout.svelte | 4 +- src/routes/settings/alerts/+page.svelte | 56 +-- src/routes/spaces/[relay]/[h]/+page.svelte | 18 +- 16 files changed, 292 insertions(+), 243 deletions(-) diff --git a/.ackrc b/.ackrc index fca27f60..7811c3c8 100644 --- a/.ackrc +++ b/.ackrc @@ -3,6 +3,7 @@ --ignore-dir=build --ignore-dir=ios/DerivedData --ignore-dir=ios/App/App/public +--ignore-dir=ios/App/Pods --ignore-file=match:.svg --ignore-file=match:package-lock.json diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 392e4f51..b9f239c9 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -1,30 +1,30 @@ // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN include ':capacitor-android' -project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/android/capacitor') +project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/android/capacitor') include ':capacitor-community-safe-area' -project(':capacitor-community-safe-area').projectDir = new File('../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.4.3/node_modules/@capacitor-community/safe-area/android') +project(':capacitor-community-safe-area').projectDir = new File('../node_modules/.pnpm/@capacitor-community+safe-area@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor-community/safe-area/android') include ':capacitor-app' -project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@7.1.0_@capacitor+core@7.4.3/node_modules/@capacitor/app/android') +project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/app/android') include ':capacitor-filesystem' -project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@7.1.4_@capacitor+core@7.4.3/node_modules/@capacitor/filesystem/android') +project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@8.1.0_@capacitor+core@8.0.1/node_modules/@capacitor/filesystem/android') include ':capacitor-keyboard' -project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/keyboard/android') +project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/keyboard/android') include ':capacitor-preferences' -project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.4.3/node_modules/@capacitor/preferences/android') +project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/preferences/android') include ':capacitor-push-notifications' -project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/push-notifications/android') +project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications/android') include ':capawesome-capacitor-android-dark-mode-support' -project(':capawesome-capacitor-android-dark-mode-support').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-android-dark-mode-support@7.0.0_@capacitor+core@7.4.3/node_modules/@capawesome/capacitor-android-dark-mode-support/android') +project(':capawesome-capacitor-android-dark-mode-support').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-android-dark-mode-support@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-android-dark-mode-support/android') include ':capawesome-capacitor-badge' -project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.4.3/node_modules/@capawesome/capacitor-badge/android') +project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge/android') include ':nostr-signer-capacitor-plugin' -project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.4.3/node_modules/nostr-signer-capacitor-plugin/android') +project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@8.0.1/node_modules/nostr-signer-capacitor-plugin/android') diff --git a/android/variables.gradle b/android/variables.gradle index 8c75136a..b2f2ac12 100644 --- a/android/variables.gradle +++ b/android/variables.gradle @@ -1,7 +1,7 @@ ext { minSdkVersion = 23 - compileSdkVersion = 35 - targetSdkVersion = 35 + compileSdkVersion = 36 + targetSdkVersion = 36 androidxActivityVersion = '1.9.2' //https://github.com/ionic-team/capacitor/issues/7866 // androidxAppCompatVersion = '1.7.0' diff --git a/ios/App/Podfile b/ios/App/Podfile index 13f4a851..03d6927d 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -1,4 +1,4 @@ -require_relative '../../node_modules/.pnpm/@capacitor+ios@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/ios/scripts/pods_helpers' +require_relative '../../node_modules/.pnpm/@capacitor+ios@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/ios/scripts/pods_helpers' platform :ios, '14.0' use_frameworks! @@ -9,16 +9,16 @@ use_frameworks! install! 'cocoapods', :disable_input_output_paths => true def capacitor_pods - pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/ios' - pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/ios' - pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.4.3/node_modules/@capacitor-community/safe-area' - pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@7.1.0_@capacitor+core@7.4.3/node_modules/@capacitor/app' - pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@7.1.4_@capacitor+core@7.4.3/node_modules/@capacitor/filesystem' - pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/keyboard' - pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.4.3/node_modules/@capacitor/preferences' - pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/push-notifications' - pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.4.3/node_modules/@capawesome/capacitor-badge' - pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.4.3/node_modules/nostr-signer-capacitor-plugin' + pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/ios' + pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/ios' + pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor-community/safe-area' + pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/app' + pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@8.1.0_@capacitor+core@8.0.1/node_modules/@capacitor/filesystem' + pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/keyboard' + pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/preferences' + pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications' + pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge' + pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@8.0.1/node_modules/nostr-signer-capacitor-plugin' end target 'Flotilla Chat' do diff --git a/package.json b/package.json index 1fe6acf8..1acdf500 100644 --- a/package.json +++ b/package.json @@ -37,18 +37,18 @@ }, "type": "module", "dependencies": { - "@capacitor-community/safe-area": "7.0.0-alpha.1", - "@capacitor/android": "^7.4.3", - "@capacitor/app": "^7.1.0", - "@capacitor/cli": "^7.4.3", - "@capacitor/core": "^7.4.3", - "@capacitor/filesystem": "^7.1.4", - "@capacitor/ios": "^7.4.3", - "@capacitor/keyboard": "^7.0.3", - "@capacitor/preferences": "^7.0.2", - "@capacitor/push-notifications": "^7.0.3", - "@capawesome/capacitor-android-dark-mode-support": "^7.0.0", - "@capawesome/capacitor-badge": "^7.0.1", + "@capacitor-community/safe-area": "^8.0.1", + "@capacitor/android": "^8.0.1", + "@capacitor/app": "^8.0.0", + "@capacitor/cli": "^8.0.1", + "@capacitor/core": "^8.0.1", + "@capacitor/filesystem": "^8.1.0", + "@capacitor/ios": "^8.0.1", + "@capacitor/keyboard": "^8.0.0", + "@capacitor/preferences": "^8.0.0", + "@capacitor/push-notifications": "^8.0.0", + "@capawesome/capacitor-android-dark-mode-support": "^8.0.0", + "@capawesome/capacitor-badge": "^8.0.0", "@getalby/lightning-tools": "^6.0.0", "@getalby/sdk": "^5.1.2", "@pomade/core": "^0.0.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 122d9c6457d6b5aa4795f297201856e287a57587..01667a8cc8db1a91e66894c60c161e619e41d319 100644 GIT binary patch delta 20310 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 F{{i5|mMH)L delta 19041 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 diff --git a/src/app/components/NewNotificationSound.svelte b/src/app/components/NewNotificationSound.svelte index 7fcd7e30..18d499b8 100644 --- a/src/app/components/NewNotificationSound.svelte +++ b/src/app/components/NewNotificationSound.svelte @@ -1,7 +1,7 @@ diff --git a/src/app/components/SignerStatus.svelte b/src/app/components/SignerStatus.svelte index c8690eed..3e7054f5 100644 --- a/src/app/components/SignerStatus.svelte +++ b/src/app/components/SignerStatus.svelte @@ -47,7 +47,7 @@ {/if} -
+

Logged in with {#if $session.method === SessionMethod.Nip01} diff --git a/src/app/components/SpaceMenu.svelte b/src/app/components/SpaceMenu.svelte index a4c755be..311d02c2 100644 --- a/src/app/components/SpaceMenu.svelte +++ b/src/app/components/SpaceMenu.svelte @@ -103,7 +103,7 @@

- +
-
+
{#if hasNip29($relay)} Recent Activity @@ -239,7 +239,7 @@ {/if}
-
+
diff --git a/src/app/core/requests.ts b/src/app/core/requests.ts index b32d9c5a..448a8fe6 100644 --- a/src/app/core/requests.ts +++ b/src/app/core/requests.ts @@ -15,11 +15,6 @@ import { import { EVENT_TIME, RELAY_INVITE, - ALERT_EMAIL, - ALERT_WEB, - ALERT_IOS, - ALERT_ANDROID, - ALERT_STATUS, matchFilters, getTagValue, getAddress, diff --git a/src/app/core/state.ts b/src/app/core/state.ts index 5d366bf9..ea3ed80a 100644 --- a/src/app/core/state.ts +++ b/src/app/core/state.ts @@ -1,7 +1,7 @@ import twColors from "tailwindcss/colors" import {context as pomadeContext} from "@pomade/core" import {Capacitor} from "@capacitor/core" -import {get, derived, readable, writable} from "svelte/store" +import {derived, readable, writable} from "svelte/store" import * as nip19 from "nostr-tools/nip19" import { on, @@ -323,11 +323,9 @@ export const settingsByPubkey = deriveItemsByKey({ export const getSettingsByPubkey = getter(settingsByPubkey) -export const getSettings = (pubkey: string) => getSettingsByPubkey().get(pubkey) - export const loadSettings = makeLoadItem( makeOutboxLoader(APP_DATA, {"#d": [SETTINGS]}), - getSettings, + (pubkey: string) => getSettingsByPubkey().get(pubkey), ) export const userSettings = makeUserData(settingsByPubkey, loadSettings) @@ -336,7 +334,9 @@ export const loadUserSettings = makeUserLoader(loadSettings) export const userSettingsValues = derived(userSettings, $s => $s?.values || defaultSettings) -export const getSetting = (key: keyof Settings["values"]) => get(userSettingsValues)[key] as T +export const getSettings = getter(userSettingsValues) + +export const getSetting = (key: keyof Settings["values"]) => getSettings()[key] as T // Relays sending events with empty signatures that the user has to choose to trust @@ -346,11 +346,11 @@ export const relaysPendingTrust = writable([]) export const relaysMostlyRestricted = writable>({}) -// Alerts +// Push notifications -export const alertToken = writable() +export const pushToken = writable() -export const alertSecret = writable() +export const pushSecret = writable() // Chats diff --git a/src/app/util/notifications.ts b/src/app/util/notifications.ts index 40fc17a8..e684613c 100644 --- a/src/app/util/notifications.ts +++ b/src/app/util/notifications.ts @@ -1,4 +1,4 @@ -import type {Unsubscriber} from "svelte/store" +import type {Unsubscriber, Subscriber} from "svelte/store" import {derived, get} from "svelte/store" import {Capacitor} from "@capacitor/core" import {Badge} from "@capawesome/capacitor-badge" @@ -14,17 +14,19 @@ import { publishThunk, getPubkeyRelays, loadRelay, + waitForThunkError, } from "@welshman/app" +import type {Maybe +} from "@welshman/lib" import { + on, + call, + assoc, poll, prop, sha256, textEncoder, parseJson, - ms, - maybe, - int, - MINUTE, flatten, find, spec, @@ -32,18 +34,12 @@ import { identity, now, groupBy, - hash, tryCatch, postJson, fetchJson, } from "@welshman/lib" -import type {TrustedEvent} from "@welshman/util" -import { - deriveEventsByIdByUrl, - deriveEventsById, - deriveEventsDesc, - deriveDeduplicated, -} from "@welshman/store" +import type {TrustedEvent, Filter} from "@welshman/util" +import {deriveEventsByIdByUrl} from "@welshman/store" import { ZAP_GOAL, EVENT_TIME, @@ -73,16 +69,18 @@ import { MESSAGE_KINDS, PUSH_BRIDGE, PUSH_SERVER, - alertToken, - alertSecret, + pushToken, + pushSecret, chatsById, hasNip29, - getSetting, + getSettings, + userSettingsValues, userGroupList, getSpaceUrlsFromGroupList, getSpaceRoomsFromGroupList, makeCommentFilter, userSpaceUrls, + makeRoomId, } from "@app/core/state" import {kv} from "@app/core/storage" import {goto} from "$app/navigation" @@ -104,7 +102,7 @@ export const setChecked = (key: string) => checked.update(state => ({...state, [ const goalCommentFilters = [{kinds: [COMMENT], "#K": [String(ZAP_GOAL)]}] const threadCommentFilters = [{kinds: [COMMENT], "#K": [String(THREAD)]}] const calendarCommentFilters = [{kinds: [COMMENT], "#K": [String(EVENT_TIME)]}] -const messageFilters = [{kinds: CONTENT_KINDS}] +const messageFilters = [{kinds: MESSAGE_KINDS}] const dmFilters = [{kinds: DM_KINDS}] const allFilters = flatten([ goalCommentFilters, @@ -114,11 +112,6 @@ const allFilters = flatten([ dmFilters, ]) -export const latestNotification = deriveDeduplicated( - deriveEventsDesc(deriveEventsById({repository, filters: allFilters})), - first, -) - export const notifications = derived( throttled( 1000, @@ -268,55 +261,107 @@ export const notifications = derived( }, ) -// Badges +export const onNotification = call(() => { + const filters = allFilters.map(assoc("since", now())) + const subscribers: Subscriber[] = [] -export const badgeCount = derived(notifications, notifications => { - return notifications.size + let unsubscribe: Unsubscriber | undefined + + return (f: (event: TrustedEvent) => void) => { + subscribers.push(f) + + if (!unsubscribe) { + unsubscribe = on(repository, "update", ({added}) => { + const $pubkey = pubkey.get() + const {muted_rooms} = getSettings() + + for (const event of added) { + if (event.pubkey == $pubkey) { + continue + } + + const h = getTagValue("h", event.tags) + const muted = Array.from(tracker.getRelays(event.id)).every( + url => h && muted_rooms.includes(makeRoomId(url, h)), + ) + + if (muted) { + continue + } + + if (matchFilters(filters, event)) { + for (const f of subscribers) { + f(event) + } + } + } + }) + } + + return () => { + subscribers.splice(subscribers.indexOf(f), 1) + + if (subscribers.length === 0) { + unsubscribe?.() + unsubscribe = undefined + } + } + } }) -export const handleBadgeCountChanges = async (count: number) => { - if (getSetting("alerts_badge")) { - try { - await Badge.set({count}) - } catch (err) { - // failed to set badge - } - } else { - await clearBadges() - } -} +// Badges + +export const syncBadges = () => + derived([notifications, userSettingsValues], identity).subscribe( + async ([$notifications, {alerts_badge}]) => { + if (alerts_badge) { + try { + await Badge.set({count: $notifications.size}) + } catch (err) { + // pass - firefox doesn't support badges + } + } else { + await clearBadges() + } + }, + ) export const clearBadges = async () => { try { await Badge.clear() } catch (e) { - // Pass - firefox doesn't support this + // pass - firefox doesn't support this } } // Local notifications -interface IAlertsAdapter { +interface IPushAdapter { request: () => Promise start: () => Unsubscriber } -class CapacitorNotifications implements IAlertsAdapter { - async ensureSubscription() { - const token = get(alertToken) +class CapacitorNotifications implements IPushAdapter { + async sync() { + const token = get(pushToken) if (!token) { return } - const info = await tryCatch(async () => { - const secret = get(alertSecret) + const info: Maybe<{ + secret: string + callback: string + }> = await tryCatch(async () => { + const secret = get(pushSecret) if (secret) { const {callback} = await fetchJson(`${PUSH_SERVER}/subscription/${secret}`) if (callback) { return {secret, callback} + } else { + pushSecret.set(undefined) } } @@ -328,92 +373,85 @@ class CapacitorNotifications implements IAlertsAdapter { const json = await postJson(`${PUSH_SERVER}/subscription/${channel}`, data) if (json) { - return { - secret: json.sk, - callback: json.callback, - } as { - secret: string - callback: string - } + pushSecret.set(json.sk) + + return {secret: json.sk, callback: json.callback} } }) - if (info) { - alertSecret.set(info.secret) + if (!info) { + return + } - const getPushStuff = async (url: string) => { - let relay = await loadRelay(url) + const getPushStuff = async (url: string) => { + let relay = await loadRelay(url) - if (!relay?.self || !relay?.supported_nips?.includes("9a")) { - relay = await loadRelay(PUSH_BRIDGE) - } - - if (relay?.self) { - return {url: relay.url, pubkey: relay.self} - } + if (!relay?.self || !relay?.supported_nips?.includes("9a")) { + relay = await loadRelay(PUSH_BRIDGE) } - for (const url of get(userSpaceUrls)) { - const stuff = await getPushStuff(url) + if (relay?.self) { + return {url: relay.url, pubkey: relay.self} + } + } - if (!stuff) { - console.warn(`Failed to subscribe ${url} to space notifications`) - continue - } + const syncSubscription = async ( + key: string, + relay: string, + filters: Filter[], + ignore: Filter[] = [], + ) => { + const stuff = await getPushStuff(relay) - const filters = [{kinds: MESSAGE_KINDS}, makeCommentFilter(CONTENT_KINDS)] - // const ignore = [] todo - muted rooms + if (!stuff) { + 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)) - publishThunk({ - relays: [stuff.url], + const thunk = publishThunk({ + relays: [url], event: makeEvent(30390, { - content: await signer.get().nip44.encrypt( - stuff.pubkey, - JSON.stringify([ - ["relay", url], - ["callback", info.callback], - // ...ignore.map(filter => ["ignore", JSON.stringify(filter)]), - ...filters.map(filter => ["filter", JSON.stringify(filter)]), - ]), - ), + content: await signer + .get() + .nip44.encrypt( + pubkey, + JSON.stringify([ + ["relay", relay], + ["callback", info.callback], + ...ignore.map(filter => ["ignore", JSON.stringify(filter)]), + ...filters.map(filter => ["filter", JSON.stringify(filter)]), + ]), + ), tags: [ - ["d", await sha256(textEncoder.encode(info.callback + url + "spaces"))], - ["p", stuff.pubkey], + ["d", identifier], + ["p", pubkey], ], }), }) - } - const $pubkey = pubkey.get()! + const error = await waitForThunkError(thunk) - for (const url of getPubkeyRelays($pubkey, RelayMode.Messaging)) { - const stuff = await getPushStuff(url) - - if (!stuff) { - console.warn(`Failed to subscribe ${url} to messaging notifications`) - continue + if (error) { + console.warn(`Failed to subscribe ${relay} to ${key} notifications:`, error) } - - publishThunk({ - relays: [stuff.url], - event: makeEvent(30390, { - content: await signer.get().nip44.encrypt( - stuff.pubkey, - JSON.stringify([ - ["relay", url], - ["callback", info.callback], - ["filter", JSON.stringify({kinds: DM_KINDS, "#p": [$pubkey]})], - ]), - ), - tags: [ - ["d", await sha256(textEncoder.encode(info.callback + url + "messages"))], - ["p", stuff.pubkey], - ], - }), - }) } - } else { - alertSecret.set(undefined) + } + + for (const relay of get(userSpaceUrls)) { + const {muted_rooms} = getSettings() + const filters = [{kinds: MESSAGE_KINDS}, makeCommentFilter(CONTENT_KINDS)] + const ignore = [{"#h": [muted_rooms]}] + + syncSubscription("spaces", relay, filters, ignore) + } + + const $pubkey = pubkey.get()! + + for (const relay of getPubkeyRelays($pubkey, RelayMode.Messaging)) { + const filters = [{kinds: DM_KINDS, "#p": [$pubkey]}] + + syncSubscription("messages", relay, filters) } } @@ -445,18 +483,18 @@ class CapacitorNotifications implements IAlertsAdapter { }) if (token) { - alertToken.set(token) + pushToken.set(token) return "granted" } - alertToken.set(undefined) + pushToken.set(undefined) return "denied" } start() { - this.ensureSubscription().then(() => { + this.sync().then(() => { PushNotifications.addListener( "pushNotificationActionPerformed", async (action: ActionPerformed) => { @@ -472,7 +510,7 @@ class CapacitorNotifications implements IAlertsAdapter { } } -class WebNotifications implements IAlertsAdapter { +class WebNotifications implements IPushAdapter { async request() { if (Notification?.permission === "default") { await Notification.requestPermission() @@ -506,21 +544,19 @@ class WebNotifications implements IAlertsAdapter { } start() { - let initialized = false + return onNotification(event => { + const {alerts_messages, alerts_mentions, alerts_spaces} = getSettings() - return latestNotification.subscribe(event => { - if (!initialized) { - initialized = true - } else if (event && document.hidden && Notification?.permission === "granted") { - if (getSetting("alerts_messages") && matchFilters(dmFilters, event)) { + if (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 ( + alerts_mentions && event.pubkey !== pubkey.get() && - getSetting("alerts_mentions") && getPubkeyTagValues(event.tags).includes(pubkey.get()!) ) { this.notify(event, "Someone mentioned you", "Someone tagged you in a message.") - } else if (getSetting("alerts_spaces")) { + } else if (alerts_spaces) { this.notify(event, "New activity", "Someone posted a new message.") } } @@ -528,49 +564,56 @@ class WebNotifications implements IAlertsAdapter { } } -export class Alerts { - static _adapter: IAlertsAdapter | undefined +export class Push { + static _adapter: IPushAdapter | undefined static _unsubscriber: Unsubscriber | undefined static _getAdapter() { - if (!Alerts._adapter) { + if (!Push._adapter) { if (Capacitor.isNativePlatform()) { - Alerts._adapter = new CapacitorNotifications() + Push._adapter = new CapacitorNotifications() } else { - Alerts._adapter = new WebNotifications() + Push._adapter = new WebNotifications() } } - return Alerts._adapter + return Push._adapter } static start() { - return Alerts._getAdapter().start() + return Push._getAdapter().start() } static request() { - return Alerts._getAdapter().request() + return Push._getAdapter().request() } static resume() { - if (getSetting("alerts_push")) { - const promise = Alerts.request() - const controller = new AbortController() + 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", Alerts.start()) - } - }) + promise.then(permissions => { + if (permissions === "granted" && !controller.signal.aborted) { + controller.signal.addEventListener("abort", Push.start()) + } + }) - Alerts._unsubscriber = () => controller.abort() + Push._unsubscriber = () => controller.abort() + } else { + Push.stop() + } + }) + + return () => { + unsubscribe() + Push.stop() } - - return Alerts.stop } static stop() { - Alerts._unsubscriber?.() - Alerts._unsubscriber = undefined + Push._unsubscriber?.() + Push._unsubscriber = undefined } } diff --git a/src/lib/components/Link.svelte b/src/lib/components/Link.svelte index 61e18bb0..1226c5b6 100644 --- a/src/lib/components/Link.svelte +++ b/src/lib/components/Link.svelte @@ -16,7 +16,7 @@ replaceState?: boolean disabled?: boolean class?: string - 'data-tip'?: string + "data-tip"?: string } = $props() const go = (e: Event) => { diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 384f5992..f85f92aa 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -123,13 +123,13 @@ unsubscribers.push(setupHistory(), setupAnalytics(), syncApplicationData()) // Subscribe to badge count for changes - unsubscribers.push(notifications.badgeCount.subscribe(notifications.handleBadgeCountChanges)) + unsubscribers.push(notifications.syncBadges) // Initialize keyboard state tracking unsubscribers.push(syncKeyboard()) // Initialize background notifications - unsubscribers.push(notifications.Alerts.resume()) + unsubscribers.push(notifications.Push.resume()) // Listen for signer errors, report to user via toast unsubscribers.push( diff --git a/src/routes/settings/alerts/+page.svelte b/src/routes/settings/alerts/+page.svelte index 6a753e13..acdcb61f 100644 --- a/src/routes/settings/alerts/+page.svelte +++ b/src/routes/settings/alerts/+page.svelte @@ -2,13 +2,15 @@ import cx from "classnames" import {sleep, remove} from "@welshman/lib" import {displayRelayUrl} from "@welshman/util" + import {Capacitor} from "@capacitor/core" + import {Badge} from "@capawesome/capacitor-badge" import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import {preventDefault} from "@lib/html" import Icon from "@lib/components/Icon.svelte" import Button from "@lib/components/Button.svelte" import RoomName from "@app/components/RoomName.svelte" import {pushToast} from "@app/util/toast" - import {Alerts, clearBadges} from "@app/util/notifications" + import {Push, clearBadges} from "@app/util/notifications" import {userSettingsValues, splitRoomId} from "@app/core/state" import {publishSettings} from "@app/core/commands" @@ -24,7 +26,7 @@ const onAlertsPushChange = () => { if (settings.alerts_push) { - Alerts.request().then(permissions => { + Push.request().then(permissions => { if (permissions !== "granted") { sleep(300).then(() => { settings.alerts_push = false @@ -46,12 +48,6 @@ const onsubmit = preventDefault(async () => { await publishSettings($state.snapshot(settings)) - if (settings.alerts_push) { - await Alerts.start() - } else { - await Alerts.stop() - } - pushToast({message: "Your settings have been saved!"}) }) @@ -61,18 +57,26 @@
Alert Settings -
-

Show badge for unread alerts

- -
-
-

Play sound for new activity

- -
+ {#await Badge.isSupported()} + + {:then { isSupported }} + {#if isSupported} +
+

Show badge for unread alerts

+ +
+ {/if} + {/await} + {#if !Capacitor.isNativePlatform()} +
+

Play sound for new activity

+ +
+ {/if}

Enable push notifications

-
+
Alert Types

Notify me about new activity

@@ -100,7 +108,11 @@ bind:checked={settings.alerts_messages} />
-
+
Muted Rooms {#each settings.muted_rooms as id (id)} {@const [url, h] = splitRoomId(id)} diff --git a/src/routes/spaces/[relay]/[h]/+page.svelte b/src/routes/spaces/[relay]/[h]/+page.svelte index 31341290..1e2f8ccd 100644 --- a/src/routes/spaces/[relay]/[h]/+page.svelte +++ b/src/routes/spaces/[relay]/[h]/+page.svelte @@ -1,5 +1,4 @@