Rework preferences handling, improve search

This commit is contained in:
Pieter Vander Vennet 2024-09-10 02:19:55 +02:00
parent b45cfcaa18
commit 4085bbc1ac
19 changed files with 438 additions and 534 deletions

View file

@ -19,6 +19,104 @@ import { QueryParameters } from "../Web/QueryParameters"
import { ThemeMetaTagging } from "./UserSettingsMetaTagging"
import { MapProperties } from "../../Models/MapProperties"
import Showdown from "showdown"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import { GeocodeResult } from "../Geocoding/GeocodingProvider"
export class OptionallySyncedHistory<T> {
public readonly syncPreference: UIEventSource<"sync" | "local" | "no">
public readonly value: Store<T[]>
private readonly synced: UIEventSource<T[]>
private readonly local: UIEventSource<T[]>
private readonly thisSession: UIEventSource<T[]>
private readonly _maxHistory: number
private readonly _isSame: (a: T, b: T) => boolean
private osmconnection: OsmConnection
constructor(key: string, osmconnection: OsmConnection, maxHistory: number = 20, isSame?: (a: T, b: T) => boolean) {
this.osmconnection = osmconnection
this._maxHistory = maxHistory
this._isSame = isSame
this.syncPreference = osmconnection.GetPreference(
"preference-" + key + "-history",
"sync",
)
console.log(">>>",key, this.syncPreference)
const synced = this.synced = UIEventSource.asObject<T[]>(osmconnection.GetLongPreference(key + "-history"), [])
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)
if (this._isSame) {
for (let i = 0; i < list.length; i++) {
for (let j = i + 1; j < list.length; j++) {
if (this._isSame(list[i], list[j])) {
list.splice(j, 1)
}
}
}
}
synced.set(list)
} else if (syncmode === "local") {
local.set(synced.data?.slice(0, maxHistory))
synced.set([])
} else {
synced.set([])
local.set([])
}
})
this.value = this.syncPreference.bind(syncPref => this.getAppropriateStore(syncPref))
}
private getAppropriateStore(syncPref?: string) {
syncPref ??= this.syncPreference.data
if (syncPref === "sync") {
return this.synced
}
if (syncPref === "local") {
return this.local
}
return this.thisSession
}
public add(t: T) {
const store = this.getAppropriateStore()
let oldList = store.data ?? []
if (this._isSame) {
oldList = oldList.filter(x => !this._isSame(t, x))
}
console.log("Setting new history:", store, [t, ...oldList])
store.set([t, ...oldList].slice(0, this._maxHistory))
}
/**
* Adds the value when the user is actually logged in
* @param t
*/
public addDefferred(t: T) {
if (t === undefined) {
return
}
this.osmconnection.isLoggedIn.addCallbackAndRun(loggedIn => {
if (!loggedIn) {
return
}
this.add(t)
return true
})
}
clear() {
this.getAppropriateStore().set([])
}
}
/**
* The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,
@ -62,7 +160,7 @@ export default class UserRelatedState {
*/
public readonly gpsLocationHistoryRetentionTime = new UIEventSource(
7 * 24 * 60 * 60,
"gps_location_retention"
"gps_location_retention",
)
public readonly addNewFeatureMode = new UIEventSource<
@ -80,87 +178,62 @@ export default class UserRelatedState {
public readonly preferencesAsTags: UIEventSource<Record<string, string>>
private readonly _mapProperties: MapProperties
public readonly recentlyVisitedThemes: UIEventSource<string[]>
public readonly recentlyVisitedThemes: OptionallySyncedHistory<string>
public readonly recentlyVisitedSearch: OptionallySyncedHistory<GeocodeResult>
constructor(
osmConnection: OsmConnection,
layout?: LayoutConfig,
featureSwitches?: FeatureSwitchState,
mapProperties?: MapProperties
mapProperties?: MapProperties,
) {
this.osmConnection = osmConnection
this._mapProperties = mapProperties
this.showAllQuestionsAtOnce = UIEventSource.asBoolean(
this.osmConnection.GetPreference("show-all-questions", "false", {
documentation:
"Either 'true' or 'false'. If set, all questions will be shown all at once",
})
this.osmConnection.GetPreference("show-all-questions", "false"),
)
this.language = this.osmConnection.GetPreference("language")
this.showTags = this.osmConnection.GetPreference("show_tags")
this.showCrosshair = this.osmConnection.GetPreference("show_crosshair")
this.fixateNorth = this.osmConnection.GetPreference("fixate-north")
console.log("Fixate north is:", this.fixateNorth)
this.morePrivacy = this.osmConnection.GetPreference("more_privacy", "no")
this.a11y = this.osmConnection.GetPreference("a11y")
this.mangroveIdentity = new MangroveIdentity(
this.osmConnection.GetLongPreference("identity", "mangrove"),
this.osmConnection.GetPreference("identity-creation-date", "mangrove")
)
this.preferredBackgroundLayer = this.osmConnection.GetPreference(
"preferred-background-layer",
undefined,
{
documentation:
"The ID of a layer or layer category that MapComplete uses by default",
}
this.osmConnection.GetPreference("identity-creation-date", "mangrove"),
)
this.preferredBackgroundLayer = this.osmConnection.GetPreference("preferred-background-layer")
this.addNewFeatureMode = this.osmConnection.GetPreference(
"preferences-add-new-mode",
"button_click_right",
{
documentation: "How adding a new feature is done",
}
)
this.imageLicense = this.osmConnection.GetPreference("pictures-license", "CC0", {
documentation: "The license under which new images are uploaded",
})
this.installedUserThemes = this.InitInstalledUserThemes()
this.imageLicense = this.osmConnection.GetPreference("pictures-license", "CC0")
this.installedUserThemes = UserRelatedState.initInstalledUserThemes(osmConnection)
this.translationMode = this.initTranslationMode()
this.homeLocation = this.initHomeLocation()
this.preferencesAsTags = this.initAmendedPrefs(layout, featureSwitches)
const prefs = this.osmConnection
this.recentlyVisitedThemes = UIEventSource.asObject(prefs.GetLongPreference("recently-visited-themes"), [])
if (layout) {
const osmConn = this.osmConnection
const recentlyVisited = this.recentlyVisitedThemes
function update() {
if (!osmConn.isLoggedIn.data) {
return
}
const previously = recentlyVisited.data
if (previously[0] === layout.id) {
return true
}
const newThemes = Utils.Dedup([layout.id, ...previously]).slice(0, 30)
recentlyVisited.set(newThemes)
return true
}
this.recentlyVisitedThemes.addCallbackAndRun(() => update())
this.osmConnection.isLoggedIn.addCallbackAndRun(() => update())
}
this.recentlyVisitedThemes = new OptionallySyncedHistory<string>(
"theme",
this.osmConnection,
10,
(a, b) => a === b,
)
this.recentlyVisitedSearch = new OptionallySyncedHistory<GeocodeResult>("places",
this.osmConnection,
15,
(a, b) => a.osm_id === b.osm_id && a.osm_type === b.osm_type,
)
this.syncLanguage()
this.recentlyVisitedThemes.addDefferred(layout?.id)
}
private syncLanguage() {
@ -214,9 +287,9 @@ export default class UserRelatedState {
} catch (e) {
console.warn(
"Removing theme " +
id +
" as it could not be parsed from the preferences; the content is:",
str
id +
" as it could not be parsed from the preferences; the content is:",
str,
)
pref.setData(null)
return undefined
@ -246,18 +319,31 @@ export default class UserRelatedState {
title: layout.title.translations,
shortDescription: layout.shortDescription.translations,
definition: layout["definition"],
})
}),
)
}
}
private InitInstalledUserThemes(): Store<string[]> {
public static initInstalledUserThemes(osmConnection: OsmConnection): Store<string[]> {
const prefix = "mapcomplete-unofficial-theme-"
const postfix = "-combined-length"
return this.osmConnection.preferencesHandler.preferences.map((prefs) =>
return osmConnection.preferencesHandler.allPreferences.map((prefs) =>
Object.keys(prefs)
.filter((k) => k.startsWith(prefix) && k.endsWith(postfix))
.map((k) => k.substring(prefix.length, k.length - postfix.length))
.filter((k) => k.startsWith(prefix))
.map((k) => k.substring(prefix.length)),
)
}
/**
* List of all hidden themes that have been seen before
* @param osmConnection
*/
public static initDiscoveredHiddenThemes(osmConnection: OsmConnection): Store<string[]> {
const prefix = "mapcomplete-hidden-theme-"
const userPreferences = osmConnection.preferencesHandler.allPreferences
return userPreferences.map((preferences) =>
Object.keys(preferences)
.filter((key) => key.startsWith(prefix))
.map((key) => key.substring(prefix.length, key.length - "-enabled".length)),
)
}
@ -273,7 +359,7 @@ export default class UserRelatedState {
return undefined
}
return [home.lon, home.lat]
})
}),
).map((homeLonLat) => {
if (homeLonLat === undefined) {
return empty
@ -303,7 +389,7 @@ export default class UserRelatedState {
* */
private initAmendedPrefs(
layout?: LayoutConfig,
featureSwitches?: FeatureSwitchState
featureSwitches?: FeatureSwitchState,
): UIEventSource<Record<string, string>> {
const amendedPrefs = new UIEventSource<Record<string, string>>({
_theme: layout?.id,
@ -324,23 +410,13 @@ export default class UserRelatedState {
}
const osmConnection = this.osmConnection
osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => {
osmConnection.preferencesHandler.allPreferences.addCallback((newPrefs) => {
for (const k in newPrefs) {
const v = newPrefs[k]
if (v === "undefined" || !v) {
if (v === "undefined" || v === "null" || !v) {
continue
}
if (k.endsWith("-combined-length")) {
const l = Number(v)
const key = k.substring(0, k.length - "length".length)
let combined = ""
for (let i = 0; i < l; i++) {
combined += (newPrefs[key + i])
}
amendedPrefs.data[key.substring(0, key.length - "-combined-".length)] = combined
} else {
amendedPrefs.data[k] = newPrefs[k]
}
amendedPrefs.data[k] = newPrefs[k] ?? ""
}
amendedPrefs.ping()
@ -359,19 +435,19 @@ export default class UserRelatedState {
const missingLayers = Utils.Dedup(
untranslated
.filter((k) => k.startsWith("layers:"))
.map((k) => k.slice("layers:".length).split(".")[0])
.map((k) => k.slice("layers:".length).split(".")[0]),
)
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,
@ -388,7 +464,7 @@ export default class UserRelatedState {
}
amendedPrefs.ping()
},
[this.translationMode]
[this.translationMode],
)
this.mangroveIdentity.getKeyId().addCallbackAndRun((kid) => {
@ -407,7 +483,7 @@ export default class UserRelatedState {
.makeHtml(userDetails.description)
?.replace(/&gt;/g, ">")
?.replace(/&lt;/g, "<")
?.replace(/\n/g, "")
?.replace(/\n/g, ""),
)
}
@ -418,7 +494,7 @@ export default class UserRelatedState {
(c: { contributor: string; commits: number }) => {
const replaced = c.contributor.toLowerCase().replace(/\s+/g, "")
return replaced === simplifiedName
}
},
)
if (isTranslator) {
amendedPrefs.data["_translation_contributions"] = "" + isTranslator.commits
@ -427,7 +503,7 @@ export default class UserRelatedState {
(c: { contributor: string; commits: number }) => {
const replaced = c.contributor.toLowerCase().replace(/\s+/g, "")
return replaced === simplifiedName
}
},
)
if (isCodeContributor) {
amendedPrefs.data["_code_contributions"] = "" + isCodeContributor.commits
@ -441,18 +517,14 @@ export default class UserRelatedState {
// Language is managed separately
continue
}
if (tags[key + "-combined-0"]) {
// A combined value exists
if (tags[key].startsWith("undefined")) {
// Sometimes, a long string of 'undefined' will show up, we ignore them
continue
}
this.osmConnection.GetLongPreference(key, "").setData(tags[key])
} else {
this.osmConnection
.GetPreference(key, undefined, { prefix: "" })
.setData(tags[key])
if(tags[key] === null){
continue
}
let pref = this.osmConnection.preferencesHandler.getExistingPreference(key, undefined, "")
if (!pref) {
pref = this.osmConnection.GetPreference(key, undefined, {prefix: ""})
}
pref.set(tags[key])
}
})