Files
webmail/lib/notification-mutes.ts
T

104 lines
4.0 KiB
TypeScript

/**
* Which accounts and folders stay silent when new mail arrives (#1169).
*
* An account is either muted or not. A folder can follow its account or carry
* its own setting, and its own setting always wins: mute one folder in a
* normal account, or mute a whole account and unmute the folders that still
* matter. Ids are the server's JMAP account and mailbox ids, which is what the
* push filter needs; shared-account mailboxes must pass their originalId.
*/
export interface NotificationMutes {
/** JMAP account ids muted as a whole. */
accounts: string[];
/** `accountId/mailboxId` -> true (muted) or false (unmuted). Missing = follow the account. */
mailboxes: Record<string, boolean>;
}
export const EMPTY_NOTIFICATION_MUTES: NotificationMutes = { accounts: [], mailboxes: {} };
function mailboxKey(accountId: string, mailboxId: string): string {
return `${accountId}/${mailboxId}`;
}
/** Repairs a value from storage or another build into a usable mutes object. */
export function normalizeNotificationMutes(value: unknown): NotificationMutes {
if (!value || typeof value !== 'object' || Array.isArray(value)) return EMPTY_NOTIFICATION_MUTES;
const raw = value as { accounts?: unknown; mailboxes?: unknown };
const accounts = Array.isArray(raw.accounts)
? raw.accounts.filter((id): id is string => typeof id === 'string' && id !== '')
: [];
const mailboxes: Record<string, boolean> = {};
if (raw.mailboxes && typeof raw.mailboxes === 'object' && !Array.isArray(raw.mailboxes)) {
for (const [key, muted] of Object.entries(raw.mailboxes as Record<string, unknown>)) {
if (typeof muted === 'boolean') mailboxes[key] = muted;
}
}
return { accounts, mailboxes };
}
export function isAccountMuted(mutes: NotificationMutes, accountId: string): boolean {
return mutes.accounts.includes(accountId);
}
/** The folder's own setting, or undefined when it follows its account. */
export function mailboxMuteOverride(
mutes: NotificationMutes,
accountId: string,
mailboxId: string,
): boolean | undefined {
return mutes.mailboxes[mailboxKey(accountId, mailboxId)];
}
export function isMailboxMuted(mutes: NotificationMutes, accountId: string, mailboxId: string): boolean {
return mailboxMuteOverride(mutes, accountId, mailboxId) ?? isAccountMuted(mutes, accountId);
}
/** Mailbox ids of one account with their own setting, split by that setting. */
export function mailboxOverridesForAccount(
mutes: NotificationMutes,
accountId: string,
): { muted: string[]; unmuted: string[] } {
const prefix = mailboxKey(accountId, '');
const muted: string[] = [];
const unmuted: string[] = [];
for (const [key, value] of Object.entries(mutes.mailboxes)) {
if (!key.startsWith(prefix)) continue;
(value ? muted : unmuted).push(key.slice(prefix.length));
}
return { muted, unmuted };
}
export function setAccountMuted(mutes: NotificationMutes, accountId: string, muted: boolean): NotificationMutes {
const accounts = mutes.accounts.filter((id) => id !== accountId);
if (muted) accounts.push(accountId);
return { ...mutes, accounts };
}
/**
* Mutes or unmutes one folder. Setting it back to what the account says drops
* the folder's own setting, so a folder muted and unmuted again follows the
* account later instead of staying pinned to "unmuted".
*/
export function setMailboxMuted(
mutes: NotificationMutes,
accountId: string,
mailboxId: string,
muted: boolean,
): NotificationMutes {
const mailboxes = { ...mutes.mailboxes };
const key = mailboxKey(accountId, mailboxId);
if (muted === isAccountMuted(mutes, accountId)) delete mailboxes[key];
else mailboxes[key] = muted;
return { ...mutes, mailboxes };
}
/** Drops every folder setting of one account so all its folders follow the account again. */
export function resetMailboxMutes(mutes: NotificationMutes, accountId: string): NotificationMutes {
const prefix = mailboxKey(accountId, '');
const mailboxes = Object.fromEntries(
Object.entries(mutes.mailboxes).filter(([key]) => !key.startsWith(prefix)),
);
return { ...mutes, mailboxes };
}