Fix: see #2212: actually save custom themes as visited

This commit is contained in:
Pieter Vander Vennet 2024-10-17 02:10:25 +02:00
parent 91f5c8f166
commit 9427083939
19 changed files with 129 additions and 75 deletions

View file

@ -183,7 +183,7 @@ export default class GeoLocationHandler {
}
private initUserLocationTrail() {
const features = LocalStorageSource.GetParsed<Feature[]>("gps_location_history", [])
const features = LocalStorageSource.getParsed<Feature[]>("gps_location_history", [])
const now = new Date().getTime()
features.data = features.data.filter((ff) => {
if (ff.properties === undefined) {

View file

@ -31,7 +31,7 @@ export default class InitialMapPositioning {
deflt: number,
docs: string
): UIEventSource<number> {
const localStorage = LocalStorageSource.Get(key)
const localStorage = LocalStorageSource.get(key)
const previousValue = localStorage.data
const src = UIEventSource.asFloat(
QueryParameters.GetQueryParameter(key, "" + deflt, docs).syncWith(localStorage)

View file

@ -132,14 +132,14 @@ export default class DetermineLayout {
let json: any
// layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter
const dedicatedHashFromLocalStorage = LocalStorageSource.Get(
const dedicatedHashFromLocalStorage = LocalStorageSource.get(
"user-layout-" + userLayoutParam.data?.replace(" ", "_")
)
if (dedicatedHashFromLocalStorage.data?.length < 10) {
dedicatedHashFromLocalStorage.setData(undefined)
}
const hashFromLocalStorage = LocalStorageSource.Get("last-loaded-user-layout")
const hashFromLocalStorage = LocalStorageSource.get("last-loaded-user-layout")
if (hash.length < 10) {
hash = dedicatedHashFromLocalStorage.data ?? hashFromLocalStorage.data
} else {

View file

@ -24,7 +24,7 @@ import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesSto
*/
export class Changes {
public readonly pendingChanges: UIEventSource<ChangeDescription[]> =
LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
LocalStorageSource.getParsed<ChangeDescription[]>("pending-changes", [])
public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined)
public readonly state: {
allElements?: IndexedFeatureSource

View file

@ -210,7 +210,7 @@ export class OsmConnection {
console.log("Trying to log in...")
this.updateAuthObject()
LocalStorageSource.Get("location_before_login").setData(
LocalStorageSource.get("location_before_login").setData(
Utils.runningFromConsole ? undefined : window.location.href
)
this.auth.xhr(
@ -521,7 +521,7 @@ export class OsmConnection {
this.auth.authenticate(function () {
// Fully authed at this point
console.log("Authentication successful!")
const previousLocation = LocalStorageSource.Get("location_before_login")
const previousLocation = LocalStorageSource.get("location_before_login")
callback(previousLocation.data)
})
}

View file

@ -6,7 +6,11 @@ import { Utils } from "../../Utils"
export class OsmPreferences {
private preferences: Record<string, UIEventSource<string>> = {}
/**
* A 'cache' of all the preference stores
* @private
*/
private readonly preferences: Record<string, UIEventSource<string>> = {}
private localStorageInited: Set<string> = new Set()
/**
@ -15,6 +19,10 @@ export class OsmPreferences {
*/
private seenKeys: string[] = []
/**
* Contains a dictionary which has all preferences
* @private
*/
private readonly _allPreferences: UIEventSource<Record<string, string>> = new UIEventSource({})
public readonly allPreferences: Store<Readonly<Record<string, string>>> = this._allPreferences
private readonly _fakeUser: boolean
@ -51,6 +59,7 @@ export class OsmPreferences {
this.setPreferencesAll(key, value)
}
pref.addCallback(v => {
console.log("Got an update:", key, "--->", v)
this.uploadKvSplit(key, v)
this.setPreferencesAll(key, v)
})
@ -101,11 +110,11 @@ export class OsmPreferences {
key = key.replace(/[:/"' {}.%\\]/g, "")
const localStorage = LocalStorageSource.Get(key)
const localStorage = LocalStorageSource.get(key) // cached
if (localStorage.data === "null" || localStorage.data === "undefined") {
localStorage.set(undefined)
}
const pref: UIEventSource<string> = this.initPreference(key, localStorage.data ?? defaultValue)
const pref: UIEventSource<string> = this.initPreference(key, localStorage.data ?? defaultValue) // cached
if (this.localStorageInited.has(key)) {
return pref
}

View file

@ -58,7 +58,7 @@ export class GeoLocationState {
* @private
*/
private readonly _previousLocationGrant: UIEventSource<boolean> =
LocalStorageSource.GetParsed<boolean>("geolocation-permissions", false)
LocalStorageSource.getParsed<boolean>("geolocation-permissions", false)
/**
* Used to detect a permission retraction

View file

@ -43,8 +43,8 @@ export class OptionallySyncedHistory<T> {
"sync",
)
const synced = this.synced = UIEventSource.asObject<T[]>(osmconnection.getPreference(key + "-history"), [])
const local = this.local = LocalStorageSource.GetParsed<T[]>(key + "-history", [])
const thisSession = this.thisSession = new UIEventSource<T[]>([], "optionally-synced:"+key+"(session only)")
const local = this.local = LocalStorageSource.getParsed<T[]>(key + "-history", [])
const thisSession = this.thisSession = new UIEventSource<T[]>([], "optionally-synced:" + key + "(session only)")
this.syncPreference.addCallback(syncmode => {
if (syncmode === "sync") {
let list = [...thisSession.data, ...synced.data].slice(0, maxHistory)
@ -164,7 +164,7 @@ export default class UserRelatedState {
"button" | "button_click_right" | "button_click" | "click" | "click_right"
>("button_click_right")
public readonly showScale : UIEventSource<boolean>
public readonly showScale: UIEventSource<boolean>
/**
* Preferences as tags exposes many preferences and state properties as record.
@ -202,8 +202,8 @@ export default class UserRelatedState {
this.a11y = this.osmConnection.getPreference("a11y")
this.mangroveIdentity = new MangroveIdentity(
this.osmConnection.getPreference("identity", undefined,"mangrove"),
this.osmConnection.getPreference("identity-creation-date", undefined,"mangrove"),
this.osmConnection.getPreference("identity", undefined, "mangrove"),
this.osmConnection.getPreference("identity-creation-date", undefined, "mangrove"),
)
this.preferredBackgroundLayer = this.osmConnection.getPreference("preferred-background-layer")
@ -211,7 +211,7 @@ export default class UserRelatedState {
"preferences-add-new-mode",
"button_click_right",
)
this.showScale = UIEventSource.asBoolean(this.osmConnection.GetPreference("preference-show-scale","false"))
this.showScale = UIEventSource.asBoolean(this.osmConnection.GetPreference("preference-show-scale", "false"))
this.imageLicense = this.osmConnection.getPreference("pictures-license", "CC0")
this.installedUserThemes = UserRelatedState.initInstalledUserThemes(osmConnection)
@ -272,7 +272,19 @@ export default class UserRelatedState {
}
}
public getUnofficialTheme(id: string): (MinimalLayoutInformation & { definition }) | undefined {
/**
* Adds a newly visited unofficial theme (or update the info).
*
* @param themeInfo note that themeInfo.id should be the URL where it was found
*/
public addUnofficialTheme(themeInfo: MinimalLayoutInformation) {
const pref = this.osmConnection.getPreference("unofficial-theme-" + themeInfo.id)
this.osmConnection.isLoggedIn.when(
() => pref.set(JSON.stringify(themeInfo))
)
}
public getUnofficialTheme(id: string): MinimalLayoutInformation | undefined {
const pref = this.osmConnection.getPreference("unofficial-theme-" + id)
const str = pref.data
@ -282,7 +294,7 @@ export default class UserRelatedState {
}
try {
return <MinimalLayoutInformation & { definition: string }>JSON.parse(str)
return JSON.parse(str)
} catch (e) {
console.warn(
"Removing theme " +
@ -516,10 +528,10 @@ export default class UserRelatedState {
// Language is managed separately
continue
}
if(tags[key] === null){
if (tags[key] === null) {
continue
}
let pref = this.osmConnection.GetPreference(key, undefined, {prefix: ""})
let pref = this.osmConnection.GetPreference(key, undefined, { prefix: "" })
pref.set(tags[key])
}

View file

@ -23,7 +23,7 @@ export class Stores {
}
public static FromPromiseWithErr<T>(
promise: Promise<T>
promise: Promise<T>,
): Store<{ success: T } | { error: any }> {
return UIEventSource.FromPromiseWithErr(promise)
}
@ -133,13 +133,13 @@ export abstract class Store<T> implements Readable<T> {
abstract map<J>(
f: (t: T) => J,
extraStoresToWatch: Store<any>[],
callbackDestroyFunction: (f: () => void) => void
callbackDestroyFunction: (f: () => void) => void,
): Store<J>
public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
extraStoresToWatch?: Store<any>[],
callbackDestroyFunction?: (f: () => void) => void
callbackDestroyFunction?: (f: () => void) => void,
): Store<J> {
return this.map((t) => {
if (t === undefined) {
@ -176,7 +176,7 @@ export abstract class Store<T> implements Readable<T> {
abstract addCallbackAndRun(callback: (data: T) => void): () => void
public withEqualityStabilized(
comparator: (t: T | undefined, t1: T | undefined) => boolean
comparator: (t: T | undefined, t1: T | undefined) => boolean,
): Store<T> {
let oldValue = undefined
return this.map((v) => {
@ -342,6 +342,16 @@ export abstract class Store<T> implements Readable<T> {
}
public abstract destroy()
when(callback: () => void, condition?: (v:T) => boolean) {
condition ??= v => v === true
this.addCallbackAndRunD(v => {
if ( condition(v)) {
callback()
return true
}
})
}
}
export class ImmutableStore<T> extends Store<T> {
@ -384,7 +394,7 @@ export class ImmutableStore<T> extends Store<T> {
map<J>(
f: (t: T) => J,
extraStores: Store<any>[] = undefined,
ondestroyCallback?: (f: () => void) => void
ondestroyCallback?: (f: () => void) => void,
): ImmutableStore<J> {
if (extraStores?.length > 0) {
return new MappedStore(this, f, extraStores, undefined, f(this.data), ondestroyCallback)
@ -454,7 +464,7 @@ class ListenerTracker<T> {
let endTime = new Date().getTime() / 1000
if (endTime - startTime > 500) {
console.trace(
"Warning: a ping took more then 500ms; this is probably a performance issue"
"Warning: a ping took more then 500ms; this is probably a performance issue",
)
}
if (toDelete !== undefined) {
@ -496,7 +506,7 @@ class MappedStore<TIn, T> extends Store<T> {
extraStores: Store<any>[],
upstreamListenerHandler: ListenerTracker<TIn> | undefined,
initialState: T,
onDestroy?: (f: () => void) => void
onDestroy?: (f: () => void) => void,
) {
super()
this._upstream = upstream
@ -536,7 +546,7 @@ class MappedStore<TIn, T> extends Store<T> {
map<J>(
f: (t: T) => J,
extraStores: Store<any>[] = undefined,
ondestroyCallback?: (f: () => void) => void
ondestroyCallback?: (f: () => void) => void,
): Store<J> {
let stores: Store<any>[] = undefined
if (extraStores?.length > 0 || this._extraStores?.length > 0) {
@ -558,7 +568,7 @@ class MappedStore<TIn, T> extends Store<T> {
stores,
this._callbacks,
f(this.data),
ondestroyCallback
ondestroyCallback,
)
}
@ -614,7 +624,7 @@ class MappedStore<TIn, T> extends Store<T> {
this._unregisterFromUpstream = this._upstream.addCallback((_) => self.update())
this._unregisterFromExtraStores = this._extraStores?.map((store) =>
store?.addCallback((_) => self.update())
store?.addCallback((_) => self.update()),
)
this._callbacksAreRegistered = true
}
@ -651,7 +661,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
public static flatten<X>(
source: Store<Store<X>>,
possibleSources?: Store<object>[]
possibleSources?: Store<object>[],
): UIEventSource<X> {
const sink = new UIEventSource<X>(source.data?.data)
@ -680,7 +690,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
*/
public static FromPromise<T>(
promise: Promise<T>,
onError: (e) => void = undefined
onError: (e) => void = undefined,
): UIEventSource<T> {
const src = new UIEventSource<T>(undefined)
promise?.then((d) => src.setData(d))
@ -701,7 +711,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
* @constructor
*/
public static FromPromiseWithErr<T>(
promise: Promise<T>
promise: Promise<T>,
): UIEventSource<{ success: T } | { error: any } | undefined> {
const src = new UIEventSource<{ success: T } | { error: any }>(undefined)
promise
@ -733,7 +743,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
return undefined
}
return "" + fl
}
},
)
}
@ -764,7 +774,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
return undefined
}
return "" + fl
}
},
)
}
@ -772,7 +782,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
return stringUIEventSource.sync(
(str) => str === "true",
[],
(b) => "" + b
(b) => "" + b,
)
}
@ -790,7 +800,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
}
},
[],
(b) => JSON.stringify(b) ?? ""
(b) => JSON.stringify(b) ?? "",
)
}
@ -880,7 +890,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
public map<J>(
f: (t: T) => J,
extraSources: Store<any>[] = [],
onDestroy?: (f: () => void) => void
onDestroy?: (f: () => void) => void,
): Store<J> {
return new MappedStore(this, f, extraSources, this._callbacks, f(this.data), onDestroy)
}
@ -892,7 +902,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
extraSources: Store<any>[] = [],
callbackDestroyFunction?: (f: () => void) => void
callbackDestroyFunction?: (f: () => void) => void,
): Store<J | undefined> {
return new MappedStore(
this,
@ -910,7 +920,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
this.data === undefined || this.data === null
? <undefined | null>this.data
: f(<any>this.data),
callbackDestroyFunction
callbackDestroyFunction,
)
}
@ -930,7 +940,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
f: (t: T) => J,
extraSources: Store<any>[],
g: (j: J, t: T) => T,
allowUnregister = false
allowUnregister = false,
): UIEventSource<J> {
const self = this

View file

@ -4,8 +4,11 @@ import { UIEventSource } from "../UIEventSource"
* UIEventsource-wrapper around localStorage
*/
export class LocalStorageSource {
static GetParsed<T>(key: string, defaultValue: T): UIEventSource<T> {
return LocalStorageSource.Get(key).sync(
private static readonly _cache: Record<string, UIEventSource<string>> = {}
static getParsed<T>(key: string, defaultValue: T): UIEventSource<T> {
return LocalStorageSource.get(key).sync(
(str) => {
if (str === undefined) {
return defaultValue
@ -17,34 +20,40 @@ export class LocalStorageSource {
}
},
[],
(value) => JSON.stringify(value)
(value) => JSON.stringify(value),
)
}
static Get(key: string, defaultValue: string = undefined): UIEventSource<string> {
static get(key: string, defaultValue: string = undefined): UIEventSource<string> {
const cached = LocalStorageSource._cache[key]
if (cached) {
return cached
}
let saved = defaultValue
try {
let saved = localStorage.getItem(key)
saved = localStorage.getItem(key)
if (saved === "undefined") {
saved = undefined
}
const source = new UIEventSource<string>(saved ?? defaultValue, "localstorage:" + key)
source.addCallback((data) => {
if(data === undefined || data === "" || data === null){
localStorage.removeItem(key)
return
}
try {
localStorage.setItem(key, data)
} catch (e) {
// Probably exceeded the quota with this item!
// Lets nuke everything
localStorage.clear()
}
})
return source
} catch (e) {
return new UIEventSource<string>(defaultValue)
console.error("Could not get value", key, "from local storage")
}
const source = new UIEventSource<string>(saved ?? defaultValue, "localstorage:" + key)
source.addCallback((data) => {
if (data === undefined || data === "" || data === null) {
localStorage.removeItem(key)
return
}
try {
localStorage.setItem(key, data)
} catch (e) {
// Probably exceeded the quota with this item!
// Let's nuke everything
localStorage.clear()
}
})
LocalStorageSource._cache[key] = source
return source
}
}

View file

@ -86,7 +86,7 @@ export default class FilteredLayer {
) {
let isDisplayed: UIEventSource<boolean>
if (layer.syncSelection === "local") {
isDisplayed = LocalStorageSource.GetParsed(
isDisplayed = LocalStorageSource.getParsed(
context + "-layer-" + layer.id + "-enabled",
layer.shownByDefault,
)

View file

@ -57,7 +57,7 @@ export class MenuState {
})
}
const visitedBefore = LocalStorageSource.GetParsed<boolean>(
const visitedBefore = LocalStorageSource.getParsed<boolean>(
themeid + "thememenuisopened",
false
)

View file

@ -370,7 +370,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.changes,
this.geolocation.geolocationState.currentGPSLocation,
this.indexedFeatures,
this.reportError
this.reportError,
)
this.favourites = new FavouritesFeatureSource(this)
const longAgo = new Date()
@ -532,7 +532,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
* Selects the feature that is 'i' closest to the map center
*/
private selectClosestAtCenter(i: number = 0) {
console.log("Selecting closest",i)
console.log("Selecting closest", i)
if (this.userRelatedState.a11y.data !== "never") {
this.visualFeedback.setData(true)
}
@ -908,6 +908,21 @@ export default class ThemeViewState implements SpecialVisualizationState {
* Setup various services for which no reference are needed
*/
private initActors() {
if (!this.layout.official) {
// Add custom themes to the "visited custom themes"
const th = this.layout
this.userRelatedState.addUnofficialTheme({
id: th.id,
icon: th.icon,
title: th.title.translations,
shortDescription: th.shortDescription.translations ,
layers: th.layers.filter(l => l.isNormal()).map(l => l.id)
})
}
this.selectedElement.addCallback((selected) => {
if (selected === undefined) {
this.focusOnMap()

View file

@ -55,7 +55,6 @@
const customThemes: Store<MinimalLayoutInformation[]> = Stores.ListStabilized<string>(state.installedUserThemes)
.mapD(stableIds => Utils.NoNullInplace(stableIds.map(id => state.getUnofficialTheme(id))))
function filtered(themes: Store<MinimalLayoutInformation[]>): Store<MinimalLayoutInformation[]> {
return searchStable.map(search => {
if (!search) {

View file

@ -42,8 +42,8 @@
})
}
let customWidth = LocalStorageSource.Get("custom-png-width", "20")
let customHeight = LocalStorageSource.Get("custom-png-height", "20")
let customWidth = LocalStorageSource.get("custom-png-width", "20")
let customHeight = LocalStorageSource.get("custom-png-height", "20")
async function offerCustomPng(): Promise<Blob> {
console.log(

View file

@ -24,7 +24,7 @@
export let coordinate: UIEventSource<{ lon: number; lat: number }>
export let state: SpecialVisualizationState
let comment: UIEventSource<string> = LocalStorageSource.Get("note-text")
let comment: UIEventSource<string> = LocalStorageSource.get("note-text")
let created = false
let notelayer: FilteredLayer = state.layerState.filteredLayers.get("note")

View file

@ -37,7 +37,7 @@ export abstract class EditJsonState<T> {
public readonly osmConnection: OsmConnection
public readonly showIntro: UIEventSource<"no" | "intro" | "tagrenderings"> = <any>(
LocalStorageSource.Get("studio-show-intro", "intro")
LocalStorageSource.get("studio-show-intro", "intro")
)
public readonly expertMode: UIEventSource<boolean>

View file

@ -29,7 +29,7 @@
const store = state.getStoreFor(path)
let value = store.data
let hasSeenIntro = UIEventSource.asBoolean(
LocalStorageSource.Get("studio-seen-tagrendering-tutorial", "false")
LocalStorageSource.get("studio-seen-tagrendering-tutorial", "false")
)
onMount(() => {
if (!hasSeenIntro.data) {

View file

@ -74,7 +74,7 @@ export default class Locale {
if (typeof navigator !== "undefined") {
browserLanguage = Locale.getBestSupportedLanguage()
}
source = LocalStorageSource.Get("language", browserLanguage)
source = LocalStorageSource.get("language", browserLanguage)
}
if (!Utils.runningFromConsole && typeof document !== undefined) {