Notifications with the app closed, for every signed-in account
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 3m3s
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 3m3s
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
ci / announce (pull_request) Skipped
Second of three for background push across accounts (multi-account spec, MA-8 part 2). Turning on "Notify me even when inbuxa is closed" now registers this browser in every signed-in account, each through its own session (the route from the previous change), and renewal keeps them all current. GET /api/auth/accounts says which mail account each session is, so the subscription can name it. The remembered endpoint is kept per account, and survives the clean-up that follows switching accounts. The service worker is told who the other accounts are. A push for one of them is titled with that account's address, shown even while a tab is focused (the tab only shows the front account's mail), and its Mark read and Archive act through that account's session. Clicking it brings that account to the front and opens the message. While push is on, the tab's own polling of other accounts stops notifying, so nothing arrives twice. Signing out of one account removes this device's subscription there only; signing out of all, or turning push off, removes every one. Also fixes where every background notification opened: the worker linked to /mail/inbox/<thread>, and the route takes a mailbox id there, so a click landed on the inbox list with "That folder no longer exists". The worker now gets each account's inbox id and links to the message. Checked end to end in Chrome against a local server with two accounts: both registered and verified, a message to the account not in front showed a notification under its address, and clicking it switched accounts and opened the message. No new strings. typecheck, tests (web 1547, server 279) and build pass.
This commit is contained in:
1 parent
6b979c5ac7
commit
9fcf4812f3
13 files changed
+394
-39
No files matched your search
+80
-6
@@ -386,8 +386,11 @@ async function readFacts() {
|
||||
* rather than swallowed. A tap that silently does nothing is the failure worth
|
||||
* avoiding here: the reader has already put the phone down.
|
||||
*/
|
||||
async function jmap(methodCalls) {
|
||||
const res = await fetch(`${BASE}/api/jmap`, {
|
||||
async function jmap(methodCalls, sessionId) {
|
||||
// inbuxa MA-8: an account not in front is reached through its own session,
|
||||
// on the webmail server's narrow route for it
|
||||
const path = sessionId ? `${BASE}/api/auth/accounts/${encodeURIComponent(sessionId)}/jmap` : `${BASE}/api/jmap`;
|
||||
const res = await fetch(path, {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: { "content-type": "application/json", accept: "application/json", "x-requested-with": "ihasmail" },
|
||||
@@ -456,6 +459,15 @@ self.addEventListener("push", (event) => {
|
||||
|
||||
const emails = (data && data["@type"] === "EmailPush" && Array.isArray(data.emails)) ? data.emails : [];
|
||||
event.waitUntil((async () => {
|
||||
const facts = await readFacts();
|
||||
/*
|
||||
* inbuxa MA-8: whose mail this is. The payload names its account; one that
|
||||
* isn't the account in front is one of the others signed in here, or one
|
||||
* that has been signed out since (said plainly, with nothing to act on).
|
||||
*/
|
||||
const forAccount = data && data.accountId && facts && data.accountId !== facts.accountId ? data.accountId : null;
|
||||
const other = forAccount ? (facts.others || []).find((o) => o.accountId === forAccount) || null : null;
|
||||
if (forAccount) return showOtherAccount(emails, facts, other);
|
||||
/*
|
||||
* Someone reading the app already knows. A focused, visible window of this
|
||||
* app gets its new mail from its own event stream, so a notification on
|
||||
@@ -464,7 +476,6 @@ self.addEventListener("push", (event) => {
|
||||
*/
|
||||
const windows = await self.clients.matchAll({ type: "window" });
|
||||
if (windows.some((w) => w.focused && w.visibilityState === "visible")) return;
|
||||
const facts = await readFacts();
|
||||
const strings = facts?.strings ?? { newMail: "New mail", newMessage: "New message", noSubject: "(no subject)" };
|
||||
/*
|
||||
* Mark the app icon, without claiming a number.
|
||||
@@ -502,8 +513,7 @@ self.addEventListener("push", (event) => {
|
||||
// be drawn.
|
||||
actions: email.id ? actionsFor(facts) : [],
|
||||
data: {
|
||||
// The route names a conversation, and `m` the message in it.
|
||||
url: email.id && email.threadId ? `${BASE}/mail/inbox/${email.threadId}?m=${encodeURIComponent(email.id)}` : `${BASE}/mail`,
|
||||
url: messageUrl(facts && facts.inboxId, email),
|
||||
id: email.id || null,
|
||||
title,
|
||||
accountId: facts?.accountId ?? null,
|
||||
@@ -515,6 +525,70 @@ self.addEventListener("push", (event) => {
|
||||
})());
|
||||
});
|
||||
|
||||
/*
|
||||
* inbuxa MA-8: new mail for a signed-in account that isn't in front.
|
||||
*
|
||||
* Shown even while a tab is focused: that tab's own stream only carries the
|
||||
* account in front, so nothing else would tell. The account's address is the
|
||||
* title, so it can't be taken for the front account's mail; the tag carries
|
||||
* the account, so two accounts' notifications don't replace each other; the
|
||||
* buttons act through that account's session; and opening it brings that
|
||||
* account forward before showing the message.
|
||||
*/
|
||||
async function showOtherAccount(emails, facts, other) {
|
||||
const strings = facts.strings;
|
||||
const icon = `${BASE}/img/icon-192.png?v=${BRAND_V}`;
|
||||
const badge = `${BASE}/img/favicon-64.png?v=${BRAND_V}`;
|
||||
if ("setAppBadge" in self.navigator) await self.navigator.setAppBadge().catch(() => {});
|
||||
if (!other || !emails.length) {
|
||||
// Signed out since, or nothing to show: say only what is true
|
||||
await self.registration.showNotification(other ? other.username : strings.newMail, {
|
||||
body: other ? strings.newMail : undefined,
|
||||
icon, badge,
|
||||
tag: `ihasmail-other-${other ? other.accountId : "unknown"}`,
|
||||
data: { url: other ? openUrl(other, `${BASE}/mail`) : `${BASE}/mail` },
|
||||
});
|
||||
return;
|
||||
}
|
||||
for (const email of emails.slice(0, 5)) {
|
||||
const { title, body, preview } = textOf(email, strings);
|
||||
const at = messageUrl(other.inboxId, email);
|
||||
await self.registration.showNotification(other.username, {
|
||||
body: `${title}: ${body}${preview ? `\n${preview}` : ""}`,
|
||||
icon, badge,
|
||||
tag: `ihasmail-${other.accountId}-${email.id || body}`,
|
||||
actions: email.id ? actionsFor({ ...facts, archiveId: other.archiveId }) : [],
|
||||
data: {
|
||||
url: openUrl(other, at),
|
||||
id: email.id || null,
|
||||
title: other.username,
|
||||
accountId: other.accountId,
|
||||
archiveId: other.archiveId,
|
||||
sessionId: other.sessionId,
|
||||
failed: strings.failed ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Where opening a notification for an account not in front goes: it comes forward first. */
|
||||
/**
|
||||
* Where a notification opens. The route names a mailbox by id and then a
|
||||
* conversation, and `m` the message in it. It used to say `inbox` where the id
|
||||
* goes, which the app reads as a folder that no longer exists, so every click
|
||||
* landed on the inbox list with "That folder no longer exists" instead of the
|
||||
* message. Without an inbox id from the briefing, the inbox is the honest
|
||||
* landing.
|
||||
*/
|
||||
function messageUrl(inboxId, email) {
|
||||
if (!inboxId || !email.id || !email.threadId) return `${BASE}/mail`;
|
||||
return `${BASE}/mail/${encodeURIComponent(inboxId)}/${encodeURIComponent(email.threadId)}?m=${encodeURIComponent(email.id)}`;
|
||||
}
|
||||
|
||||
function openUrl(other, next) {
|
||||
return `${BASE}/?account=${encodeURIComponent(other.sessionId)}&next=${encodeURIComponent(next)}`;
|
||||
}
|
||||
|
||||
/*
|
||||
* Do what the button said, without opening anything.
|
||||
*
|
||||
@@ -535,7 +609,7 @@ async function runAction(action, data) {
|
||||
: { "keywords/$seen": true };
|
||||
try {
|
||||
if (action === "archive" && !archiveId) throw new Error("no archive mailbox");
|
||||
await jmap([["Email/set", { accountId, update: { [id]: patch } }, "0"]]);
|
||||
await jmap([["Email/set", { accountId, update: { [id]: patch } }, "0"]], data.sessionId || null);
|
||||
} catch {
|
||||
await self.registration.showNotification(data.title || "ihasmail", {
|
||||
body: data.failed || "Could not do that — open ihasmail and try again",
|
||||
|
||||
Reference in new issue
Block a user