From 2cec096040c98a001d1ed81e4f2bf82cb4e1f43f Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:46:53 +0200 Subject: [PATCH] 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. --- src/navigation/__tests__/linking.test.ts | 15 +++++++++++++++ src/navigation/linking.ts | 15 ++++++++++----- src/widgets/clicks.ts | 6 ++++-- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/navigation/__tests__/linking.test.ts b/src/navigation/__tests__/linking.test.ts index 6ed59783..88d2e0d4 100644 --- a/src/navigation/__tests__/linking.test.ts +++ b/src/navigation/__tests__/linking.test.ts @@ -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( diff --git a/src/navigation/linking.ts b/src/navigation/linking.ts index 7c621b4e..8b786423 100644 --- a/src/navigation/linking.ts +++ b/src/navigation/linking.ts @@ -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 }; diff --git a/src/widgets/clicks.ts b/src/widgets/clicks.ts index 687866ee..d490c711 100644 --- a/src/widgets/clicks.ts +++ b/src/widgets/clicks.ts @@ -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) => - `${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) => + `${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)}` : ''}`,