209 lines
5.4 KiB
TypeScript
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()
|
|
})
|
|
}
|