Merge pull request 'Open a locked account whole: calendar, contacts and files too' (#26) from feature/delegate-whole-account into main
This commit was merged in pull request #26.
This commit is contained in:
commit
9e47437ef8
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