diff --git a/src/app/mail/index.ts b/src/app/mail/index.ts index 9d4644f8..64127ff3 100644 --- a/src/app/mail/index.ts +++ b/src/app/mail/index.ts @@ -1,12 +1,76 @@ -import {hash, maybe, throttle} from "@welshman/lib" +import {hash, maybe, spec, throttle} from "@welshman/lib" import type {Filter} from "@welshman/util" -import {Address, DELETE, makeEvent, makeHttpAuth, makeHttpAuthHeader} from "@welshman/util" +import {Address, DELETE, MESSAGE, makeEvent, makeHttpAuth, makeHttpAuthHeader} from "@welshman/util" +import {merged} from "@welshman/store" import {Relays, User} from "@welshman/app" import {buildUrl} from "@lib/util" -import {app, thunks} from "@app/core" +import {app, messagingRelayLists, roomLists, thunks} from "@app/core" import {EMAIL_NOTIFICATION_SERVER, PUSH_BRIDGE} from "@app/env" import {ensureEmailSalt, userSettingsValues} from "@app/settings" -import {emailNotificationState, syncRelaySubscriptions} from "@app/push/adapters/common" +import {emailNotificationState} from "@app/push/adapters/common" +import {DM_KINDS, CONTENT_KINDS, makeCommentFilter} from "@app/content" + +// Email digests sync their own kind 30390 subscriptions. Unlike push, the email +// subscription is driven by the email digest setting (SettingsValues.email) plus +// per-space alert preferences, and must NOT be gated on the push notificationSettings +// toggles - otherwise turning off push "new activity" would delete the 30390 and stop +// email digests entirely. +const syncRelaySubscriptionsForEmail = ( + signal: AbortSignal, + sync: (url: string, key: string, filters: Filter[], ignore: Filter[]) => void, +) => { + const $pubkey = User.require(app.get()).pubkey + + const unsubscribeSpaces = merged([roomLists.get().urls($pubkey).$, userSettingsValues]).subscribe( + throttle(3000, ([$spaceUrls, {email, alerts}]) => { + const baseFilters = [{kinds: [MESSAGE, ...CONTENT_KINDS]}, makeCommentFilter(CONTENT_KINDS)] + + for (const url of $spaceUrls) { + const filters: Filter[] = [] + const ignore: Filter[] = [] + + if (!email) { + sync(url, "spaces", filters, []) + continue + } + + const {notify = true, exceptions = []} = alerts.find(spec({url})) || {} + + 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}))) + } + + // Mentions are part of the digest regardless of push settings, and + // guarantee there is always at least one filter so the subscription + // stays live while email digests are enabled. + filters.push(...baseFilters.map(f => ({...f, "#p": [$pubkey]}))) + + sync(url, "spaces", filters, ignore) + } + }), + ) + + const unsubscribeMessages = merged([ + messagingRelayLists.get().urls($pubkey).$, + userSettingsValues, + ]).subscribe( + throttle(3000, ([$messagingUrls, {email}]) => { + for (const url of $messagingUrls) { + sync(url, "messages", email ? [{kinds: DM_KINDS, "#p": [$pubkey]}] : [], []) + } + }), + ) + + signal.addEventListener("abort", () => { + unsubscribeSpaces() + unsubscribeMessages() + }) +} export class MailNotifications { _controller = maybe() @@ -96,8 +160,9 @@ export class MailNotifications { await this.register(email, frequency) } - // Sync kind 30390 subscriptions using the same filters as push - syncRelaySubscriptions( + // Sync kind 30390 subscriptions for the email digest. Filters are derived + // from the email setting + per-space alerts, independent of push toggles. + syncRelaySubscriptionsForEmail( this._controller.signal, async (url: string, key: string, filters: Filter[], ignore: Filter[]) => { const {callback} = emailNotificationState.get()