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
+128
-37
@@ -80,6 +80,10 @@ export class ShareRestoration {
|
||||
this._initialShareRestoreTimeout = null;
|
||||
this._layerStateCoordinator = null;
|
||||
this._layerStateRestorePromise = null;
|
||||
// Bumped by every share opened after start-up; an older restore that
|
||||
// settles later must neither apply follow-up work nor release URL writes.
|
||||
this._shareRestoreGeneration = 0;
|
||||
this._navigatedShareRestoreActive = false;
|
||||
}
|
||||
attachLinks(shareLinkManager) {
|
||||
this.shareLinkManager = shareLinkManager;
|
||||
@@ -108,42 +112,12 @@ export class ShareRestoration {
|
||||
const applyCamera =
|
||||
Number.isInteger(generation) &&
|
||||
this.navigation._reassertNavigationHandoff(generation);
|
||||
void (async () => {
|
||||
try {
|
||||
const share = await this.shareLinkManager.applyState(savedState, {
|
||||
applyCamera,
|
||||
navigationToken: generation,
|
||||
});
|
||||
const layers = await (this._layerStateRestorePromise ||
|
||||
Promise.resolve([]));
|
||||
const tracking =
|
||||
share.camera === 'applied'
|
||||
? await this._layerStateCoordinator?.restoreShareTrackingSelection?.()
|
||||
: {
|
||||
status: 'superseded',
|
||||
cleared:
|
||||
this._layerStateCoordinator?.cancelPendingShareTracking?.(
|
||||
'shared-camera-superseded',
|
||||
{ clearSelection: true },
|
||||
) === true,
|
||||
};
|
||||
this.shareLinkManager.completeInitialRestore();
|
||||
this._settleInitialShareRestore({
|
||||
status: 'settled',
|
||||
share,
|
||||
layers,
|
||||
tracking,
|
||||
});
|
||||
} catch (error) {
|
||||
this.shareLinkManager.completeInitialRestore();
|
||||
this._settleInitialShareRestore({
|
||||
status: 'failed',
|
||||
error: String(error?.message || error),
|
||||
share: null,
|
||||
layers: [],
|
||||
});
|
||||
}
|
||||
})();
|
||||
void this._runShareRestore(savedState, {
|
||||
applyCamera,
|
||||
navigationToken: generation,
|
||||
layersPromise: this._layerStateRestorePromise,
|
||||
settle: (result) => this._settleInitialShareRestore(result),
|
||||
});
|
||||
}, 1500);
|
||||
} else {
|
||||
this.syncShareState();
|
||||
@@ -155,7 +129,8 @@ export class ShareRestoration {
|
||||
if (
|
||||
this._disposed ||
|
||||
!this._hasShareState ||
|
||||
!this._resolveInitialShareRestore
|
||||
(!this._resolveInitialShareRestore &&
|
||||
!this._navigatedShareRestoreActive)
|
||||
)
|
||||
return;
|
||||
stampInitialShareGesture((options) =>
|
||||
@@ -176,6 +151,122 @@ export class ShareRestoration {
|
||||
passive: true,
|
||||
},
|
||||
);
|
||||
// A different share opened in this tab later (pasted address, link,
|
||||
// Back/Forward) runs the same restore as start-up.
|
||||
this.shareLinkManager?.watchNavigation?.((state) =>
|
||||
this._restoreNavigatedShare(state),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply one incoming share: camera, visual, map and panels, then layers and
|
||||
* any shared Follow. Only the newest share may finish: a superseded run
|
||||
* stops before follow-up work and leaves URL writes suppressed for the
|
||||
* newer one.
|
||||
*/
|
||||
async _runShareRestore(
|
||||
state,
|
||||
{ applyCamera, navigationToken, layersPromise, settle },
|
||||
) {
|
||||
const generation = this._shareRestoreGeneration;
|
||||
const current = () =>
|
||||
!this._disposed && generation === this._shareRestoreGeneration;
|
||||
try {
|
||||
const share = await this.shareLinkManager.applyState(state, {
|
||||
applyCamera,
|
||||
navigationToken,
|
||||
});
|
||||
const layers = await (layersPromise || Promise.resolve([]));
|
||||
if (!current()) return;
|
||||
const tracking =
|
||||
share.camera === 'applied'
|
||||
? await this._layerStateCoordinator?.restoreShareTrackingSelection?.()
|
||||
: {
|
||||
status: 'superseded',
|
||||
cleared:
|
||||
this._layerStateCoordinator?.cancelPendingShareTracking?.(
|
||||
'shared-camera-superseded',
|
||||
{ clearSelection: true },
|
||||
) === true,
|
||||
};
|
||||
if (!current()) return;
|
||||
this.shareLinkManager.completeInitialRestore(state.restoreToken);
|
||||
settle({ status: 'settled', share, layers, tracking });
|
||||
} catch (error) {
|
||||
if (!current()) return;
|
||||
this.shareLinkManager.completeInitialRestore(state.restoreToken);
|
||||
settle({
|
||||
status: 'failed',
|
||||
error: String(error?.message || error),
|
||||
share: null,
|
||||
layers: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Restore a share opened in this tab after start-up, superseding any other. */
|
||||
_restoreNavigatedShare(state) {
|
||||
if (this._disposed || !state) return;
|
||||
this._shareRestoreGeneration += 1;
|
||||
const settle = (result) => {
|
||||
this._navigatedShareRestoreActive = false;
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('gev:share-navigation-restore-settled', {
|
||||
detail: result,
|
||||
}),
|
||||
);
|
||||
};
|
||||
try {
|
||||
return this._beginNavigatedShareRestore(state, settle);
|
||||
} catch (error) {
|
||||
// Never leave the address bar frozen on a restore that did not start.
|
||||
this.shareLinkManager.completeInitialRestore(state.restoreToken);
|
||||
settle({
|
||||
status: 'failed',
|
||||
error: String(error?.message || error),
|
||||
share: null,
|
||||
layers: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_beginNavigatedShareRestore(state, settle) {
|
||||
// A start-up restore still waiting for its delayed flight never runs.
|
||||
clearTimeout(this._initialShareRestoreTimeout);
|
||||
this._initialShareRestoreTimeout = null;
|
||||
this._settleInitialShareRestore({
|
||||
status: 'superseded',
|
||||
share: null,
|
||||
layers: [],
|
||||
});
|
||||
this._initialShareState = state;
|
||||
this._initialShareSelectionSuperseded = false;
|
||||
this._hasShareState = true;
|
||||
this._navigatedShareRestoreActive = true;
|
||||
// A new deferred navigation generation retires any older share flight.
|
||||
const navigationToken = this.navigation._beginDeferredNavigation(
|
||||
'shared view',
|
||||
{ cancelPendingSelection: false },
|
||||
);
|
||||
// Releasing the previous Follow is part of the passive share restore and
|
||||
// must not be saved as the recipient's own layer preference.
|
||||
const applyCamera =
|
||||
Number.isInteger(navigationToken) &&
|
||||
this.navigation._reassertNavigationHandoff(navigationToken, {
|
||||
trackingOrigin: 'programmatic',
|
||||
});
|
||||
const layersPromise = this._layerStateCoordinator
|
||||
? this._layerStateCoordinator.applyShareLayerState({
|
||||
shareLayerState: state.layerState || null,
|
||||
shareCreatedAtMs: state.sharedAtMs ?? null,
|
||||
})
|
||||
: null;
|
||||
return this._runShareRestore(state, {
|
||||
applyCamera,
|
||||
navigationToken,
|
||||
layersPromise,
|
||||
settle,
|
||||
});
|
||||
}
|
||||
connect(dataManager) {
|
||||
this._dataManager = dataManager;
|
||||
|
||||
Reference in new issue
Block a user