forked from coracle/flotilla
199 lines
5.5 KiB
TypeScript
199 lines
5.5 KiB
TypeScript
import {get} from "svelte/store"
|
|
import {Capacitor} from "@capacitor/core"
|
|
import {PushNotifications} from "@capacitor/push-notifications"
|
|
import {
|
|
pubkey,
|
|
publishThunk,
|
|
loadRelay,
|
|
waitForThunkError,
|
|
userMessagingRelayList,
|
|
} from "@welshman/app"
|
|
import {assoc, hash, maybe} from "@welshman/lib"
|
|
import type {Filter} from "@welshman/util"
|
|
import {DELETE, getRelaysFromList, makeEvent, Address} from "@welshman/util"
|
|
import {buildUrl} from "@lib/util"
|
|
import {PUSH_BRIDGE, PUSH_SERVER, pushState, userSpaceUrls, device} from "@app/core/state"
|
|
import type {IPushAdapter} from "@app/util/push/adapters/common"
|
|
import {
|
|
onPushNotificationAction,
|
|
syncRelaySubscriptions,
|
|
requestPermissions,
|
|
requestToken,
|
|
} from "@app/util/push/adapters/common"
|
|
|
|
export class CapacitorNotifications implements IPushAdapter {
|
|
_controller = maybe<AbortController>()
|
|
|
|
async request() {
|
|
const status = await requestPermissions()
|
|
|
|
if (status !== "granted") {
|
|
return status
|
|
}
|
|
|
|
const {token, error = "denied"} = await requestToken()
|
|
|
|
pushState.update(assoc("token", token))
|
|
|
|
return token ? "granted" : error
|
|
}
|
|
|
|
async _syncServer(signal: AbortSignal) {
|
|
const {token, subscription} = pushState.get()
|
|
|
|
if (!token) {
|
|
throw new Error("Attempted to sync push server without a token")
|
|
}
|
|
|
|
if (!subscription) {
|
|
try {
|
|
const channel = Capacitor.getPlatform() === "ios" ? "apns" : "fcm"
|
|
const url = buildUrl(PUSH_SERVER, "subscription", channel)
|
|
const res = await fetch(url, {
|
|
signal,
|
|
method: "POST",
|
|
body: JSON.stringify({token}),
|
|
headers: {
|
|
Accept: "application/json",
|
|
"Content-Type": "application/json",
|
|
},
|
|
})
|
|
|
|
if (!res.ok) {
|
|
console.warn(`Failed to register with push server (status ${res.status})`)
|
|
} else {
|
|
const json = await res.json()
|
|
|
|
if (json?.callback && json?.key) {
|
|
pushState.update(assoc("subscription", json))
|
|
} else {
|
|
console.warn("Failed to register with push server (bad response)")
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn("Failed to register with push server:", e)
|
|
}
|
|
}
|
|
}
|
|
|
|
_getSubscriptionIdentifier = (relay: string, key: string) =>
|
|
String(hash(relay + key + device.get()))
|
|
|
|
_getPushUrl = async (url: string) => {
|
|
for (const candidate of [url, PUSH_BRIDGE]) {
|
|
const relay = await loadRelay(candidate)
|
|
|
|
if (relay?.supported_nips?.map(String)?.includes("9a")) {
|
|
return candidate
|
|
}
|
|
}
|
|
}
|
|
|
|
_syncRelay = async (relay: string, key: string, filters: Filter[], ignore: Filter[] = []) => {
|
|
const {subscription} = pushState.get()
|
|
|
|
if (!subscription) {
|
|
console.warn(`Failed to subscribe ${relay} to notifications: no subscription`)
|
|
return
|
|
}
|
|
|
|
const url = await this._getPushUrl(relay)
|
|
|
|
if (!url) {
|
|
console.warn(`Failed to subscribe ${relay} to notifications: unsupported`)
|
|
return
|
|
}
|
|
|
|
const identifier = this._getSubscriptionIdentifier(relay, key)
|
|
|
|
const thunk = publishThunk({
|
|
relays: [url],
|
|
event: makeEvent(30390, {
|
|
tags: [
|
|
["d", identifier],
|
|
["relay", relay],
|
|
["callback", subscription.callback],
|
|
...ignore.map(filter => ["ignore", JSON.stringify(filter)]),
|
|
...filters.map(filter => ["filter", JSON.stringify(filter)]),
|
|
],
|
|
}),
|
|
})
|
|
|
|
const error = await waitForThunkError(thunk)
|
|
|
|
if (error) {
|
|
console.warn(`Failed to subscribe ${relay} to ${key} notifications:`, error)
|
|
}
|
|
}
|
|
|
|
_unsyncRelay = async (relay: string, key: string) => {
|
|
const url = await this._getPushUrl(relay)
|
|
|
|
if (!url) {
|
|
console.warn(`Failed to unsubscribe ${relay} from notifications: unsupported`)
|
|
return
|
|
}
|
|
|
|
const relays = [url]
|
|
const identifier = this._getSubscriptionIdentifier(relay, key)
|
|
const address = new Address(30390, pubkey.get()!, identifier).toString()
|
|
const event = makeEvent(DELETE, {tags: [["a", address]]})
|
|
const error = await waitForThunkError(publishThunk({relays, event}))
|
|
|
|
if (error) {
|
|
console.warn(`Failed to unsubscribe ${relay} from notifications:`, error)
|
|
}
|
|
}
|
|
|
|
async enable() {
|
|
if (!this._controller) {
|
|
this._controller = new AbortController()
|
|
|
|
PushNotifications.addListener("pushNotificationActionPerformed", onPushNotificationAction)
|
|
|
|
this._controller.signal.addEventListener("abort", () => {
|
|
PushNotifications.removeAllListeners()
|
|
})
|
|
|
|
try {
|
|
await this._syncServer(this._controller.signal)
|
|
|
|
syncRelaySubscriptions(this._controller.signal, (url, key, filters, ignore) => {
|
|
if (filters.length > 0) {
|
|
this._syncRelay(url, key, filters, ignore)
|
|
} else {
|
|
this._unsyncRelay(url, key)
|
|
}
|
|
})
|
|
} catch (e) {
|
|
console.error(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
async disable() {
|
|
this._controller?.abort()
|
|
this._controller = undefined
|
|
|
|
const {subscription} = pushState.get()
|
|
|
|
if (subscription) {
|
|
const res = await fetch(buildUrl(PUSH_SERVER, "subscription", subscription.key), {
|
|
method: "delete",
|
|
})
|
|
|
|
if (!res.ok) {
|
|
console.warn("Failed to delete push subscription")
|
|
}
|
|
}
|
|
|
|
pushState.set({})
|
|
|
|
await Promise.all(get(userSpaceUrls).map(url => this._unsyncRelay(url, "spaces")))
|
|
|
|
await Promise.all(
|
|
getRelaysFromList(get(userMessagingRelayList)).map(url => this._unsyncRelay(url, "messages")),
|
|
)
|
|
}
|
|
}
|