fix: keep the account and other query parameters of a mail link Lite replays after a 404 #1352

This commit is contained in:
Linus Rath
2026-10-26 20:01:10 +01:00
parent 613451dfef
commit af536ac90a
3 changed files with 48 additions and 3 deletions
+2 -2
View File
@@ -157,7 +157,7 @@ import {
import { consumePendingDeepLink, subscribePendingDeepLink } from "@/lib/deep-link-handoff";
import { useProInterfaceActive } from "@/components/pro/pro-interface-redirect";
import type { QuoteHeader } from "@/lib/plugin-types";
import { useLiteLinkSegments } from "@/hooks/use-lite-link-segments";
import { initialLinkSearch, useLiteLinkSegments } from "@/hooks/use-lite-link-segments";
export interface MailAppProps {
/**
@@ -1893,7 +1893,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
// actually opened with on first client render.
const initialSearchRef = useRef<string | null>(null);
if (initialSearchRef.current === null && typeof window !== "undefined") {
initialSearchRef.current = window.location.search;
initialSearchRef.current = initialLinkSearch('mail');
}
useEffect(() => {
+33 -1
View File
@@ -14,9 +14,41 @@ vi.mock('@/lib/lite', async (importOriginal) => {
};
});
import { useLiteLinkSegments } from '@/hooks/use-lite-link-segments';
import { initialLinkSearch, useLiteLinkSegments } from '@/hooks/use-lite-link-segments';
import { LITE_PENDING_PATH_KEY } from '@/lib/lite';
// Mail reads the query it was opened with on its first render, while the
// address bar still shows the surface root the 404 shim jumped to (#1352).
describe('initialLinkSearch', () => {
beforeEach(() => sessionStorage.clear());
it('takes the query of a parked deep link in the first render', () => {
liteState.IS_LITE = true;
window.history.replaceState({}, '', '/en/mail/');
sessionStorage.setItem(LITE_PENDING_PATH_KEY, '/en/mail/message/m1/?account=bob%40example.org#x');
const seen: string[] = [];
renderHook(() => {
useLiteLinkSegments('mail', []);
seen.push(initialLinkSearch('mail'));
});
expect(seen[0]).toBe('?account=bob%40example.org');
// The restored URL carries it from then on.
expect(window.location.search).toBe('?account=bob%40example.org');
});
it('reads the address bar otherwise', () => {
liteState.IS_LITE = true;
window.history.replaceState({}, '', '/en/mail/message/m1?account=a');
sessionStorage.setItem(LITE_PENDING_PATH_KEY, '/en/calendar/week/2026-09-17?view=1');
expect(initialLinkSearch('mail')).toBe('?account=a');
liteState.IS_LITE = false;
sessionStorage.setItem(LITE_PENDING_PATH_KEY, '/en/mail/message/m2?account=b');
expect(initialLinkSearch('mail')).toBe('?account=a');
});
});
describe('useLiteLinkSegments', () => {
beforeEach(() => {
sessionStorage.clear();
+13
View File
@@ -53,3 +53,16 @@ export function useLiteLinkSegments(surface: AppSurface, linkSegments?: string[]
return segments;
}
/**
* The query a surface was opened with, for a caller that reads it during its
* first render: until the effect above restores a parked deep link, the
* address bar shows the surface root without the link's query (#1352).
*/
export function initialLinkSearch(surface: AppSurface): string {
const pending = IS_LITE ? peekPendingLitePath(surface, getPathPrefix()) : null;
if (pending === null) return window.location.search;
const query = pending.split('#')[0];
const start = query.indexOf('?');
return start === -1 ? '' : query.slice(start);
}