Open a locked account whole: calendar, contacts and files too #26
No files matched your search
+6
-2
@@ -258,8 +258,8 @@ 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
|
||||
* inbuxa AL-7: a delegated account, the whole of it, 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);
|
||||
@@ -271,6 +271,10 @@ function AuthedApp() {
|
||||
const mail = useMail.getState();
|
||||
void mail.loadMailboxes();
|
||||
void mail.loadIdentities();
|
||||
// The whole account follows: calendar, contacts and files too
|
||||
void useCalendar.getState().init();
|
||||
void useContacts.getState().init();
|
||||
void useFiles.getState().init();
|
||||
if (!viewing) return;
|
||||
const poll = window.setInterval(() => {
|
||||
if (document.visibilityState === "visible") {
|
||||
|
||||
@@ -18,6 +18,9 @@ const delegated = (access: string, sendAs = false) => ({
|
||||
isReadOnly: access === "read",
|
||||
accountCapabilities: {
|
||||
[CAP.mail]: {},
|
||||
[CAP.calendars]: {},
|
||||
[CAP.contacts]: {},
|
||||
[CAP.filenode]: {},
|
||||
"urn:inbuxa:jmap": { delegation: { locked: true, access, sendAs, until: null } },
|
||||
},
|
||||
});
|
||||
@@ -26,11 +29,15 @@ const sessionWith = (locked: Record<string, unknown> | null) =>
|
||||
({
|
||||
capabilities: { [CAP.core]: { maxCallsInRequest: 16, maxObjectsInGet: 500 }, [CAP.mail]: {} },
|
||||
accounts: {
|
||||
own: { name: "[email protected]", isPersonal: true, accountCapabilities: { [CAP.mail]: {} } },
|
||||
own: {
|
||||
name: "[email protected]",
|
||||
isPersonal: true,
|
||||
accountCapabilities: { [CAP.mail]: {}, [CAP.calendars]: {}, [CAP.contacts]: {}, [CAP.filenode]: {} },
|
||||
},
|
||||
shared: { name: "[email protected]", isPersonal: false, accountCapabilities: { [CAP.mail]: {} } },
|
||||
...(locked ? { locked } : {}),
|
||||
},
|
||||
primaryAccounts: { [CAP.mail]: "own" },
|
||||
primaryAccounts: { [CAP.mail]: "own", [CAP.calendars]: "own", [CAP.contacts]: "own", [CAP.filenode]: "own" },
|
||||
state: "s",
|
||||
}) as unknown as JmapSession;
|
||||
|
||||
@@ -94,6 +101,19 @@ describe("viewing a locked account", () => {
|
||||
expect(useMail.getState().accountId).toBe("own");
|
||||
});
|
||||
|
||||
it("brings the whole account: calendar, contacts and files, never the reader's settings", () => {
|
||||
const s = () => useSession.getState();
|
||||
expect(s().viewAccountFor(CAP.calendars)).toBe("own");
|
||||
s().view("locked");
|
||||
for (const cap of [CAP.calendars, CAP.contacts, CAP.filenode]) {
|
||||
expect(s().viewAccountFor(cap)).toBe("locked");
|
||||
}
|
||||
// Settings live in the reader's own Files, whatever is in view
|
||||
expect(s().ownAccountFor(CAP.filenode)).toBe("own");
|
||||
s().view(null);
|
||||
expect(s().viewAccountFor(CAP.contacts)).toBe("own");
|
||||
});
|
||||
|
||||
it("refuses an account that isn't delegated", () => {
|
||||
useSession.getState().view("shared");
|
||||
expect(useSession.getState().viewing).toBeNull();
|
||||
|
||||
@@ -404,7 +404,8 @@ export const useCalendar = create<CalendarState>((set, get) => ({
|
||||
|
||||
async init() {
|
||||
// The reader's own: a shared calendar is shown beside theirs, not instead.
|
||||
const accountId = useSession.getState().ownAccountFor(CAP.calendars);
|
||||
// inbuxa AL-7: or the locked account in view, the whole of it
|
||||
const accountId = useSession.getState().viewAccountFor(CAP.calendars);
|
||||
const available = Boolean(accountId && client.hasCapability(CAP.calendars));
|
||||
if (accountId !== get().accountId) set({ accountId, calendars: {}, events: {}, ranges: {} });
|
||||
set({ available });
|
||||
@@ -431,7 +432,7 @@ export const useCalendar = create<CalendarState>((set, get) => ({
|
||||
*/
|
||||
async loadSharedCalendars() {
|
||||
const session = useSession.getState();
|
||||
const own = session.ownAccountFor(CAP.calendars);
|
||||
const own = session.viewAccountFor(CAP.calendars);
|
||||
const accounts = Object.entries(session.session?.accounts ?? {}).filter(([id, a]) => a.isPersonal === false && id !== own);
|
||||
// Every account at once, in one request, and listed in the session's order.
|
||||
const answers = await Promise.all(
|
||||
|
||||
@@ -270,7 +270,8 @@ export const useContacts = create<ContactsState>((set, get) => ({
|
||||
// The reader's own, not whichever account is selected: a shared address
|
||||
// book is shown beside theirs rather than instead of it, so nothing here
|
||||
// should move when the switcher does.
|
||||
const accountId = useSession.getState().ownAccountFor(CAP.contacts);
|
||||
// inbuxa AL-7: or the locked account in view, the whole of it
|
||||
const accountId = useSession.getState().viewAccountFor(CAP.contacts);
|
||||
const available = Boolean(accountId && client.hasCapability(CAP.contacts));
|
||||
if (accountId !== get().accountId) set({ accountId, books: {}, cards: {}, cardState: null, loaded: false, selection: { accountId: null, bookId: "all" } });
|
||||
set({ available });
|
||||
@@ -300,7 +301,7 @@ export const useContacts = create<ContactsState>((set, get) => ({
|
||||
*/
|
||||
async loadShared() {
|
||||
const session = useSession.getState();
|
||||
const own = session.ownAccountFor(CAP.contacts);
|
||||
const own = session.viewAccountFor(CAP.contacts);
|
||||
const s = session.session;
|
||||
const accounts = Object.entries(s?.accounts ?? {}).filter(([id, a]) => a.isPersonal === false && id !== own);
|
||||
if (!accounts.length) {
|
||||
|
||||
@@ -139,13 +139,16 @@ export const useFiles = create<FilesState>((set, get) => ({
|
||||
|
||||
async init() {
|
||||
const session = useSession.getState();
|
||||
const ownAccountId = session.ownAccountFor(CAP.filenode);
|
||||
// inbuxa AL-7: home is the locked account in view, the whole of it
|
||||
const ownAccountId = session.viewAccountFor(CAP.filenode);
|
||||
const available = Boolean(ownAccountId && client.hasCapability(CAP.filenode));
|
||||
// Stay where the reader is if the session still offers that account;
|
||||
// whether it still holds files is `discoverShared`'s to say.
|
||||
// whether it still holds files is `discoverShared`'s to say. A new home
|
||||
// (a locked account opened or left) starts there.
|
||||
const browsing = get().accountId;
|
||||
const offered = Object.entries(session.session?.accounts ?? {}).some(([id, a]) => id === browsing && a.isPersonal === false);
|
||||
if (!(browsing && (browsing === ownAccountId || offered))) set(emptyForAccount(ownAccountId));
|
||||
const moved = ownAccountId !== get().ownAccountId;
|
||||
if (moved || !(browsing && (browsing === ownAccountId || offered))) set(emptyForAccount(ownAccountId));
|
||||
set({ available, ownAccountId });
|
||||
},
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@ interface SessionState {
|
||||
/** Selected mail account (defaults to primary). */
|
||||
accountId: Id | null;
|
||||
/**
|
||||
* A locked account handed to the reader that the mail view shows instead
|
||||
* of their own (inbuxa AL-7). Only mail follows it: settings, filters,
|
||||
* push and everything else stay the reader's own.
|
||||
* A locked account handed to the reader that the app shows instead of
|
||||
* their own (inbuxa AL-7): its mail, calendar, contacts and files. The
|
||||
* reader's settings, filters, signatures and push stay their own.
|
||||
*/
|
||||
viewing: Id | null;
|
||||
/** The name of an account whose delegation ended while it was in view. */
|
||||
@@ -42,6 +42,13 @@ interface SessionState {
|
||||
accountFor(cap: string): Id | null;
|
||||
/** The user's own account for a capability, whatever they are looking at. */
|
||||
ownAccountFor(cap: string): Id | null;
|
||||
/**
|
||||
* inbuxa AL-7: the account calendar, contacts and files show: the locked
|
||||
* account in view, if it offers `cap`, else the reader's own. Never for
|
||||
* anything the reader keeps (settings, signatures, push): those stay
|
||||
* `ownAccountFor`.
|
||||
*/
|
||||
viewAccountFor(cap: string): Id | null;
|
||||
}
|
||||
|
||||
let refreshing: Promise<void> | null = null;
|
||||
@@ -161,6 +168,13 @@ export const useSession = create<SessionState>((set, get) => ({
|
||||
ownAccountFor(cap) {
|
||||
return ownAccountForCapability(get().session, cap);
|
||||
},
|
||||
|
||||
viewAccountFor(cap) {
|
||||
const { session, viewing } = get();
|
||||
const viewed = viewing ? session?.accounts[viewing] : undefined;
|
||||
if (viewing && viewed && cap in (viewed.accountCapabilities ?? {})) return viewing;
|
||||
return ownAccountForCapability(session, cap);
|
||||
},
|
||||
}));
|
||||
|
||||
function applySession(s: JmapSession, set: (p: Partial<SessionState>) => void) {
|
||||
|
||||
Reference in new issue
Block a user