fix: open a widget's event in the account it belongs to #52

Event links from the widgets named only the event (and the JMAP account
of a shared calendar), never the signed-in account it was shown for.
With another account active by then, the Calendar tab opened without
the event, or with an id both accounts use, on the other account's
event. The links now carry `appAccount`, and the app switches to that
account first, as it already did for message links.
This commit is contained in:
Linus Rath
2026-10-02 11:46:53 +02:00
parent a694751944
commit 2cec096040
3 changed files with 29 additions and 7 deletions
+15
View File
@@ -153,6 +153,21 @@ describe('handleDeepLink', () => {
expect(usePendingCalendarOpen.getState().consume()).toBeNull();
});
it('switches to the signed-in account a widget event link names first', async () => {
const navigation = nav();
const url = links.event({ serverId: 'j', accountId: 'usera@example.org@mail', jmapAccountId: 'd' });
const link = parseDeepLink(url);
expect(link).toEqual({ kind: 'calendar', eventId: 'j', jmapAccountId: 'd', accountId: 'usera@example.org@mail' });
const switchAccount = vi.fn(async () => true);
await handleDeepLink(link!, { navigation: navigation as never, resolveThreadId: async () => null, switchAccount });
expect(switchAccount).toHaveBeenCalledWith('usera@example.org@mail');
expect(usePendingCalendarOpen.getState().consume()).toMatchObject({ serverId: 'j', accountId: 'd' });
// An account that is no longer signed in opens nothing.
expect(await handleDeepLink(link!, {
navigation: navigation as never, resolveThreadId: async () => null, switchAccount: async () => false,
})).toBe(false);
});
it('parks the settings tab and opens the Settings tab', async () => {
const navigation = nav();
await handleDeepLink(
+10 -5
View File
@@ -36,8 +36,9 @@ export type DeepLink =
| { kind: 'scheduled' }
| { kind: 'search'; query: string }
// `jmapAccountId`: the JMAP account owning the event (`?account=`), for
// one on a calendar shared with the user.
| { kind: 'calendar'; eventId?: string; date?: string; jmapAccountId?: string }
// one on a calendar shared with the user; `accountId`: the signed-in
// account it belongs to (`?appAccount=`, from widgets).
| { kind: 'calendar'; eventId?: string; date?: string; jmapAccountId?: string; accountId?: string }
| { kind: 'contact'; contactId: string }
| { kind: 'contacts' }
| { kind: 'files' }
@@ -139,9 +140,13 @@ export function parseDeepLink(url: string): DeepLink | null {
case 'calendar': {
if (kind === 'event' && value) {
const jmapAccountId = search.get('account') ?? undefined;
return jmapAccountId
? { kind: 'calendar', eventId: decodeSegment(value), jmapAccountId }
: { kind: 'calendar', eventId: decodeSegment(value) };
const appAccountId = search.get('appAccount') ?? undefined;
return {
kind: 'calendar',
eventId: decodeSegment(value),
...(jmapAccountId ? { jmapAccountId } : {}),
...(appAccountId ? { accountId: appAccountId } : {}),
};
}
const date = [kind, value].find((s) => s && /^\d{4}-\d{2}-\d{2}$/.test(s));
return { kind: 'calendar', date };
+4 -2
View File
@@ -86,8 +86,10 @@ export const links = {
compose: (params: { to?: string; subject?: string; body?: string } = {}) =>
`${base}compose${query({ to: params.to, subject: params.subject, body: params.body })}`,
calendar: () => `${base}calendar`,
event: (e: Pick<EventItem, 'serverId' | 'jmapAccountId'>) =>
`${base}calendar/event/${enc(e.serverId)}${query({ account: e.jmapAccountId })}`,
// `account` is the JMAP account of a shared calendar (as in webmail
// permalinks); `appAccount` the signed-in account the event was shown for.
event: (e: Pick<EventItem, 'serverId' | 'jmapAccountId' | 'accountId'>) =>
`${base}calendar/event/${enc(e.serverId)}${query({ account: e.jmapAccountId, appAccount: e.accountId })}`,
contacts: () => `${base}contacts`,
files: () => `${base}files`,
settings: (tab?: string) => `${base}settings${tab ? `/${enc(tab)}` : ''}`,