Files
flotilla/src/app/util/push/adapters/common.ts
T

209 lines
5.4 KiB
TypeScript

import {goto} from "$app/navigation"
import type {Subscriber, Unsubscriber} from "svelte/store"
import {
PushNotifications,
type ActionPerformed,
type RegistrationError,
type Token,
} from "@capacitor/push-notifications"
import type {PluginListenerHandle} from "@capacitor/core"
import {pubkey, repository, tracker, userMessagingRelayList} from "@welshman/app"
import {merged} from "@welshman/store"
import {assoc, call, now, on, poll, spec, throttle} from "@welshman/lib"
import {load, LOCAL_RELAY_URL} from "@welshman/net"
import type {RepositoryUpdate} from "@welshman/net"
import {
getIdFilters,
getRelaysFromList,
getTagValue,
matchFilters,
type Filter,
type TrustedEvent,
} from "@welshman/util"
import {
DM_KINDS,
CONTENT_KINDS,
MESSAGE_KINDS,
notificationSettings,
pushState,
shouldNotify,
userSpaceUrls,
userSettingsValues,
makeCommentFilter,
} from "@app/core/state"
import {makeSpacePath, getEventPath} from "@app/util/routes"
export interface IPushAdapter {
request: (prompt?: boolean) => Promise<string>
disable: () => Promise<void>
enable: () => Promise<void>
}
export type PushPermissionResult = {
token?: string
error?: string
}
export const onNotification = call(() => {
const allFilters = [{kinds: [...MESSAGE_KINDS, ...DM_KINDS]}, makeCommentFilter(MESSAGE_KINDS)]
const filters = allFilters.map(assoc("since", now()))
const subscribers: Subscriber<TrustedEvent>[] = []
let unsubscribe: Unsubscriber | undefined
return (f: (event: TrustedEvent) => void) => {
subscribers.push(f)
if (!unsubscribe) {
unsubscribe = on(repository, "update", ({added}: RepositoryUpdate) => {
const $pubkey = pubkey.get()
for (const event of added) {
if (event.pubkey == $pubkey) {
continue
}
const h = getTagValue("h", event.tags)
if (Array.from(tracker.getRelays(event.id)).every(url => !shouldNotify(url, h))) {
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 onPushNotificationAction = async (action: ActionPerformed) => {
const {relay, id} = action.notification.data
const [event] = await load({
relays: [relay, LOCAL_RELAY_URL],
filters: getIdFilters([id]),
})
if (event) {
goto(await getEventPath(event, [relay]))
} else {
goto(makeSpacePath(relay))
}
}
export const requestPermissions = async (): Promise<string> => {
let status = await PushNotifications.checkPermissions()
if (["prompt", "prompt-with-rationale"].includes(status.receive)) {
status = await PushNotifications.requestPermissions()
}
return status.receive
}
export const requestToken = async (): Promise<PushPermissionResult> => {
let {token} = pushState.get()
let error = "failed to retrieve token"
if (!token) {
const listeners = [
PushNotifications.addListener("registration", ({value}: Token) => {
token = value
}),
PushNotifications.addListener("registrationError", (err: RegistrationError) => {
error = err.error
}),
]
await Promise.all([
PushNotifications.register(),
poll({
condition: () => Boolean(token),
signal: AbortSignal.timeout(5000),
}),
])
listeners.forEach(p => p.then((listener: PluginListenerHandle) => listener.remove()))
}
return token ? {token} : {error}
}
export const syncRelaySubscriptions = (
signal: AbortSignal,
sync: (url: string, key: string, filters: Filter[], ignore: Filter[]) => void,
) => {
const $pubkey = pubkey.get()
if (!$pubkey) {
throw new Error("Attempted to sync push subscriptions without an active pubkey")
}
const unsubscribeSpaces = merged([
userSpaceUrls,
notificationSettings,
userSettingsValues,
]).subscribe(
throttle(3000, ([$userSpaceUrls, {spaces, mentions}, {alerts}]) => {
const baseFilters = [{kinds: MESSAGE_KINDS}, makeCommentFilter(CONTENT_KINDS)]
for (const url of $userSpaceUrls) {
const {notify = true, exceptions = []} = alerts.find(spec({url})) || {}
const filters: Filter[] = []
const ignore: Filter[] = []
if (spaces) {
if (notify) {
if (exceptions.length > 0) {
ignore.push({"#h": exceptions})
}
filters.push(...baseFilters)
} else {
if (exceptions.length > 0) {
filters.push(...baseFilters.map(f => ({...f, "#h": exceptions})))
}
}
}
if (mentions) {
filters.push(...baseFilters.map(f => ({...f, "#p": [$pubkey]})))
}
sync(url, "spaces", filters, ignore)
}
}),
)
const unsubscribeMessages = merged([userMessagingRelayList, notificationSettings]).subscribe(
throttle(3000, ([$userMessagingRelayList, {messages}]) => {
for (const url of getRelaysFromList($userMessagingRelayList)) {
const filters: Filter[] = []
if (messages) {
filters.push({kinds: DM_KINDS, "#p": [$pubkey]})
}
sync(url, "messages", filters, [])
}
}),
)
signal.addEventListener("abort", () => {
unsubscribeSpaces()
unsubscribeMessages()
})
}