diff --git a/frontend/src/components/ActivityRow.svelte b/frontend/src/components/ActivityRow.svelte index a330d7d..2aa0acd 100644 --- a/frontend/src/components/ActivityRow.svelte +++ b/frontend/src/components/ActivityRow.svelte @@ -5,6 +5,7 @@ type PrivatePayload, } from '../lib/crypto'; import { api } from '../lib/api'; + import { onSpaLink } from '../lib/navigate'; import { privateTagIndex } from '../lib/tagIndex'; import type { Activity } from '../../../shared/types'; @@ -199,7 +200,9 @@ {#if activity.visibility === 'private'} {#if decrypted}

- + onSpaLink(e, `/aktivitet/${activity.id}`)} + style="color: inherit; text-decoration: none;"> {decrypted.title} Privat @@ -215,7 +218,9 @@ {#if decrypted.tags.length}
{#each decrypted.tags as t} - onSpaLink(e, `/etiketter/${encodeURIComponent(t)}`)} + class="tag private" style="text-decoration: none; color: inherit;">{t} {/each}
@@ -236,7 +241,9 @@ {/if} {:else}

- + onSpaLink(e, `/aktivitet/${activity.id}`)} + style="color: inherit; text-decoration: none;"> {activity.title} @@ -258,7 +265,9 @@ {#if activity.tags.length}
{#each activity.tags as t} - onSpaLink(e, `/etiketter/${encodeURIComponent(t)}`)} + class="tag" style="text-decoration: none; color: inherit;">{t} {/each}
@@ -271,7 +280,8 @@

Lagt til av {#if activity.owner_username} - {activity.owner_display} + onSpaLink(e, `/${activity.owner_username}/liste`)}>{activity.owner_display} {:else} {activity.owner_display} {/if} diff --git a/frontend/src/lib/navigate.ts b/frontend/src/lib/navigate.ts new file mode 100644 index 0000000..929c8c1 --- /dev/null +++ b/frontend/src/lib/navigate.ts @@ -0,0 +1,36 @@ +/** + * Client-side navigation helper for the SPA. + * + * Why this exists: a plain `` to an in-app route causes a + * full page reload. The DEK lives only in memory (see session.svelte.ts) + * and gets dropped on reload, which means clicking an internal link as a + * regular browser navigation makes private content un-decryptable. We + * route in-app navigation through history.pushState instead so the SPA + * keeps its in-memory state (DEK, decrypted activities, scroll position). + * + * Links still keep their `href` attribute so: + * - Right-click → "Open in new tab" works + * - Cmd/Ctrl + click opens in a new tab + * - Middle-click opens in a new tab + * - Copy-link-address works + * - Screen readers announce them as links + * onSpaLink only swallows the *plain* left-click. + */ + +export function navigate(path: string): void { + if (window.location.pathname === path) return; + window.history.pushState({}, '', path); + // App.svelte's popstate listener owns the "parse + apply route" logic; + // dispatch one ourselves so pushState behaves the same as back/forward. + window.dispatchEvent(new PopStateEvent('popstate')); +} + +export function onSpaLink(e: MouseEvent, path: string): void { + // Let the browser handle modified clicks (new tab, new window, download) + // and right/middle clicks. We only take over the plain left-click. + if (e.defaultPrevented) return; + if (e.button !== 0) return; + if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; + e.preventDefault(); + navigate(path); +}