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 @@
-
+