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

+57 -5
View File
@@ -1,7 +1,7 @@
import { Fragment, lazy, Suspense, useEffect, useState } from "react";
import { Fragment, lazy, Suspense, useEffect, useRef, useState } from "react";
import { Route, Switch, Redirect, useLocation, Router } from "wouter";
import { useSession } from "@/store/session";
import { useMail } from "@/store/mail";
import { notifyOwnWhileAway, ownAccountAway, useMail } from "@/store/mail";
import { scheduleSupported, useScheduled } from "@/store/scheduled";
import { useContacts } from "@/store/contacts";
import { useCalendar } from "@/store/calendar";
@@ -119,6 +119,7 @@ export function App() {
function AuthedApp() {
const accountId = useSession((s) => s.accountId);
const viewing = useSession((s) => s.viewing);
const [location] = useLocation();
/*
@@ -230,6 +231,9 @@ function AuthedApp() {
timer = null;
for (const [a, types] of pending) {
if (a === useMail.getState().accountId) void useMail.getState().applyChanges(types);
// inbuxa AL-7: the reader's own mail, while a delegated account
// is in view, is still announced
if (a === ownAccountAway() && types.has("Email")) void notifyOwnWhileAway();
if (a === useContacts.getState().accountId) useContacts.getState().applyChanges(types);
if (a === useCalendar.getState().accountId) useCalendar.getState().applyChanges(types);
if (a === useFiles.getState().accountId) useFiles.getState().applyChanges(types);
@@ -253,16 +257,61 @@ function AuthedApp() {
};
}, [accountId]);
/*
* inbuxa AL-7: a delegated account's mail, when it comes into view, and the
* reader's own when it goes back. Push carries nothing for an account only
* shared with the reader, so while one is open it is polled.
*/
const viewedOnce = useRef(false);
useEffect(() => {
if (!viewedOnce.current) {
viewedOnce.current = true;
if (!viewing) return;
}
const mail = useMail.getState();
void mail.loadMailboxes();
void mail.loadIdentities();
if (!viewing) return;
const poll = window.setInterval(() => {
if (document.visibilityState === "visible") {
void useMail.getState().applyChanges(new Set(["Email", "Mailbox"]));
}
}, 60_000);
return () => window.clearInterval(poll);
}, [viewing]);
// inbuxa AL-7: a delegation given or taken away while the app is open shows
// up when the reader comes back to it, without signing in again
useEffect(() => {
let last = 0;
const onVisible = () => {
if (document.visibilityState !== "visible" || Date.now() - last < 60_000) return;
last = Date.now();
void useSession.getState().refresh();
};
document.addEventListener("visibilitychange", onVisible);
return () => document.removeEventListener("visibilitychange", onVisible);
}, []);
const delegationEnded = useSession((s) => s.delegationEnded);
useEffect(() => {
if (!delegationEnded) return;
toast.show(t("You no longer have access to {name}. Back to your own mail.", { name: delegationEnded }));
useSession.getState().clearDelegationEnded();
}, [delegationEnded]);
// Unread badge in title/favicon
const inboxUnread = useMail((s) => {
const id = s.roleId("inbox");
return id ? (s.mailboxes[id]?.unreadEmails ?? 0) : 0;
});
const appName = useSession((s) => s.session?.ihasmail?.appName) || DEFAULT_APP_NAME;
// inbuxa AL-7: a locked account in view is named, with a padlock, in the tab
const viewingName = useSession((s) => (s.viewing ? s.session?.accounts[s.viewing]?.name : undefined));
useEffect(() => {
setBaseTitle(appName);
setBaseTitle(viewingName ? `🔒 ${viewingName} · ${appName}` : appName);
setUnreadBadge(inboxUnread);
}, [inboxUnread, appName]);
}, [inboxUnread, appName, viewingName]);
/*
* Leave the service worker its briefing.
@@ -276,8 +325,11 @@ function AuthedApp() {
const archiveId = useMail((s) => s.roleId("archive"));
const languageVersion = useLanguageVersion();
useEffect(() => {
// inbuxa AL-7: the worker acts on the reader's own mail; while a
// delegated account is in view, the archive folder here is its
if (viewing) return;
void publishWorkerFacts(accountId, archiveId);
}, [accountId, archiveId, languageVersion]);
}, [accountId, archiveId, languageVersion, viewing]);
// Request notification permission lazily when enabled
const notif = useSettings((s) => s.settings.desktopNotifications);