Open locked accounts handed to you, beside your own mail

When the server hands a locked account to the reader (urn:inbuxa:jmap
delegation), the account popover offers it. Only mail follows the switch;
the reader's own settings, push and notifications stay theirs. A red bar,
a red wordmark with a padlock and the tab title say which account is in
view. Read delegates can't change anything, organize delegates can't
delete, and writing needs send-as. A delegation taken away drops back to
the reader's own mail.

14 new strings in all nine catalogs, unreviewed (inbuxa AL-7, AL-8).
This commit is contained in:
jcoffey-dev committed 2026-09-27 14:32:10 -07:00
1 parent ffe1a898f5
commit 8674b70f62
22 files changed
+679 -25

No files matched your search

+61 -3
View File
@@ -104,6 +104,15 @@ export const useMail = create<MailState>((set, get) => ({
setAccount(accountId) {
if (accountId === get().accountId) return;
// inbuxa AL-7: leaving the reader's own mail for a delegated account,
// remember where theirs was, so new mail there is still announced
const own = useSession.getState().accountId;
const leaving = get().accountId;
if (leaving && leaving === own && accountId && accountId !== own) {
ownWhileAway = { accountId: own, inbox: get().roleId("inbox"), state: get().emailState };
} else if (accountId === own) {
ownWhileAway = null;
}
resetBodyOrder();
snapshots.clear();
set({
@@ -1485,10 +1494,56 @@ function removeFromList(ids: Id[], set: (fn: (s: MailState) => Partial<MailState
}
async function notifyNewMail(created: Id[], get: () => MailState) {
const s = settings();
const inbox = get().roleId("inbox");
if (!inbox) return;
const emails = await get().getEmails(created);
announceNewMail(emails, inbox);
}
/**
* The reader's own account while a delegated one is in view (inbuxa AL-7):
* its inbox and how far its mail was seen, so what arrives there meanwhile
* is still announced.
*/
let ownWhileAway: { accountId: Id; inbox: Id | null; state: string | null } | null = null;
/** New mail in the reader's own account, while a delegated one is in view. */
export async function notifyOwnWhileAway(): Promise<void> {
const away = ownWhileAway;
if (!away?.inbox || !away.state) return;
try {
const changes = await client.call<ChangesResponse>("Email/changes", {
accountId: away.accountId,
sinceState: away.state,
maxChanges: 50,
});
if (ownWhileAway !== away) return;
away.state = changes.newState;
if (!changes.created.length) return;
const got = await client.call<GetResponse<Email>>("Email/get", {
accountId: away.accountId,
ids: changes.created,
properties: LIST_PROPS,
});
announceNewMail(got.list, away.inbox);
} catch {
/* the next change tries again */
}
}
export function ownAccountAway(): Id | null {
return ownWhileAway?.accountId ?? null;
}
/** The reader's own inbox, whatever account is in view (inbuxa AL-7). */
export function ownInboxId(): Id | null {
const mail = useMail.getState();
if (mail.accountId === useSession.getState().accountId) return mail.roleId("inbox");
return ownWhileAway?.inbox ?? null;
}
function announceNewMail(emails: Email[], inbox: Id) {
const s = settings();
const fresh = emails.filter((e) => e.mailboxIds[inbox] && !e.keywords.$seen && !e.keywords.$draft);
if (!fresh.length) return;
if (s.notificationSound) playNewMailSound();
@@ -1514,9 +1569,12 @@ async function notifyNewMail(created: Id[], get: () => MailState) {
}
}
/** Keep the store bound to the selected account. */
/**
* Keep the store bound to the account in view: a delegated account while one
* is open (inbuxa AL-7), the reader's own otherwise.
*/
useSession.subscribe((s) => {
useMail.getState().setAccount(s.status === "authenticated" ? s.accountId : null);
useMail.getState().setAccount(s.status === "authenticated" ? (s.viewing ?? s.accountId) : null);
});