feat: improve new email push notification logic for inbox
This commit is contained in:
+20
-5
@@ -19,7 +19,19 @@ export const DEFAULT_RELAY_BASE_URL =
|
|||||||
const SUBSCRIPTION_EXPIRES_DAYS = 90;
|
const SUBSCRIPTION_EXPIRES_DAYS = 90;
|
||||||
const SUBSCRIPTION_REFRESH_THRESHOLD_DAYS = 7;
|
const SUBSCRIPTION_REFRESH_THRESHOLD_DAYS = 7;
|
||||||
|
|
||||||
const PUSH_TYPES = ['Email', 'EmailDelivery', 'Mailbox'] as const;
|
// Only `EmailDelivery` state-changes when new mail is actually delivered.
|
||||||
|
// `Email` fires for any mutation (sending, drafting, moving, marking read,
|
||||||
|
// deleting) and `Mailbox` fires for mailbox edits — both produced spurious
|
||||||
|
// system notifications, so we keep them out of the push subscription.
|
||||||
|
// In-app sync uses a separate StateChange channel and is unaffected.
|
||||||
|
const PUSH_TYPES = ['EmailDelivery'] as const;
|
||||||
|
|
||||||
|
function sameTypes(a: readonly string[] | null | undefined, b: readonly string[]): boolean {
|
||||||
|
if (!a || a.length !== b.length) return false;
|
||||||
|
const sortedA = [...a].sort();
|
||||||
|
const sortedB = [...b].sort();
|
||||||
|
return sortedA.every((t, i) => t === sortedB[i]);
|
||||||
|
}
|
||||||
|
|
||||||
export interface EnableWebPushParams {
|
export interface EnableWebPushParams {
|
||||||
client: IJMAPClient;
|
client: IJMAPClient;
|
||||||
@@ -190,17 +202,20 @@ async function pollVerificationCode(
|
|||||||
|
|
||||||
async function refreshSubscriptionExpires(
|
async function refreshSubscriptionExpires(
|
||||||
client: IJMAPClient,
|
client: IJMAPClient,
|
||||||
sub: { id: string; expires: string | null },
|
sub: { id: string; expires: string | null; types: string[] | null },
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (sub.expires) {
|
const typesNeedUpdate = !sameTypes(sub.types, PUSH_TYPES);
|
||||||
|
if (!typesNeedUpdate && sub.expires) {
|
||||||
const remainingMs = new Date(sub.expires).getTime() - Date.now();
|
const remainingMs = new Date(sub.expires).getTime() - Date.now();
|
||||||
const thresholdMs = SUBSCRIPTION_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000;
|
const thresholdMs = SUBSCRIPTION_REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000;
|
||||||
if (Number.isFinite(remainingMs) && remainingMs > thresholdMs) return true;
|
if (Number.isFinite(remainingMs) && remainingMs > thresholdMs) return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return await client.updatePushSubscription(sub.id, {
|
const patch: { expires?: string; types?: string[] } = {
|
||||||
expires: expiresFromNow(SUBSCRIPTION_EXPIRES_DAYS),
|
expires: expiresFromNow(SUBSCRIPTION_EXPIRES_DAYS),
|
||||||
});
|
};
|
||||||
|
if (typesNeedUpdate) patch.types = [...PUSH_TYPES];
|
||||||
|
return await client.updatePushSubscription(sub.id, patch);
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,15 @@ async function handlePush(event) {
|
|||||||
? preview.unreadTotal
|
? preview.unreadTotal
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
|
// The push subscription is scoped to EmailDelivery, but we still see the
|
||||||
|
// occasional wake-up that does not correspond to a new unread message
|
||||||
|
// (legacy subscription with broader types, races with marking-as-read,
|
||||||
|
// verification pings). Without a concrete unread email to surface, stay
|
||||||
|
// silent rather than firing a generic "New mail" toast for a non-event.
|
||||||
|
if (!email && unreadTotal === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let title;
|
let title;
|
||||||
let body;
|
let body;
|
||||||
let tag = "bulwark-mail";
|
let tag = "bulwark-mail";
|
||||||
|
|||||||
Reference in New Issue
Block a user