fix(spa): route internal links client-side so the DEK survives
Plain <a href="/..."> links to in-app routes were causing full page
reloads. The DEK lives only in memory (session.svelte.ts), so every
reload drops it and private rows can't decrypt until the user signs
in again. Earlier fix (03ac99e) made the post-reload state usable;
this one removes the reload in the first place.
New helper lib/navigate.ts:
- navigate(path): pushState + dispatch synthetic popstate so the
existing window popstate listener in App.svelte re-parses and
applies the route. SPA state (DEK, decrypted activities, scroll
position) is preserved.
- onSpaLink(event, path): swallow plain left-click only. Modified
clicks (Cmd/Ctrl/Shift/Alt, middle-click, right-click) fall
through to the browser so "open in new tab", copy-link-address,
and screen-reader behaviour all still work.
Applied to the five internal anchors in ActivityRow.svelte:
permalink title (private + non-private), tag chips (private +
non-private), and the owner attribution link to /<user>/liste.
Verified in the browser: clicking a permalink now preserves a
window.__spaProbe marker; back-button likewise stays in-app.
This commit is contained in:
parent
f4816502ed
commit
834abfdfb0
2 changed files with 51 additions and 5 deletions
|
|
@ -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}
|
||||
<h3 id={`act-${activity.id}-h`} style="display: flex; align-items: center;">
|
||||
<a href={`/aktivitet/${activity.id}`} style="color: inherit; text-decoration: none;">
|
||||
<a href={`/aktivitet/${activity.id}`}
|
||||
onclick={(e) => onSpaLink(e, `/aktivitet/${activity.id}`)}
|
||||
style="color: inherit; text-decoration: none;">
|
||||
{decrypted.title}
|
||||
</a>
|
||||
<span class="vis-badge private">Privat</span>
|
||||
|
|
@ -215,7 +218,9 @@
|
|||
{#if decrypted.tags.length}
|
||||
<div>
|
||||
{#each decrypted.tags as t}
|
||||
<a href={`/etiketter/${encodeURIComponent(t)}`} class="tag private"
|
||||
<a href={`/etiketter/${encodeURIComponent(t)}`}
|
||||
onclick={(e) => onSpaLink(e, `/etiketter/${encodeURIComponent(t)}`)}
|
||||
class="tag private"
|
||||
style="text-decoration: none; color: inherit;">{t}</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -236,7 +241,9 @@
|
|||
{/if}
|
||||
{:else}
|
||||
<h3 id={`act-${activity.id}-h`} style="display: flex; align-items: center;">
|
||||
<a href={`/aktivitet/${activity.id}`} style="color: inherit; text-decoration: none;">
|
||||
<a href={`/aktivitet/${activity.id}`}
|
||||
onclick={(e) => onSpaLink(e, `/aktivitet/${activity.id}`)}
|
||||
style="color: inherit; text-decoration: none;">
|
||||
{activity.title}
|
||||
</a>
|
||||
<span class="vis-badge {activity.visibility}">
|
||||
|
|
@ -258,7 +265,9 @@
|
|||
{#if activity.tags.length}
|
||||
<div>
|
||||
{#each activity.tags as t}
|
||||
<a href={`/etiketter/${encodeURIComponent(t)}`} class="tag"
|
||||
<a href={`/etiketter/${encodeURIComponent(t)}`}
|
||||
onclick={(e) => onSpaLink(e, `/etiketter/${encodeURIComponent(t)}`)}
|
||||
class="tag"
|
||||
style="text-decoration: none; color: inherit;">{t}</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -271,7 +280,8 @@
|
|||
<p class="muted" style="font-size: 0.8rem;">
|
||||
Lagt til av
|
||||
{#if activity.owner_username}
|
||||
<a href={`/${activity.owner_username}/liste`}>{activity.owner_display}</a>
|
||||
<a href={`/${activity.owner_username}/liste`}
|
||||
onclick={(e) => onSpaLink(e, `/${activity.owner_username}/liste`)}>{activity.owner_display}</a>
|
||||
{:else}
|
||||
{activity.owner_display}
|
||||
{/if}
|
||||
|
|
|
|||
36
frontend/src/lib/navigate.ts
Normal file
36
frontend/src/lib/navigate.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* Client-side navigation helper for the SPA.
|
||||
*
|
||||
* Why this exists: a plain `<a href="/...">` 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue