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)}` : ''}`,