mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
fix(share): restore a share link opened in an already-open tab (#1416)
* fix(share): restore a share link opened by hash navigation Opening a different share link in an already-open tab (pasting it into the address bar, following a link, or Back/Forward) changed the URL but left the camera, visual style, map, panels and layers on the old view until a reload. The share manager now listens for hashchange and popstate. A new hash that parses as a share and differs from the last hash the app read or wrote runs the start-up restore pipeline: lane authority is reserved, URL writes are held (including a pending debounced write of the old view), the camera restore takes a new navigation generation, and the layer coordinator replaces any in-flight layer restore with the new share's layers through the passive share origin. A newer navigation supersedes an older restore end to end; only the newest one releases URL writes. The app's own replaceState writes never restore. Listeners are removed on destroy. Returning to the Normal style and resetting an already-active preset now also apply when a share is restored after start-up, so the result matches a fresh load of the same link. * docs: changelog for #1416
This commit is contained in:
1 parent
301bc2e0b3
commit
b7ece1d4d1
8 files changed
+688
-58
No files matched your search
+69
-10
@@ -122,6 +122,13 @@ export class ShareLinkManager {
|
||||
this._panelStateProvider = null;
|
||||
this._styleParamStateProvider = null;
|
||||
this._initialRestorePending = false;
|
||||
// Each incoming share (start-up or a later hash navigation) gets a token;
|
||||
// only the newest one may release URL-write suppression.
|
||||
this._restoreToken = 0;
|
||||
// The hash this page last read or wrote. A location change back to it is
|
||||
// the app's own state, never an incoming share.
|
||||
this._knownHash = null;
|
||||
this._unwatchNavigation = null;
|
||||
this._restoreAuthority = {
|
||||
visual: 0,
|
||||
map: 0,
|
||||
@@ -152,6 +159,15 @@ export class ShareLinkManager {
|
||||
*/
|
||||
parseInitialHash() {
|
||||
const hash = window.location.hash.slice(1);
|
||||
this._knownHash = hash;
|
||||
const state = this._parseShareHash(hash);
|
||||
if (!state) return null;
|
||||
this._beginIncomingRestore(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
/** Parse one hash without side effects. Returns null unless it is a share. */
|
||||
_parseShareHash(hash) {
|
||||
if (!hash) return null;
|
||||
|
||||
const params = new URLSearchParams(hash);
|
||||
@@ -242,15 +258,55 @@ export class ShareLinkManager {
|
||||
panelState: decodePanelStateParams(params),
|
||||
sharedAtMs: decodeShareCreatedAtMs(params),
|
||||
};
|
||||
return state;
|
||||
}
|
||||
|
||||
/** Reserve restore lanes and hold URL writes for one incoming share. */
|
||||
_beginIncomingRestore(state) {
|
||||
state.restoreAuthority = {
|
||||
visual: this._restoreAuthority.visual,
|
||||
map: this._restoreAuthority.map,
|
||||
panels: new Map(this._restoreAuthority.panels),
|
||||
};
|
||||
|
||||
// Hold URL writes until the complete incoming state has been restored.
|
||||
state.restoreToken = ++this._restoreToken;
|
||||
// Hold URL writes until the complete incoming state has been restored. A
|
||||
// debounced write of the previous view must not land over the new link.
|
||||
this._initialRestorePending = true;
|
||||
return state;
|
||||
clearTimeout(this._debounceTimer);
|
||||
this._debounceTimer = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a share link opened in this tab after start-up: a pasted address,
|
||||
* a followed link, or Back/Forward. The app's own replaceState writes never
|
||||
* fire these events, and a hash equal to the last one read or written is
|
||||
* ignored anyway, so only a genuinely different incoming share restores.
|
||||
*/
|
||||
watchNavigation(onNavigate) {
|
||||
this._unwatchNavigation?.();
|
||||
if (this._destroyed || typeof onNavigate !== 'function') return;
|
||||
const target = globalThis.window;
|
||||
if (typeof target?.addEventListener !== 'function') return;
|
||||
const handler = () => this._handleLocationChange(onNavigate);
|
||||
target.addEventListener('hashchange', handler);
|
||||
target.addEventListener('popstate', handler);
|
||||
this._unwatchNavigation = () => {
|
||||
target.removeEventListener('hashchange', handler);
|
||||
target.removeEventListener('popstate', handler);
|
||||
this._unwatchNavigation = null;
|
||||
};
|
||||
}
|
||||
|
||||
_handleLocationChange(onNavigate) {
|
||||
if (this._destroyed) return;
|
||||
const hash = window.location.hash.slice(1);
|
||||
// Back/Forward fires both popstate and hashchange for one entry.
|
||||
if (hash === this._knownHash) return;
|
||||
this._knownHash = hash;
|
||||
const state = this._parseShareHash(hash);
|
||||
if (!state) return;
|
||||
this._beginIncomingRestore(state);
|
||||
onNavigate(state);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -374,9 +430,13 @@ export class ShareLinkManager {
|
||||
};
|
||||
}
|
||||
|
||||
/** Release initial hash suppression only after every restore owner settles. */
|
||||
completeInitialRestore() {
|
||||
/**
|
||||
* Release hash suppression only after every restore owner settles. A token
|
||||
* from a superseded share restore leaves the newer one holding the URL.
|
||||
*/
|
||||
completeInitialRestore(restoreToken = this._restoreToken) {
|
||||
if (!this._initialRestorePending) return;
|
||||
if (restoreToken !== this._restoreToken) return;
|
||||
this._initialRestorePending = false;
|
||||
this._scheduleUpdate();
|
||||
}
|
||||
@@ -528,11 +588,9 @@ export class ShareLinkManager {
|
||||
if (!params) return;
|
||||
// The hash belongs to this page's address, not the document base the
|
||||
// app may load under.
|
||||
history.replaceState(
|
||||
null,
|
||||
'',
|
||||
new URL(`#${params.toString()}`, window.location.href).href,
|
||||
);
|
||||
const url = new URL(`#${params.toString()}`, window.location.href);
|
||||
this._knownHash = url.hash.slice(1);
|
||||
history.replaceState(null, '', url.href);
|
||||
}
|
||||
|
||||
/** Build a deterministic snapshot without mutating history. */
|
||||
@@ -615,6 +673,7 @@ export class ShareLinkManager {
|
||||
this._debounceTimer = null;
|
||||
this._removeCameraChanged?.();
|
||||
this._removeCameraChanged = null;
|
||||
this._unwatchNavigation?.();
|
||||
this._layerStateProvider = null;
|
||||
this._panelStateProvider = null;
|
||||
this._styleParamStateProvider = null;
|
||||
|
||||
Reference in new issue
Block a user