Feature(offline): more offline hardening

This commit is contained in:
Pieter Vander Vennet 2025-08-08 13:19:49 +02:00
parent f1da97285f
commit 561e4cb009
7 changed files with 103 additions and 69 deletions

View file

@ -19,6 +19,7 @@ import LayerConfig from "./LayerConfig"
import ComparingTag from "../../Logic/Tags/ComparingTag" import ComparingTag from "../../Logic/Tags/ComparingTag"
import { Unit } from "../Unit" import { Unit } from "../Unit"
import { Lists } from "../../Utils/Lists" import { Lists } from "../../Utils/Lists"
import { IsOnline } from "../../Logic/Web/IsOnline"
export interface Mapping { export interface Mapping {
readonly if: UploadableTag readonly if: UploadableTag
@ -1154,10 +1155,10 @@ export class TagRenderingConfigUtils {
const extraMappings = tags.bindD((tags) => { const extraMappings = tags.bindD((tags) => {
const country = tags._country const country = tags._country
if (country === undefined) { if (country === undefined) {
return undefined return undefined
} }
const center = GeoOperations.centerpointCoordinates(feature) const center = GeoOperations.centerpointCoordinates(feature)
return UIEventSource.fromPromise( return UIEventSource.fromPromiseWithErr(
NameSuggestionIndex.generateMappings( NameSuggestionIndex.generateMappings(
config.freeform.key, config.freeform.key,
tags, tags,
@ -1167,7 +1168,20 @@ export class TagRenderingConfigUtils {
) )
) )
}) })
return extraMappings.mapD((extraMappings) => { return extraMappings.map((extraMappingsErr) => {
if(extraMappingsErr?.["error"]){
console.log("Could not download the NSI: ", extraMappingsErr["error"])
return config
}
const extraMappings = extraMappingsErr?.success
if(extraMappings === undefined){
if(!IsOnline.isOnline.data){
// The 'extraMappings' will still attempt to download the NSI - it might be in the service worker's cache
// As such, if they happen to come through anyway, they'll be shown
return config
}
return undefined
}
if (extraMappings.length == 0) { if (extraMappings.length == 0) {
return config return config
} }
@ -1187,6 +1201,6 @@ export class TagRenderingConfigUtils {
}) ?? [] }) ?? []
clone.mappings = [...oldMappingsCloned, ...extraMappings] clone.mappings = [...oldMappingsCloned, ...extraMappings]
return clone return clone
}) }, [IsOnline.isOnline])
} }
} }

View file

@ -13,9 +13,10 @@
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte" import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import GlobeAlt from "@babeard/svelte-heroicons/mini/GlobeAlt"
import { ComparisonState } from "./ComparisonState" import { ComparisonState } from "./ComparisonState"
import LoginToggle from "../Base/LoginToggle.svelte" import LoginToggle from "../Base/LoginToggle.svelte"
import { IsOnline } from "../../Logic/Web/IsOnline"
import GlobeAlt from "@babeard/svelte-heroicons/mini/GlobeAlt"
export let externalData: Store< export let externalData: Store<
| { success: { content: Record<string, string> } } | { success: { content: Record<string, string> } }
@ -33,7 +34,7 @@
* A switch that signals that the information should be downloaded. * A switch that signals that the information should be downloaded.
* The actual 'download' code is _not_ implemented here * The actual 'download' code is _not_ implemented here
*/ */
export let downloadInformation : UIEventSource<boolean> export let downloadInformation: UIEventSource<boolean>
export let collapsed: boolean export let collapsed: boolean
const t = Translations.t.external const t = Translations.t.external
@ -48,45 +49,47 @@
let propertyKeysExternal = comparisonState.mapD((ct) => ct.propertyKeysExternal) let propertyKeysExternal = comparisonState.mapD((ct) => ct.propertyKeysExternal)
let hasDifferencesAtStart = comparisonState.mapD((ct) => ct.hasDifferencesAtStart) let hasDifferencesAtStart = comparisonState.mapD((ct) => ct.hasDifferencesAtStart)
let enableLogin = state.featureSwitches.featureSwitchEnableLogin let enableLogin = state.featureSwitches.featureSwitchEnableLogin
const online = IsOnline.isOnline
</script> </script>
{#if $online}
<LoginToggle {state} silentFail> <LoginToggle {state} hiddenFail>
{#if !$sourceUrl || !$enableLogin} {#if !$sourceUrl || !$enableLogin}
<!-- empty block --> <!-- empty block -->
{:else if !$downloadInformation} {:else if !$downloadInformation}
<button on:click={() => downloadInformation.set(true)}> <button on:click={() => downloadInformation.set(true)}>
Attempt to download information from the website {$sourceUrl} Attempt to download information from the website {$sourceUrl}
</button> </button>
{:else if $externalData === undefined} {:else if $externalData === undefined}
<div class="flex justify-center"> <div class="flex justify-center">
<Loading /> <Loading />
</div> </div>
{:else if $externalData["error"] !== undefined} {:else if $externalData["error"] !== undefined}
<div class="subtle low-interaction rounded p-2 px-4 italic"> <div class="subtle low-interaction rounded p-2 px-4 italic">
<Tr t={Translations.t.external.error} /> <Tr t={Translations.t.external.error} />
</div> </div>
{:else if $propertyKeysExternal.length === 0 && $knownImages.size + $unknownImages.length === 0} {:else if $propertyKeysExternal.length === 0 && $knownImages.size + $unknownImages.length === 0}
<Tr cls="subtle" t={t.noDataLoaded} /> <Tr cls="subtle" t={t.noDataLoaded} />
{:else if !$hasDifferencesAtStart} {:else if !$hasDifferencesAtStart}
<span class="subtle text-sm"> <span class="subtle text-sm">
<Tr t={t.allIncluded.Subs({ source: $sourceUrl })} /> <Tr t={t.allIncluded.Subs({ source: $sourceUrl })} />
</span> </span>
{:else if $comparisonState !== undefined} {:else if $comparisonState !== undefined}
<AccordionSingle expanded={!collapsed}> <AccordionSingle expanded={!collapsed}>
<span slot="header" class="flex"> <span slot="header" class="flex">
<GlobeAlt class="h-6 w-6" /> <GlobeAlt class="h-6 w-6" />
<Tr t={Translations.t.external.title} /> <Tr t={Translations.t.external.title} />
</span> </span>
<ComparisonTable <ComparisonTable
externalProperties={$externalData["success"]} externalProperties={$externalData["success"]}
{state} {state}
{feature} {feature}
{layer} {layer}
{tags} {tags}
{readonly} {readonly}
sourceUrl={$sourceUrl} sourceUrl={$sourceUrl}
comparisonState={$comparisonState} comparisonState={$comparisonState}
/> />
</AccordionSingle> </AccordionSingle>
{/if} {/if}
</LoginToggle> </LoginToggle>
{/if}

View file

@ -93,7 +93,7 @@
</Loading> </Loading>
</div> </div>
{/if} {/if}
{#if !$online} {#if !$online && $pending > 0}
<div class="alert"> <div class="alert">
<Tr t={t.upload.offline} /> <Tr t={t.upload.offline} />
</div> </div>

View file

@ -17,20 +17,18 @@ export class DeleteFlowState {
private readonly _id: OsmId private readonly _id: OsmId
private readonly _allowDeletionAtChangesetCount: number private readonly _allowDeletionAtChangesetCount: number
private readonly _osmConnection: OsmConnection private readonly _osmConnection: OsmConnection
private readonly state: SpecialVisualizationState
constructor( constructor(
id: OsmId, id: OsmId,
state: SpecialVisualizationState, state: SpecialVisualizationState,
allowDeletionAtChangesetCount?: number allowDeletionAtChangesetCount?: number
) { ) {
this.state = state
this.objectDownloader = state.osmObjectDownloader this.objectDownloader = state.osmObjectDownloader
this._id = id this._id = id
this._osmConnection = state.osmConnection this._osmConnection = state.osmConnection
this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE
this.CheckDeleteability(false) this.checkDeleteability(false)
} }
/** /**
@ -39,7 +37,7 @@ export class DeleteFlowState {
* @constructor * @constructor
* @private * @private
*/ */
public CheckDeleteability(useTheInternet: boolean): void { public checkDeleteability(useTheInternet: boolean): void {
console.log("Checking deleteability (internet?", useTheInternet, ")") console.log("Checking deleteability (internet?", useTheInternet, ")")
const t = Translations.t.delete const t = Translations.t.delete
const id = this._id const id = this._id

View file

@ -22,6 +22,7 @@
import Invalid from "../../../assets/svg/Invalid.svelte" import Invalid from "../../../assets/svg/Invalid.svelte"
import { And } from "../../../Logic/Tags/And" import { And } from "../../../Logic/Tags/And"
import type { UploadableTag } from "../../../Logic/Tags/TagTypes" import type { UploadableTag } from "../../../Logic/Tags/TagTypes"
import { IsOnline } from "../../../Logic/Web/IsOnline"
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
export let deleteConfig: DeleteConfig export let deleteConfig: DeleteConfig
@ -39,9 +40,10 @@
const canBeDeletedReason = deleteAbility.canBeDeletedReason const canBeDeletedReason = deleteAbility.canBeDeletedReason
const hasSoftDeletion = deleteConfig.softDeletionTags !== undefined const hasSoftDeletion = deleteConfig.softDeletionTags !== undefined
const online = IsOnline.isOnline
let currentState: "confirm" | "applying" | "deleted" = "confirm" let currentState: "confirm" | "applying" | "deleted" = "confirm"
$: { $: {
deleteAbility.CheckDeleteability(true) deleteAbility.checkDeleteability(true)
} }
const t = Translations.t.delete const t = Translations.t.delete
@ -97,8 +99,10 @@
currentState = "deleted" currentState = "deleted"
} }
</script> </script>
{#if !$online}
<LoginToggle ignoreLoading={true} {state} silentFail> <div class="subtle">You are offline. Deleting points is not possible</div>
{:else}
<LoginToggle ignoreLoading={true} {state} hiddenFail>
{#if $canBeDeleted === false && !hasSoftDeletion} {#if $canBeDeleted === false && !hasSoftDeletion}
<div class="low-interaction subtle flex gap-x-1 rounded p-2 text-sm italic"> <div class="low-interaction subtle flex gap-x-1 rounded p-2 text-sm italic">
<div class="relative h-fit"> <div class="relative h-fit">
@ -171,3 +175,4 @@
</AccordionSingle> </AccordionSingle>
{/if} {/if}
</LoginToggle> </LoginToggle>
{/if}

View file

@ -7,41 +7,45 @@
import LoginToggle from "../Base/LoginToggle.svelte" import LoginToggle from "../Base/LoginToggle.svelte"
import type { Feature } from "geojson" import type { Feature } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { IsOnline } from "../../Logic/Web/IsOnline"
import { Store } from "../../Logic/UIEventSource.js"
/** /**
* A full-blown 'mark as favourite'-button * A full-blown 'mark as favourite'-button
*/ */
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
export let feature: Feature export let feature: Feature
export let tags: Record<string, string> export let tags: Store<Record<string, string>>
export let layer: LayerConfig export let layer: LayerConfig
let isFavourite = tags?.map((tags) => tags._favourite === "yes") let isFavourite = tags?.map((tags) => tags._favourite === "yes")
const t = Translations.t.favouritePoi const t = Translations.t.favouritePoi
const online = IsOnline.isOnline
function markFavourite(isFavourite: boolean) { function markFavourite(isFavourite: boolean) {
state.favourites.markAsFavourite(feature, layer.id, state.theme.id, tags, isFavourite) state.favourites.markAsFavourite(feature, layer.id, state.theme.id, tags, isFavourite)
} }
</script> </script>
{#if $online}
<LoginToggle ignoreLoading={true} {state}> <LoginToggle ignoreLoading hiddenFail {state}>
{#if $isFavourite} {#if $isFavourite}
<div class="flex h-fit items-start"> <div class="flex h-fit items-start">
<button class="w-full" on:click={() => markFavourite(false)}> <button class="w-full" on:click={() => markFavourite(false)}>
<HeartSolidIcon class="mr-2 w-16 shrink-0" on:click={() => markFavourite(false)} /> <HeartSolidIcon class="mr-2 w-16 shrink-0" on:click={() => markFavourite(false)} />
<div class="flex flex-col items-start"> <div class="flex flex-col items-start">
<Tr t={t.button.unmark} /> <Tr t={t.button.unmark} />
<Tr cls="normal-font subtle" t={t.button.unmarkNotDeleted} /> <Tr cls="normal-font subtle" t={t.button.unmarkNotDeleted} />
</div>
</button>
</div>
<Tr cls="font-bold thanks m-2 p-2 block" t={t.button.isFavourite} />
{:else}
<button class="w-full" on:click={() => markFavourite(true)}>
<HeartOutlineIcon class="mr-2 w-16 shrink-0" on:click={() => markFavourite(true)} />
<div class="flex w-full flex-col items-start">
<Tr t={t.button.markAsFavouriteTitle} />
<Tr cls="normal-font subtle" t={t.button.markDescription} />
</div> </div>
</button> </button>
</div> {/if}
<Tr cls="font-bold thanks m-2 p-2 block" t={t.button.isFavourite} /> </LoginToggle>
{:else} {/if}
<button class="w-full" on:click={() => markFavourite(true)}>
<HeartOutlineIcon class="mr-2 w-16 shrink-0" on:click={() => markFavourite(true)} />
<div class="flex w-full flex-col items-start">
<Tr t={t.button.markAsFavouriteTitle} />
<Tr cls="normal-font subtle" t={t.button.markDescription} />
</div>
</button>
{/if}
</LoginToggle>

View file

@ -71,6 +71,16 @@
window.requestIdleCallback(() => { window.requestIdleCallback(() => {
InstallServiceWorker.precache(layer["_usedImages"]?.filter(i => i.startsWith("./"))) InstallServiceWorker.precache(layer["_usedImages"]?.filter(i => i.startsWith("./")))
}) })
// The NSI
window.requestIdleCallback(() => {
InstallServiceWorker.precache(
[Constants.nsiLogosEndpoint + "nsi.min.json",
Constants.nsiLogosEndpoint + "featureCollection.min.json",
],
)
})
} }
}).catch(e => console.error("Could not install service worker:", e)) }).catch(e => console.error("Could not install service worker:", e))
</script> </script>