Add email digest schedule controls to Alert Settings #5
3 changed files with 147 additions and 39 deletions
|
|
@ -79,7 +79,14 @@ export class MailNotifications {
|
||||||
// Mail.sync) coalesce into a single PUT instead of racing two subscriptions.
|
// Mail.sync) coalesce into a single PUT instead of racing two subscriptions.
|
||||||
_register = maybe<Promise<void>>()
|
_register = maybe<Promise<void>>()
|
||||||
|
|
||||||
async register(email: string, frequency: string) {
|
async register(
|
||||||
|
email: string,
|
||||||
|
frequency: string,
|
||||||
|
hour = 17,
|
||||||
|
minute = 0,
|
||||||
|
dayOfWeek?: number,
|
||||||
|
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
|
||||||
|
) {
|
||||||
if (this._register) return this._register
|
if (this._register) return this._register
|
||||||
|
|
||||||
this._register = call(async () => {
|
this._register = call(async () => {
|
||||||
|
|
@ -99,7 +106,7 @@ export class MailNotifications {
|
||||||
throw new Error("Email notification server not configured")
|
throw new Error("Email notification server not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = {email, frequency}
|
const body = {email, frequency, hour, minute, dayOfWeek, timezone}
|
||||||
const bodyJson = JSON.stringify(body)
|
const bodyJson = JSON.stringify(body)
|
||||||
|
|
||||||
let authEvent
|
let authEvent
|
||||||
|
|
@ -224,7 +231,8 @@ export class MailNotifications {
|
||||||
|
|
||||||
// Register if we don't have a subscription yet
|
// Register if we don't have a subscription yet
|
||||||
if (!emailNotificationState.get().callback && email && frequency) {
|
if (!emailNotificationState.get().callback && email && frequency) {
|
||||||
await this.register(email, frequency)
|
const {emailHour, emailMinute, emailDayOfWeek, emailTimezone} = userSettingsValues.get()
|
||||||
|
await this.register(email, frequency, emailHour, emailMinute, emailDayOfWeek, emailTimezone)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync kind 30390 subscriptions for the email digest. Filters are derived
|
// Sync kind 30390 subscriptions for the email digest. Filters are derived
|
||||||
|
|
@ -254,46 +262,70 @@ export class MailNotifications {
|
||||||
sync() {
|
sync() {
|
||||||
let reconciledPubkey: string | undefined
|
let reconciledPubkey: string | undefined
|
||||||
|
|
||||||
return userSettingsValues.subscribe(async ({email, emailAddress, emailFrequency}) => {
|
return userSettingsValues.subscribe(
|
||||||
const $pubkey = app.get().user?.pubkey
|
async ({
|
||||||
if (!$pubkey || reconciledPubkey === $pubkey) return
|
email,
|
||||||
|
emailAddress,
|
||||||
|
emailFrequency,
|
||||||
|
emailHour,
|
||||||
|
emailMinute,
|
||||||
|
emailDayOfWeek,
|
||||||
|
emailTimezone,
|
||||||
|
}) => {
|
||||||
|
const $pubkey = app.get().user?.pubkey
|
||||||
|
if (!$pubkey || reconciledPubkey === $pubkey) return
|
||||||
|
|
||||||
// Wait for the real stored settings. userSettingsValues is projected from
|
// Wait for the real stored settings. userSettingsValues is projected from
|
||||||
// AppData and reads as the default (email: false, emailAddress: "") until
|
// AppData and reads as the default (email: false, emailAddress: "") until
|
||||||
// that loads — acting on that would race the disk with a no-op. Only a
|
// that loads — acting on that would race the disk with a no-op. Only a
|
||||||
// truthy email comes from stored settings, so keep waiting until we see
|
// truthy email comes from stored settings, so keep waiting until we see
|
||||||
// it (a user who has email off simply never reconciles, which is correct).
|
// it (a user who has email off simply never reconciles, which is correct).
|
||||||
if (!email || !emailAddress) return
|
if (!email || !emailAddress) return
|
||||||
|
|
||||||
reconciledPubkey = $pubkey
|
reconciledPubkey = $pubkey
|
||||||
|
|
||||||
// GET-first: if a subscription already exists for this pubkey with the
|
// GET-first: if a subscription already exists for this pubkey with the
|
||||||
// same email and cadence, restore its state (key/callback) instead of
|
// same email and cadence, restore its state (key/callback) instead of
|
||||||
// PUTting. A PUT here would make mailship re-send a confirmation email
|
// PUTting. A PUT here would make mailship re-send a confirmation email
|
||||||
// for an unconfirmed address on every app reload. Only register when
|
// for an unconfirmed address on every app reload. Only register when
|
||||||
// there's nothing to restore or the settings changed.
|
// there's nothing to restore or the settings changed.
|
||||||
const existing = await this.lookup()
|
const existing = await this.lookup()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
await this.register(emailAddress, emailFrequency)
|
await this.register(
|
||||||
} else if (existing.email !== emailAddress || existing.frequency !== emailFrequency) {
|
emailAddress,
|
||||||
await this.register(emailAddress, emailFrequency)
|
emailFrequency,
|
||||||
} else {
|
emailHour,
|
||||||
emailNotificationState.set({
|
emailMinute,
|
||||||
key: existing.key,
|
emailDayOfWeek,
|
||||||
callback: existing.callback,
|
emailTimezone,
|
||||||
email: existing.email,
|
)
|
||||||
frequency: existing.frequency,
|
} else if (existing.email !== emailAddress || existing.frequency !== emailFrequency) {
|
||||||
error: undefined,
|
await this.register(
|
||||||
})
|
emailAddress,
|
||||||
|
emailFrequency,
|
||||||
|
emailHour,
|
||||||
|
emailMinute,
|
||||||
|
emailDayOfWeek,
|
||||||
|
emailTimezone,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
emailNotificationState.set({
|
||||||
|
key: existing.key,
|
||||||
|
callback: existing.callback,
|
||||||
|
email: existing.email,
|
||||||
|
frequency: existing.frequency,
|
||||||
|
error: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.enable()
|
await this.enable()
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
_getSubscriptionIdentifier = async (relay: string, key: string) => {
|
_getSubscriptionIdentifier = async (relay: string, key: string) => {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,10 @@ export type SettingsValues = {
|
||||||
emailAddress: string
|
emailAddress: string
|
||||||
emailFrequency: string
|
emailFrequency: string
|
||||||
emailSalt: string
|
emailSalt: string
|
||||||
|
emailHour: number
|
||||||
|
emailMinute: number
|
||||||
|
emailDayOfWeek: number | undefined
|
||||||
|
emailTimezone: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultSettings: SettingsValues = {
|
export const defaultSettings: SettingsValues = {
|
||||||
|
|
@ -53,6 +57,10 @@ export const defaultSettings: SettingsValues = {
|
||||||
emailAddress: "",
|
emailAddress: "",
|
||||||
emailFrequency: "daily",
|
emailFrequency: "daily",
|
||||||
emailSalt: "",
|
emailSalt: "",
|
||||||
|
emailHour: 17,
|
||||||
|
emailMinute: 0,
|
||||||
|
emailDayOfWeek: undefined,
|
||||||
|
emailTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Settings extends DerivedPlugin<AppDataReader> {
|
export class Settings extends DerivedPlugin<AppDataReader> {
|
||||||
|
|
@ -107,6 +115,7 @@ export const notificationSettings = withGetter(
|
||||||
spaces: true,
|
spaces: true,
|
||||||
mentions: true,
|
mentions: true,
|
||||||
messages: true,
|
messages: true,
|
||||||
|
email: false,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -183,3 +192,9 @@ export const toggleRoomNotifications = (url: string, h: string) => {
|
||||||
|
|
||||||
return publishSettings({alerts: [...alerts, {url, notify: true, exceptions: [h]}]})
|
return publishSettings({alerts: [...alerts, {url, notify: true, exceptions: [h]}]})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const publishEmailSettings = (
|
||||||
|
params: Partial<
|
||||||
|
Pick<SettingsValues, "emailHour" | "emailMinute" | "emailDayOfWeek" | "emailTimezone">
|
||||||
|
>,
|
||||||
|
) => publishSettings(params)
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,10 @@
|
||||||
email: $userSettingsValues.email,
|
email: $userSettingsValues.email,
|
||||||
emailAddress: $userSettingsValues.emailAddress,
|
emailAddress: $userSettingsValues.emailAddress,
|
||||||
emailFrequency: $userSettingsValues.emailFrequency,
|
emailFrequency: $userSettingsValues.emailFrequency,
|
||||||
|
emailHour: $userSettingsValues.emailHour,
|
||||||
|
emailMinute: $userSettingsValues.emailMinute,
|
||||||
|
emailDayOfWeek: $userSettingsValues.emailDayOfWeek,
|
||||||
|
emailTimezone: $userSettingsValues.emailTimezone,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,7 +84,14 @@
|
||||||
// Handle email notification registration
|
// Handle email notification registration
|
||||||
if (settings.email && settings.emailAddress) {
|
if (settings.email && settings.emailAddress) {
|
||||||
try {
|
try {
|
||||||
await Mail.register(settings.emailAddress, settings.emailFrequency)
|
await Mail.register(
|
||||||
|
settings.emailAddress,
|
||||||
|
settings.emailFrequency,
|
||||||
|
settings.emailHour,
|
||||||
|
settings.emailMinute,
|
||||||
|
settings.emailDayOfWeek,
|
||||||
|
settings.emailTimezone,
|
||||||
|
)
|
||||||
await Mail.enable()
|
await Mail.enable()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
settings.email = false
|
settings.email = false
|
||||||
|
|
@ -101,6 +112,10 @@
|
||||||
email: settings.email,
|
email: settings.email,
|
||||||
emailAddress: settings.emailAddress,
|
emailAddress: settings.emailAddress,
|
||||||
emailFrequency: settings.emailFrequency,
|
emailFrequency: settings.emailFrequency,
|
||||||
|
emailHour: settings.emailHour,
|
||||||
|
emailMinute: settings.emailMinute,
|
||||||
|
emailDayOfWeek: settings.emailDayOfWeek,
|
||||||
|
emailTimezone: settings.emailTimezone,
|
||||||
})
|
})
|
||||||
|
|
||||||
notificationSettings.set({
|
notificationSettings.set({
|
||||||
|
|
@ -110,6 +125,7 @@
|
||||||
spaces: settings.spaces,
|
spaces: settings.spaces,
|
||||||
mentions: settings.mentions,
|
mentions: settings.mentions,
|
||||||
messages: settings.messages,
|
messages: settings.messages,
|
||||||
|
email: settings.email,
|
||||||
})
|
})
|
||||||
|
|
||||||
pushToast({message: "Your settings have been saved!"})
|
pushToast({message: "Your settings have been saved!"})
|
||||||
|
|
@ -281,12 +297,57 @@
|
||||||
<select
|
<select
|
||||||
class="select select-bordered"
|
class="select select-bordered"
|
||||||
bind:value={settings.emailFrequency}
|
bind:value={settings.emailFrequency}
|
||||||
onchange={clearError}>
|
onchange={e => {
|
||||||
|
if (e.currentTarget.value === "weekly" && !settings.emailDayOfWeek) {
|
||||||
|
settings.emailDayOfWeek = 1
|
||||||
|
}
|
||||||
|
clearError()
|
||||||
|
}}>
|
||||||
<option value="daily">Daily</option>
|
<option value="daily">Daily</option>
|
||||||
<option value="weekly">Weekly</option>
|
<option value="weekly">Weekly</option>
|
||||||
</select>
|
</select>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Time</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
class="input input-bordered w-auto"
|
||||||
|
value={String(settings.emailHour).padStart(2, "0") +
|
||||||
|
":" +
|
||||||
|
String(settings.emailMinute).padStart(2, "0")}
|
||||||
|
oninput={e => {
|
||||||
|
const [h, m] = e.currentTarget.value.split(":").map(Number)
|
||||||
|
settings.emailHour = h
|
||||||
|
settings.emailMinute = m
|
||||||
|
}} />
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
{#if settings.emailFrequency === "weekly"}
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Day of week</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<div class="flex gap-1">
|
||||||
|
{#each [{label: "M", value: 1}, {label: "T", value: 2}, {label: "W", value: 3}, {label: "T", value: 4}, {label: "F", value: 5}, {label: "S", value: 6}, {label: "S", value: 7}] as day (day.value)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={cx("button button-sm size-8 rounded-md", {
|
||||||
|
"button-primary": settings.emailDayOfWeek === day.value,
|
||||||
|
"button-ghost": settings.emailDayOfWeek !== day.value,
|
||||||
|
})}
|
||||||
|
onclick={() => {
|
||||||
|
settings.emailDayOfWeek = day.value
|
||||||
|
}}>{day.label}</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if $emailNotificationState.error}
|
{#if $emailNotificationState.error}
|
||||||
<div class="text-sm text-error p-3">
|
<div class="text-sm text-error p-3">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue