From d7356d76b611f0ff5bf66b3969f24787e668c800 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Tue, 10 Nov 2026 09:03:14 +1100 Subject: [PATCH 1/2] Fix Profile not working on safari and mobile Prompt: There are multiple refreshes happening on mobile and safari that cause the profile not to be rendered, when it does render the heading is not present Co-authored-by: Claude Opus 5.5 --- src/navigation/browserEvents.ts | 12 ++++- test/unit/mainPage/index.test.ts | 8 ++- test/unit/navigation/browserEvents.test.ts | 61 ++++++++++++++++------ 3 files changed, 62 insertions(+), 19 deletions(-) diff --git a/src/navigation/browserEvents.ts b/src/navigation/browserEvents.ts index 4262dbe4..51a9e5a6 100644 --- a/src/navigation/browserEvents.ts +++ b/src/navigation/browserEvents.ts @@ -14,7 +14,8 @@ interface NavigationEvent extends Event { canIntercept: boolean userInitiated: boolean navigationType: string - destination: { url: string } + hashChange: boolean + destination: { url: string, sameDocument: boolean } intercept: (options: { handler: () => Promise, scroll: 'manual', focusReset: 'manual' }) => void } @@ -67,8 +68,17 @@ export function registerBrowserNavigation (controller: NavigationController): vo // NAVIGATION API: Enter on the current URL (typically one ending in #me) is a same-URL // "replace" navigation; intercepting it shows the URL without reloading. Without the API // there is no equivalent, see the fallback above. + // + // Enter is either a fragment navigation (`hashChange`) or a cross-document one. A replacement + // to the current URL without a hash change is a `history.replaceState` write. Safari marks the + // application's own writes as `userInitiated` during a tap and can dispatch them after + // `writeNavigationHistory` returns, so that guard alone does not stop them. Use the URL and hash + // rather than `sameDocument` here because WebKit versions differ in whether they report that + // property for a history API write. navigationApi.addEventListener('navigate', event => { if (isWritingNavigationHistory(window.history) || + (!event.hashChange && event.destination.sameDocument !== false && + event.destination.url === window.location.href) || !event.canIntercept || !event.userInitiated || event.navigationType !== 'replace' || event.destination.url !== window.location.href) return diff --git a/test/unit/mainPage/index.test.ts b/test/unit/mainPage/index.test.ts index 8dfd9ee6..3f90881c 100644 --- a/test/unit/mainPage/index.test.ts +++ b/test/unit/mainPage/index.test.ts @@ -167,7 +167,8 @@ describe('browser navigation without a reload', () => { canIntercept: true, userInitiated: true, navigationType: 'replace', - destination: { url: window.location.href }, + hashChange: true, + destination: { url: window.location.href, sameDocument: true }, intercept, ...overrides }) @@ -211,7 +212,10 @@ describe('browser navigation without a reload', () => { const GotoSubject = await setupLoaded({}, false) window.history.replaceState({ paneName: 'folder', paneUri: `${origin}/other/` }, '', window.location.href) - const intercept = enter() + const intercept = enter({ + hashChange: false, + destination: { url: window.location.href, sameDocument: false } + }) expect(intercept).toHaveBeenCalledOnce() await intercept.mock.calls[0][0].handler() diff --git a/test/unit/navigation/browserEvents.test.ts b/test/unit/navigation/browserEvents.test.ts index ee649db7..cdddb067 100644 --- a/test/unit/navigation/browserEvents.test.ts +++ b/test/unit/navigation/browserEvents.test.ts @@ -20,19 +20,33 @@ async function freshModule () { return import('../../../src/navigation/browserEvents') } +// Enter on a URL ending in #me is a fragment navigation to the same URL function enter (overrides: object = {}) { const intercept = vi.fn() listeners.navigate({ canIntercept: true, userInitiated: true, navigationType: 'replace', - destination: { url: window.location.href }, + hashChange: true, + destination: { url: window.location.href, sameDocument: true }, intercept, ...overrides }) return intercept } +// What Safari dispatches for history.replaceState during a tap +function historyWrite (intercept: ReturnType) { + listeners.navigate({ + canIntercept: true, + userInitiated: true, + navigationType: 'replace', + hashChange: false, + destination: { url: window.location.href, sameDocument: true }, + intercept + }) +} + const traverse = (navigationType = 'traverse') => listeners.currententrychange({ navigationType }) const nextTask = () => new Promise(resolve => setTimeout(resolve, 5)) @@ -64,11 +78,23 @@ describe('Enter on the current URL', () => { expect(show).toHaveBeenCalledWith({ kind: 'restore', forceHeading: true }) }) + it('also handles Enter on a URL without a fragment, which is a cross-document navigation', async () => { + const { registerBrowserNavigation } = await freshModule() + registerBrowserNavigation(controller) + + expect(enter({ hashChange: false, destination: { url: window.location.href, sameDocument: false } })).toHaveBeenCalledOnce() + }) + it.each([ ['not started by the user', { userInitiated: false }], ['not a replacement of the entry', { navigationType: 'push' }], ['one that cannot be intercepted', { canIntercept: false }], - ['to another URL', { destination: { url: 'https://elsewhere.example/' } }] + ['to another URL', { destination: { url: 'https://elsewhere.example/', sameDocument: false } }], + ['a history API write', { hashChange: false }], + ['a history API write with same-document metadata omitted', { + hashChange: false, + destination: { url: window.location.href, sameDocument: undefined } + }] ])('leaves a navigation alone when it is %s', async (_name, overrides) => { const { registerBrowserNavigation } = await freshModule() registerBrowserNavigation(controller) @@ -90,13 +116,7 @@ describe('application history writes in Safari', () => { const original = window.history.replaceState.bind(window.history) const intercept = vi.fn() const replace = vi.spyOn(window.history, 'replaceState').mockImplementation((state, title, url) => { - listeners.navigate({ - canIntercept: true, - userInitiated: true, - navigationType: 'replace', - destination: { url: window.location.href }, - intercept - }) + historyWrite(intercept) original(state, title, url) }) const subject = sym(window.location.href) @@ -119,13 +139,7 @@ describe('application history writes in Safari', () => { const original = window.history.replaceState.bind(window.history) const nestedIntercept = vi.fn() vi.spyOn(window.history, 'replaceState').mockImplementation((state, title, url) => { - listeners.navigate({ - canIntercept: true, - userInitiated: true, - navigationType: 'replace', - destination: { url: window.location.href }, - intercept: nestedIntercept - }) + historyWrite(nestedIntercept) original(state, title, url) }) @@ -136,6 +150,21 @@ describe('application history writes in Safari', () => { expect(show).toHaveBeenCalledOnce() }) + it('keeps the account-menu profile when Safari dispatches the write after it has returned', async () => { + const { registerBrowserNavigation } = await freshModule() + const { writeNavigationHistory } = await import('../../../src/navigation/state') + registerBrowserNavigation(controller) + const profileState = { paneName: 'profile', paneUri: 'https://pod.example/profile/card#me', viaAccountMenu: true } + const intercept = vi.fn() + + writeNavigationHistory(window.history, 'replaceState', profileState, '') + historyWrite(intercept) + + expect(intercept).not.toHaveBeenCalled() + expect(show).not.toHaveBeenCalled() + expect(window.history.state).toEqual(profileState) + }) + it('clears the write guard even if history throws', async () => { const { registerBrowserNavigation } = await freshModule() const { writeNavigationHistory } = await import('../../../src/navigation/state') From df4e2d06203f5c0073736f901814ad02f56312d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 9 Nov 2026 22:16:54 +0000 Subject: [PATCH 2/2] chore: update solidos dependencies (dev: solid-logic@6.0.1-6 solid-ui@5.0.2-8 pane-registry@5.0.2-3 folder-pane@4.0.1-2 profile-pane@4.1.0-0 source-pane@4.0.1-0) (latest: rdflib@2.4.1 activitystreams-pane@2.0.0 chat-pane@4.0.0 contacts-pane@4.0.0 issue-pane@4.0.0 meeting-pane@4.0.0) --- package-lock.json | 122 +++++++++++++++++++++++----------------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1b37d8c0..47f2a9e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3744,17 +3744,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz", - "integrity": "sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.2.tgz", + "integrity": "sha512-222PKFXKihmFpi/pS5njoBgdAqsPhj2/GBmFbrNw8Tq8gEG9Sv+kSlKFy4u2q+mJv9ZzJgM2Gkv/bUzRZwZw5g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.72.1", - "@typescript-eslint/type-utils": "8.72.1", - "@typescript-eslint/utils": "8.72.1", - "@typescript-eslint/visitor-keys": "8.72.1", + "@typescript-eslint/scope-manager": "8.72.2", + "@typescript-eslint/type-utils": "8.72.2", + "@typescript-eslint/utils": "8.72.2", + "@typescript-eslint/visitor-keys": "8.72.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -3767,7 +3767,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.72.1", + "@typescript-eslint/parser": "^8.72.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -3783,15 +3783,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.1.tgz", - "integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.2.tgz", + "integrity": "sha512-llP+T1YuJOLlxEciP6OyQGtz5SMxyn+9LA2BvZw5RmToGVa1y4beEDVQL6Gje7T5WFYh19WREjDCN9Ev4gm5AA==", "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.72.1", - "@typescript-eslint/types": "8.72.1", - "@typescript-eslint/typescript-estree": "8.72.1", - "@typescript-eslint/visitor-keys": "8.72.1", + "@typescript-eslint/scope-manager": "8.72.2", + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/typescript-estree": "8.72.2", + "@typescript-eslint/visitor-keys": "8.72.2", "debug": "^4.4.3" }, "engines": { @@ -3807,13 +3807,13 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz", - "integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.2.tgz", + "integrity": "sha512-6y5dLTZdHhI8Eh3yIQ/bfQhRKwn5e+TpJj64o18Cngc0iW/BCrVvrr6zGYqXPnYH31u6YXAYiFh0+qxX5mybJw==", "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.72.1", - "@typescript-eslint/types": "^8.72.1", + "@typescript-eslint/tsconfig-utils": "^8.72.2", + "@typescript-eslint/types": "^8.72.2", "debug": "^4.4.3" }, "engines": { @@ -3828,13 +3828,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz", - "integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.2.tgz", + "integrity": "sha512-iIoMJ6/8c1/QpFNyFjU0d1YzPco4MsZjQw0k3V+vfzNWAFmUmvhlyMoYEoYYDQZXc+J6Gi1X5XcxlzW2taIIGQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.72.1", - "@typescript-eslint/visitor-keys": "8.72.1" + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/visitor-keys": "8.72.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3845,9 +3845,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz", - "integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.2.tgz", + "integrity": "sha512-+h3iCgZXuTNtoLXcnnjP12ZUHLMqoUcIHPfQrFEXTUqvb9dF8QK9b5nv5VtSo5fLkdHuks8ehbTzZj3rImAm9w==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3861,15 +3861,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz", - "integrity": "sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.2.tgz", + "integrity": "sha512-STfkR38qIdhfkLpgtXnUjAzsNy4xouqiBO0lrVc/Wpk9Kem5/aHXUx1J8dGC16zpdYN1+yc2ILQ28er1kXX7ag==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.72.1", - "@typescript-eslint/typescript-estree": "8.72.1", - "@typescript-eslint/utils": "8.72.1", + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/typescript-estree": "8.72.2", + "@typescript-eslint/utils": "8.72.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -3886,9 +3886,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz", - "integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.2.tgz", + "integrity": "sha512-Xfn37OLZx03+PCU7qWx9DTv2FCRh1E6fk2+wONeNY15HXIwtcwuveaSwVpGAUptkNmPQegRxW5JyRel9T7wrog==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3899,15 +3899,15 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz", - "integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.2.tgz", + "integrity": "sha512-pQEGqIK/cqEvKivVWaFLYpe7XYejFxfuAOi4mu1oAWFvIo36aRHI9esRFTUxcIy/z8N482RJyH+LpZB0ZIU7Ow==", "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.72.1", - "@typescript-eslint/tsconfig-utils": "8.72.1", - "@typescript-eslint/types": "8.72.1", - "@typescript-eslint/visitor-keys": "8.72.1", + "@typescript-eslint/project-service": "8.72.2", + "@typescript-eslint/tsconfig-utils": "8.72.2", + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/visitor-keys": "8.72.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -3926,16 +3926,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.1.tgz", - "integrity": "sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.2.tgz", + "integrity": "sha512-hv3eRYbpPbbpezuax1EfGrzpa3C4ySgNZkPUkLOWOAjU8HsOKowEDAr8C7nsNl1C+SBSKDpvno6VKHnlhvp9iA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.72.1", - "@typescript-eslint/types": "8.72.1", - "@typescript-eslint/typescript-estree": "8.72.1" + "@typescript-eslint/scope-manager": "8.72.2", + "@typescript-eslint/types": "8.72.2", + "@typescript-eslint/typescript-estree": "8.72.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3950,12 +3950,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz", - "integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.2.tgz", + "integrity": "sha512-NAGrjn0Pitjqzq/GkgMC4kBYm233PQ8GffyQ9+kTA3AlNQYEzOf25EIM02BAHZv5FRJSpHpJk+OGUxVkOB93GQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/types": "8.72.2", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -10800,16 +10800,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.72.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.1.tgz", - "integrity": "sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==", + "version": "8.72.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.2.tgz", + "integrity": "sha512-c/+AKMW1KB81r28Sp0AAdcyXj5dzNmAsZFcsrxKbZTKmcBK2xIwBR31dcSYSDu3bKmzXe8GKrSxO+eDQ401Uzg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.72.1", - "@typescript-eslint/parser": "8.72.1", - "@typescript-eslint/typescript-estree": "8.72.1", - "@typescript-eslint/utils": "8.72.1" + "@typescript-eslint/eslint-plugin": "8.72.2", + "@typescript-eslint/parser": "8.72.2", + "@typescript-eslint/typescript-estree": "8.72.2", + "@typescript-eslint/utils": "8.72.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0"