Open a locked account whole: calendar, contacts and files too
Switching to a locked account handed to the reader moved only the mail. Now calendar, contacts and files follow it as well, through a new viewAccountFor that the three stores use for what they show. Settings, signatures and push keep ownAccountFor, so nothing of the reader's is ever written into the locked account (inbuxa AL-7).
This commit is contained in:
1 parent
22d891b1ed
commit
173680cc41
6 files changed
+57
-14
No files matched your search
+6
-2
@@ -258,8 +258,8 @@ function AuthedApp() {
|
|||||||
}, [accountId]);
|
}, [accountId]);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* inbuxa AL-7: a delegated account's mail, when it comes into view, and the
|
* inbuxa AL-7: a delegated account, the whole of it, when it comes into
|
||||||
* reader's own when it goes back. Push carries nothing for an account only
|
* 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.
|
* shared with the reader, so while one is open it is polled.
|
||||||
*/
|
*/
|
||||||
const viewedOnce = useRef(false);
|
const viewedOnce = useRef(false);
|
||||||
@@ -271,6 +271,10 @@ function AuthedApp() {
|
|||||||
const mail = useMail.getState();
|
const mail = useMail.getState();
|
||||||
void mail.loadMailboxes();
|
void mail.loadMailboxes();
|
||||||
void mail.loadIdentities();
|
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;
|
if (!viewing) return;
|
||||||
const poll = window.setInterval(() => {
|
const poll = window.setInterval(() => {
|
||||||
if (document.visibilityState === "visible") {
|
if (document.visibilityState === "visible") {
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ const delegated = (access: string, sendAs = false) => ({
|
|||||||
isReadOnly: access === "read",
|
isReadOnly: access === "read",
|
||||||
accountCapabilities: {
|
accountCapabilities: {
|
||||||
[CAP.mail]: {},
|
[CAP.mail]: {},
|
||||||
|
[CAP.calendars]: {},
|
||||||
|
[CAP.contacts]: {},
|
||||||
|
[CAP.filenode]: {},
|
||||||
"urn:inbuxa:jmap": { delegation: { locked: true, access, sendAs, until: null } },
|
"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]: {} },
|
capabilities: { [CAP.core]: { maxCallsInRequest: 16, maxObjectsInGet: 500 }, [CAP.mail]: {} },
|
||||||
accounts: {
|
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]: {} } },
|
shared: { name: "[email protected]", isPersonal: false, accountCapabilities: { [CAP.mail]: {} } },
|
||||||
...(locked ? { locked } : {}),
|
...(locked ? { locked } : {}),
|
||||||
},
|
},
|
||||||
primaryAccounts: { [CAP.mail]: "own" },
|
primaryAccounts: { [CAP.mail]: "own", [CAP.calendars]: "own", [CAP.contacts]: "own", [CAP.filenode]: "own" },
|
||||||
state: "s",
|
state: "s",
|
||||||
}) as unknown as JmapSession;
|
}) as unknown as JmapSession;
|
||||||
|
|
||||||
@@ -94,6 +101,19 @@ describe("viewing a locked account", () => {
|
|||||||
expect(useMail.getState().accountId).toBe("own");
|
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", () => {
|
it("refuses an account that isn't delegated", () => {
|
||||||
useSession.getState().view("shared");
|
useSession.getState().view("shared");
|
||||||
expect(useSession.getState().viewing).toBeNull();
|
expect(useSession.getState().viewing).toBeNull();
|
||||||
|
|||||||
@@ -404,7 +404,8 @@ export const useCalendar = create<CalendarState>((set, get) => ({
|
|||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
// The reader's own: a shared calendar is shown beside theirs, not instead.
|
// 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));
|
const available = Boolean(accountId && client.hasCapability(CAP.calendars));
|
||||||
if (accountId !== get().accountId) set({ accountId, calendars: {}, events: {}, ranges: {} });
|
if (accountId !== get().accountId) set({ accountId, calendars: {}, events: {}, ranges: {} });
|
||||||
set({ available });
|
set({ available });
|
||||||
@@ -431,7 +432,7 @@ export const useCalendar = create<CalendarState>((set, get) => ({
|
|||||||
*/
|
*/
|
||||||
async loadSharedCalendars() {
|
async loadSharedCalendars() {
|
||||||
const session = useSession.getState();
|
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);
|
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.
|
// Every account at once, in one request, and listed in the session's order.
|
||||||
const answers = await Promise.all(
|
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
|
// 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
|
// book is shown beside theirs rather than instead of it, so nothing here
|
||||||
// should move when the switcher does.
|
// 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));
|
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" } });
|
if (accountId !== get().accountId) set({ accountId, books: {}, cards: {}, cardState: null, loaded: false, selection: { accountId: null, bookId: "all" } });
|
||||||
set({ available });
|
set({ available });
|
||||||
@@ -300,7 +301,7 @@ export const useContacts = create<ContactsState>((set, get) => ({
|
|||||||
*/
|
*/
|
||||||
async loadShared() {
|
async loadShared() {
|
||||||
const session = useSession.getState();
|
const session = useSession.getState();
|
||||||
const own = session.ownAccountFor(CAP.contacts);
|
const own = session.viewAccountFor(CAP.contacts);
|
||||||
const s = session.session;
|
const s = session.session;
|
||||||
const accounts = Object.entries(s?.accounts ?? {}).filter(([id, a]) => a.isPersonal === false && id !== own);
|
const accounts = Object.entries(s?.accounts ?? {}).filter(([id, a]) => a.isPersonal === false && id !== own);
|
||||||
if (!accounts.length) {
|
if (!accounts.length) {
|
||||||
|
|||||||
@@ -139,13 +139,16 @@ export const useFiles = create<FilesState>((set, get) => ({
|
|||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
const session = useSession.getState();
|
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));
|
const available = Boolean(ownAccountId && client.hasCapability(CAP.filenode));
|
||||||
// Stay where the reader is if the session still offers that account;
|
// 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 browsing = get().accountId;
|
||||||
const offered = Object.entries(session.session?.accounts ?? {}).some(([id, a]) => id === browsing && a.isPersonal === false);
|
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 });
|
set({ available, ownAccountId });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ interface SessionState {
|
|||||||
/** Selected mail account (defaults to primary). */
|
/** Selected mail account (defaults to primary). */
|
||||||
accountId: Id | null;
|
accountId: Id | null;
|
||||||
/**
|
/**
|
||||||
* A locked account handed to the reader that the mail view shows instead
|
* A locked account handed to the reader that the app shows instead of
|
||||||
* of their own (inbuxa AL-7). Only mail follows it: settings, filters,
|
* their own (inbuxa AL-7): its mail, calendar, contacts and files. The
|
||||||
* push and everything else stay the reader's own.
|
* reader's settings, filters, signatures and push stay their own.
|
||||||
*/
|
*/
|
||||||
viewing: Id | null;
|
viewing: Id | null;
|
||||||
/** The name of an account whose delegation ended while it was in view. */
|
/** The name of an account whose delegation ended while it was in view. */
|
||||||
@@ -42,6 +42,13 @@ interface SessionState {
|
|||||||
accountFor(cap: string): Id | null;
|
accountFor(cap: string): Id | null;
|
||||||
/** The user's own account for a capability, whatever they are looking at. */
|
/** The user's own account for a capability, whatever they are looking at. */
|
||||||
ownAccountFor(cap: string): Id | null;
|
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;
|
let refreshing: Promise<void> | null = null;
|
||||||
@@ -161,6 +168,13 @@ export const useSession = create<SessionState>((set, get) => ({
|
|||||||
ownAccountFor(cap) {
|
ownAccountFor(cap) {
|
||||||
return ownAccountForCapability(get().session, 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) {
|
function applySession(s: JmapSession, set: (p: Partial<SessionState>) => void) {
|
||||||
|
|||||||
Reference in new issue
Block a user