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 122d9c64..01667a8c 100644 Binary files a/pnpm-lock.yaml and b/pnpm-lock.yaml differ 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 @@