diff --git a/src/Logic/Osm/OsmPreferences.ts b/src/Logic/Osm/OsmPreferences.ts index 194a174052..c320a26401 100644 --- a/src/Logic/Osm/OsmPreferences.ts +++ b/src/Logic/Osm/OsmPreferences.ts @@ -80,17 +80,10 @@ export class OsmPreferences { private async loadBulkPreferences() { const prefs = await this.getPreferencesDictDirectly() this.seenKeys = Object.keys(prefs) - const legacy = OsmPreferences.getLegacyCombinedItems(prefs) const merged = OsmPreferences.mergeDict(prefs) - if (Object.keys(legacy).length > 0) { - await this.removeLegacy(legacy) - } for (const key in merged) { this.initPreference(key, prefs[key], true) } - for (const key in legacy) { - this.initPreference(key, legacy[key], true) - } this._allPreferences.ping() } @@ -124,6 +117,7 @@ export class OsmPreferences { key, localStorage.data ?? defaultValue ) // cached + if (this.localStorageInited.has(key)) { return pref } @@ -140,15 +134,6 @@ export class OsmPreferences { this.removeAllWithPrefix("") } - public async removeLegacy(legacyDict: Record) { - for (const k in legacyDict) { - const v = legacyDict[k] - console.log("Upgrading legacy preference", k) - await this.removeAllWithPrefix(k) - this.osmConnection.getPreference(k).set(v) - } - } - /** * * OsmPreferences.mergeDict({abc: "123", def: "123", "def:0": "456", "def:1":"789"}) // => {abc: "123", def: "123456789"} @@ -160,7 +145,6 @@ export class OsmPreferences { const normalKeys = allKeys.filter((k) => !k.match(/[a-z-_0-9A-Z]*:[0-9]+/)) for (const normalKey of normalKeys) { if (normalKey.match(/-combined-[0-9]*$/) || normalKey.match(/-combined-length$/)) { - // Ignore legacy keys continue } const partKeys = OsmPreferences.keysStartingWith(allKeys, normalKey) @@ -171,42 +155,7 @@ export class OsmPreferences { } /** - * Gets all items which have a 'combined'-string, the legacy long preferences - * - * const input = { - * "extra-noncombined-key":"xyz", - * "mapcomplete-unofficial-theme-httpsrawgithubusercontentcomosm-catalanwikidataimgmainwikidataimgjson-combined-0": - * "{\"id\":\"https://raw.githubusercontent.com/osm-catalan/wikidataimg/main/wikidataimg.json\",\"icon\":\"https://upload.wikimedia.org/wikipedia/commons/5/50/Yes_Check_Circle.svg\",\"title\":{\"ca\":\"wikidataimg\",\"_context\":\"themes:wikidataimg.title\"},\"shortDescription\"", - * "mapcomplete-unofficial-theme-httpsrawgithubusercontentcomosm-catalanwikidataimgmainwikidataimgjson-combined-1": - * ":{\"ca\":\"Afegeix imatges d'articles de wikimedia\",\"_context\":\"themes:wikidataimg\"}}", - * } - * const merged = OsmPreferences.getLegacyCombinedItems(input) - * const data = merged["mapcomplete-unofficial-theme-httpsrawgithubusercontentcomosm-catalanwikidataimgmainwikidataimgjson"] - * JSON.parse(data) // => {"id": "https://raw.githubusercontent.com/osm-catalan/wikidataimg/main/wikidataimg.json", "icon": "https://upload.wikimedia.org/wikipedia/commons/5/50/Yes_Check_Circle.svg","title": { "ca": "wikidataimg", "_context": "themes:wikidataimg.title" }, "shortDescription": {"ca": "Afegeix imatges d'articles de wikimedia","_context": "themes:wikidataimg"}} - * merged["extra-noncombined-key"] // => undefined - */ - public static getLegacyCombinedItems(dict: Record): Record { - const merged: Record = {} - const keys = Object.keys(dict) - const toCheck = Utils.NoNullInplace( - Utils.Dedup(keys.map((k) => k.match(/(.*)-combined-[0-9]+$/)?.[1])) - ) - for (const key of toCheck) { - let i = 0 - let str = "" - let v: string - do { - v = dict[key + "-combined-" + i] - str += v ?? "" - i++ - } while (v !== undefined) - merged[key] = str - } - return merged - } - - /** - * Bulk-downloads all preferences + * Bulk-downloads all preferences, creates a simple record from all * @private */ private async getPreferencesDictDirectly(): Promise> { diff --git a/src/Logic/State/UserRelatedState.ts b/src/Logic/State/UserRelatedState.ts index 8c9996efca..dce95bc0b8 100644 --- a/src/Logic/State/UserRelatedState.ts +++ b/src/Logic/State/UserRelatedState.ts @@ -21,10 +21,49 @@ import Showdown from "showdown" import { LocalStorageSource } from "../Web/LocalStorageSource" import { GeocodeResult } from "../Search/GeocodingProvider" -export class OptionallySyncedHistory { +class RoundRobinStore { + private readonly _store: UIEventSource + private readonly _index: UIEventSource + private readonly _value: UIEventSource + public readonly value: Store + + constructor(store: UIEventSource, index: UIEventSource) { + this._store = store + this._index = index + this._value = new UIEventSource([]) + this.value = this._value + this._store.addCallbackD(() => this.set()) + this.set() + } + + private set() { + const v = this._store.data + const i = this._index.data + const newList = Utils.NoNull(v.slice(i + 1, v.length).concat(v.slice(0, i + 1))) + this._value.set(newList) + } + + /** + * Add a value to the underlying store + * @param t + */ + public add(t: T) { + const i = this._index.data + this._index.set((i + 1) % this._store.data.length) + this._store.data[i] = t + console.trace(">>> Setting", i, this._store.data, t, "new index:", this._index.data) + this._store.ping() + + } + + +} + +export class OptionallySyncedHistory { public readonly syncPreference: UIEventSource<"sync" | "local" | "no"> public readonly value: Store - private readonly synced: UIEventSource + private readonly syncedBackingStore: UIEventSource + private readonly syncedOrdered: RoundRobinStore private readonly local: UIEventSource private readonly thisSession: UIEventSource private readonly _maxHistory: number @@ -41,10 +80,14 @@ export class OptionallySyncedHistory { this._maxHistory = maxHistory this._isSame = isSame this.syncPreference = osmconnection.getPreference("preference-" + key + "-history", "sync") - const synced = (this.synced = UIEventSource.asObject( - osmconnection.getPreference(key + "-history"), - [] - )) + + this.syncedBackingStore = Stores.fromArray( + Utils.TimesT(maxHistory, (i) => + UIEventSource.asObject(osmconnection.getPreference(key + "-history-" + i), undefined) + )) + this.syncedOrdered = new RoundRobinStore(this.syncedBackingStore, + UIEventSource.asInt(osmconnection.getPreference(key + "-history-round-robin", "0")) + ) const local = (this.local = LocalStorageSource.getParsed(key + "-history", [])) const thisSession = (this.thisSession = new UIEventSource( [], @@ -52,7 +95,7 @@ export class OptionallySyncedHistory { )) this.syncPreference.addCallback((syncmode) => { if (syncmode === "sync") { - const list = [...thisSession.data, ...synced.data].slice(0, maxHistory) + const list = [...thisSession.data, ...this.syncedOrdered.value.data].slice(0, maxHistory) if (this._isSame) { for (let i = 0; i < list.length; i++) { for (let j = i + 1; j < list.length; j++) { @@ -62,12 +105,12 @@ export class OptionallySyncedHistory { } } } - synced.set(list) + this.syncedBackingStore.set(list) } else if (syncmode === "local") { - local.set(synced.data?.slice(0, maxHistory)) - synced.set([]) + local.set(this.syncedOrdered.value.data?.slice(0, maxHistory)) + this.syncedBackingStore.set([]) } else { - synced.set([]) + this.syncedBackingStore.set([]) local.set([]) } }) @@ -75,10 +118,10 @@ export class OptionallySyncedHistory { this.value = this.syncPreference.bind((syncPref) => this.getAppropriateStore(syncPref)) } - private getAppropriateStore(syncPref?: string) { + private getAppropriateStore(syncPref?: string): Store { syncPref ??= this.syncPreference.data if (syncPref === "sync") { - return this.synced + return this.syncedOrdered.value } if (syncPref === "local") { return this.local @@ -87,12 +130,29 @@ export class OptionallySyncedHistory { } public add(t: T) { - const store = this.getAppropriateStore() - let oldList = store.data ?? [] if (this._isSame) { - oldList = oldList.filter((x) => !this._isSame(t, x)) + const alreadyNoted = this.getAppropriateStore().data.some(item => this._isSame(item, t)) + if (alreadyNoted) { + return + } + } + + if (this.syncPreference.data === "local") { + const ls = this.local.data + ls.unshift(t) + if (ls.length >= this._maxHistory) { + ls.splice(this._maxHistory, 1) + } + this.local.ping() + } else if (this.syncPreference.data === "sync") { + this.osmconnection.isLoggedIn.addCallbackAndRun(loggedIn => { + // Wait until we are logged in and the settings are downloaded before adding the preference + if (loggedIn) { + this.syncedOrdered.add(t) + return true + } + }) } - store.set([t, ...oldList].slice(0, this._maxHistory)) } /** @@ -113,7 +173,8 @@ export class OptionallySyncedHistory { } clear() { - this.getAppropriateStore().set([]) + this.syncedBackingStore.set([]) + this.local.set([]) } } @@ -229,6 +290,7 @@ export default class UserRelatedState { 10, (a, b) => a === b ) + this.recentlyVisitedThemes.value.addCallbackAndRunD(th => console.log(">>> themes", th)) this.recentlyVisitedSearch = new OptionallySyncedHistory( "places", this.osmConnection, @@ -298,10 +360,10 @@ export default class UserRelatedState { try { return JSON.parse(str) } catch (e) { - console.warn( + e.warn( "Removing theme " + - id + - " as it could not be parsed from the preferences; the content is:", + id + + " as it could not be parsed from the preferences; the content is:", str ) pref.setData(null) @@ -331,7 +393,7 @@ export default class UserRelatedState { icon: layout.icon, title: layout.title.translations, shortDescription: layout.shortDescription.translations, - definition: layout["definition"], + definition: layout["definition"] }) ) } @@ -386,13 +448,13 @@ export default class UserRelatedState { id: "home", "user:home": "yes", _lon: homeLonLat[0], - _lat: homeLonLat[1], + _lat: homeLonLat[1] }, geometry: { type: "Point", - coordinates: homeLonLat, - }, - }, + coordinates: homeLonLat + } + } ] }) return new StaticFeatureSource(feature) @@ -414,7 +476,7 @@ export default class UserRelatedState { _applicationOpened: new Date().toISOString(), _supports_sharing: typeof window === "undefined" ? "no" : window.navigator.share ? "yes" : "no", - _iframe: Utils.isIframe ? "yes" : "no", + _iframe: Utils.isIframe ? "yes" : "no" }) if (!Utils.runningFromConsole) { amendedPrefs.data["_host"] = window.location.host @@ -462,18 +524,18 @@ export default class UserRelatedState { const zenLinks: { link: string; id: string }[] = Utils.NoNull([ hasMissingTheme ? { - id: "theme:" + layout.id, - link: LinkToWeblate.hrefToWeblateZen( - language, - "themes", - layout.id - ), - } + id: "theme:" + layout.id, + link: LinkToWeblate.hrefToWeblateZen( + language, + "themes", + layout.id + ) + } : undefined, ...missingLayers.map((id) => ({ id: "layer:" + id, - link: LinkToWeblate.hrefToWeblateZen(language, "layers", id), - })), + link: LinkToWeblate.hrefToWeblateZen(language, "layers", id) + })) ]) const untranslated_count = untranslated.length amendedPrefs.data["_translation_total"] = "" + total diff --git a/src/Logic/UIEventSource.ts b/src/Logic/UIEventSource.ts index 0626e1a7bf..965dccd17b 100644 --- a/src/Logic/UIEventSource.ts +++ b/src/Logic/UIEventSource.ts @@ -807,7 +807,7 @@ export class UIEventSource extends Store implements Writable { ) } - static asObject( + static asObject( stringUIEventSource: UIEventSource, defaultV: T ): UIEventSource { @@ -819,7 +819,7 @@ export class UIEventSource extends Store implements Writable { try { return JSON.parse(str) } catch (e) { - console.error("Could not parse value", str, "due to", e) + console.error("Could not parse value", str, "due to", e, "; the underlying data store has tag", stringUIEventSource.tag) return defaultV } },