diff --git a/deploy/gitea/install-hop-native.sh b/deploy/gitea/install-hop-native.sh index e72d1f1..66a389d 100755 --- a/deploy/gitea/install-hop-native.sh +++ b/deploy/gitea/install-hop-native.sh @@ -47,6 +47,9 @@ docker compose --env-file "$env_file" cp \ docker compose --env-file "$env_file" cp \ deploy/gitea/public/assets/js/hop-native.js \ gitea:/data/gitea/public/assets/js/hop-native-v17.js +docker compose --env-file "$env_file" cp \ + deploy/gitea/public/assets/js/hop-native.js \ + gitea:/data/gitea/public/assets/js/hop-native-v18.js docker compose --env-file "$env_file" cp \ deploy/gitea/public/assets/css/hop-home.css \ gitea:/data/gitea/public/assets/css/hop-home.css diff --git a/deploy/gitea/public/assets/js/hop-native.js b/deploy/gitea/public/assets/js/hop-native.js index 155cfdd..0b2e7ad 100644 --- a/deploy/gitea/public/assets/js/hop-native.js +++ b/deploy/gitea/public/assets/js/hop-native.js @@ -345,15 +345,37 @@ return new URLSearchParams(window.location.search).get('hop') === 'home'; } + function signedIn() { + return Boolean(document.querySelector('meta[name="hop-signed-user-id"]')); + } + + function revealHopPage() { + document.documentElement.dataset.hopNative = 'true'; + } + async function loadPublicHomepage() { const link = document.querySelector('[data-hop-public-home-link]'); if (link) link.classList.toggle('active', publicHomepageRequested()); - if (!link || !publicHomepageRequested() || document.documentElement.dataset.hopPublicHome) return; + if (!publicHomepageRequested()) return; + + if (document.querySelector('#hop-main')) { + document.documentElement.dataset.hopPublicHome = 'rendered'; + revealHopPage(); + return; + } + + if (!signedIn()) { + revealHopPage(); + return; + } + + if (document.documentElement.dataset.hopPublicHome) return; document.documentElement.dataset.hopPublicHome = 'loading'; try { - const publicURL = new URL(link.href, window.location.origin); + const publicURL = new URL(link?.href || window.location.href, window.location.origin); publicURL.search = ''; + publicURL.hash = ''; const response = await fetch(publicURL, {credentials: 'omit'}); if (!response.ok) throw new Error(`Could not load the Hop homepage (${response.status})`); @@ -372,8 +394,10 @@ bindInstallationSwitchers(); bindInstallationCopyButtons(); } catch (error) { - delete document.documentElement.dataset.hopPublicHome; + document.documentElement.dataset.hopPublicHome = 'failed'; console.error(error); + } finally { + revealHopPage(); } } @@ -389,7 +413,12 @@ bindInstallationSwitchers(); bindInstallationCopyButtons(); void loadPublicHomepage(); - document.documentElement.dataset.hopNative = 'true'; + const publicHomepageNeedsSwap = publicHomepageRequested() + && signedIn() + && !document.querySelector('#hop-main'); + if (!publicHomepageNeedsSwap || document.documentElement.dataset.hopPublicHome === 'failed') { + revealHopPage(); + } } let queued = false; diff --git a/deploy/gitea/public/assets/js/hop-native.test.mjs b/deploy/gitea/public/assets/js/hop-native.test.mjs new file mode 100644 index 0000000..a3f1f5f --- /dev/null +++ b/deploy/gitea/public/assets/js/hop-native.test.mjs @@ -0,0 +1,100 @@ +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; +import test from 'node:test'; +import vm from 'node:vm'; + +const source = await readFile(new URL('./hop-native.js', import.meta.url), 'utf8'); + +function page({search = '', signedIn = false, fetchResult} = {}) { + const dataset = {}; + const publicHomeLink = { + classList: {toggle() {}}, + href: 'https://hop.test/', + }; + let currentMainReplaced = false; + const currentMain = { + replaceWith() { + currentMainReplaced = true; + }, + }; + const publicMain = { + parentElement: {querySelectorAll: () => []}, + }; + const document = { + documentElement: {dataset}, + title: 'Dashboard', + addEventListener() {}, + querySelector(selector) { + if (selector === 'meta[name="hop-signed-user-id"]') return signedIn ? {} : null; + if (selector === '[data-hop-public-home-link]') return signedIn ? publicHomeLink : null; + if (selector === '[role="main"]') return currentMain; + return null; + }, + querySelectorAll() { + return []; + }, + }; + const context = { + console: {error() {}}, + document, + DOMParser: class { + parseFromString() { + return { + title: 'Hop', + querySelector: (selector) => selector === '#hop-main' ? publicMain : null, + }; + } + }, + fetch: fetchResult || (() => Promise.resolve({ok: true, text: () => Promise.resolve('
')})), + MutationObserver: class { observe() {} }, + URL, + URLSearchParams, + window: { + location: {href: `https://hop.test/${search}`, origin: 'https://hop.test', pathname: '/', search}, + requestAnimationFrame() {}, + }, + }; + vm.runInNewContext(source, context); + return {dataset, get currentMainReplaced() { return currentMainReplaced; }}; +} + +async function settle() { + await new Promise((resolve) => setImmediate(resolve)); +} + +test('keeps the authenticated dashboard hidden until the public homepage is ready', async () => { + let finishFetch; + const fetchResult = () => new Promise((resolve) => { finishFetch = resolve; }); + const result = page({search: '?hop=home', signedIn: true, fetchResult}); + + assert.equal(result.dataset.hopPublicHome, 'loading'); + assert.equal(result.dataset.hopNative, undefined); + assert.equal(result.currentMainReplaced, false); + + finishFetch({ok: true, text: () => Promise.resolve('
')}); + await settle(); + + assert.equal(result.dataset.hopPublicHome, 'rendered'); + assert.equal(result.dataset.hopNative, 'true'); + assert.equal(result.currentMainReplaced, true); +}); + +test('reveals the existing page when the public homepage request fails', async () => { + const result = page({ + search: '?hop=home', + signedIn: true, + fetchResult: () => Promise.reject(new Error('offline')), + }); + + assert.equal(result.dataset.hopNative, undefined); + await settle(); + + assert.equal(result.dataset.hopPublicHome, 'failed'); + assert.equal(result.dataset.hopNative, 'true'); + assert.equal(result.currentMainReplaced, false); +}); + +test('reveals ordinary pages immediately', () => { + const result = page({signedIn: true}); + assert.equal(result.dataset.hopNative, 'true'); +}); diff --git a/deploy/gitea/templates/custom/footer.tmpl b/deploy/gitea/templates/custom/footer.tmpl index dc7f7c8..5d9478f 100644 --- a/deploy/gitea/templates/custom/footer.tmpl +++ b/deploy/gitea/templates/custom/footer.tmpl @@ -1,3 +1,3 @@ - +