forked from MapComplete/MapComplete
Rework preferences handling, improve search
This commit is contained in:
parent
b45cfcaa18
commit
4085bbc1ac
19 changed files with 438 additions and 534 deletions
|
|
@ -24,7 +24,7 @@ export default class FavouritesFeatureSource extends StaticFeatureSource {
|
|||
|
||||
constructor(state: SpecialVisualizationState) {
|
||||
const features: Store<Feature[]> = Stores.ListStabilized(
|
||||
state.osmConnection.preferencesHandler.preferences.map((prefs) => {
|
||||
state.osmConnection.preferencesHandler.allPreferences.map((prefs) => {
|
||||
const feats: Feature[] = []
|
||||
const allIds = new Set<string>()
|
||||
for (const key in prefs) {
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import { Feature } from "geojson"
|
||||
import { OsmConnection } from "../Osm/OsmConnection"
|
||||
import { GeocodeResult } from "./GeocodingProvider"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
|
||||
export class RecentSearch {
|
||||
|
||||
|
||||
public readonly seenThisSession: UIEventSource<GeocodeResult[]>
|
||||
|
||||
constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) {
|
||||
const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches")
|
||||
this.seenThisSession = new UIEventSource<GeocodeResult[]>([])//UIEventSource.asObject<GeoCodeResult[]>(prefs, [])
|
||||
|
||||
prefs.addCallbackAndRunD(pref => {
|
||||
if (pref === "") {
|
||||
return
|
||||
}
|
||||
try {
|
||||
|
||||
const simpleArr = <GeocodeResult[]>JSON.parse(pref)
|
||||
if (simpleArr.length > 0) {
|
||||
this.seenThisSession.set(simpleArr)
|
||||
return true
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e, pref)
|
||||
prefs.setData("")
|
||||
}
|
||||
})
|
||||
|
||||
this.seenThisSession.stabilized(2500).addCallbackAndRunD(seen => {
|
||||
const results = []
|
||||
for (let i = 0; i < Math.min(3, seen.length); i++) {
|
||||
const gc = seen[i]
|
||||
const simple = {
|
||||
category: gc.category,
|
||||
description: gc.description,
|
||||
display_name: gc.display_name,
|
||||
lat: gc.lat, lon: gc.lon,
|
||||
osm_id: gc.osm_id,
|
||||
osm_type: gc.osm_type,
|
||||
}
|
||||
results.push(simple)
|
||||
}
|
||||
prefs.setData(JSON.stringify(results))
|
||||
|
||||
})
|
||||
|
||||
state.selectedElement.addCallbackAndRunD(selected => {
|
||||
|
||||
const [osm_type, osm_id] = selected.properties.id.split("/")
|
||||
if (!osm_id) {
|
||||
return
|
||||
}
|
||||
if (["node", "way", "relation"].indexOf(osm_type) < 0) {
|
||||
return
|
||||
}
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(selected)
|
||||
const entry = <GeocodeResult>{
|
||||
feature: selected,
|
||||
display_name: selected.properties.name ?? selected.properties.alt_name ?? selected.properties.local_name,
|
||||
osm_id, osm_type,
|
||||
lon, lat,
|
||||
}
|
||||
this.addSelected(entry)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
addSelected(entry: GeocodeResult) {
|
||||
const id = entry.osm_type + entry.osm_id
|
||||
const arr = [...(this.seenThisSession.data.reverse() ?? []).slice(0, 5)]
|
||||
.filter(e => e.osm_type + e.osm_id !== id)
|
||||
|
||||
this.seenThisSession.set([entry, ...arr])
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider"
|
||||
import * as themeOverview from "../../assets/generated/theme_overview.json"
|
||||
import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
|
||||
import MoreScreen from "../../UI/BigComponents/MoreScreen"
|
||||
import { ImmutableStore, Store } from "../UIEventSource"
|
||||
import UserRelatedState from "../State/UserRelatedState"
|
||||
|
||||
export default class ThemeSearch implements GeocodingProvider {
|
||||
|
||||
private static allThemes: MinimalLayoutInformation[] = (themeOverview["default"] ?? themeOverview)
|
||||
private readonly _state: SpecialVisualizationState
|
||||
private readonly _knownHiddenThemes: Store<Set<string>>
|
||||
private readonly _suggestionLimit: number
|
||||
|
|
@ -18,7 +17,7 @@ export default class ThemeSearch implements GeocodingProvider {
|
|||
this._state = state
|
||||
this._layersToIgnore = state.layout.layers.map(l => l.id)
|
||||
this._suggestionLimit = suggestionLimit
|
||||
this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection)
|
||||
this._knownHiddenThemes = UserRelatedState.initDiscoveredHiddenThemes(this._state.osmConnection).map(list => new Set(list))
|
||||
this._otherThemes = MoreScreen.officialThemes.themes
|
||||
.filter(th => th.id !== state.layout.id)
|
||||
}
|
||||
|
|
@ -45,7 +44,6 @@ export default class ThemeSearch implements GeocodingProvider {
|
|||
return []
|
||||
}
|
||||
const sorted = MoreScreen.sortedByLowest(query, this._otherThemes, this._layersToIgnore)
|
||||
console.log(">>>", sorted)
|
||||
return sorted
|
||||
.filter(sorted => sorted.lowest < 2)
|
||||
.map(th => th.theme)
|
||||
|
|
|
|||
|
|
@ -137,7 +137,6 @@ export class OsmConnection {
|
|||
this.preferencesHandler = new OsmPreferences(this.auth, this, this.fakeUser)
|
||||
|
||||
if (options.oauth_token?.data !== undefined) {
|
||||
console.log(options.oauth_token.data)
|
||||
this.auth.bootstrapToken(options.oauth_token.data, (err, result) => {
|
||||
console.log("Bootstrap token called back", err, result)
|
||||
this.AttemptLogin()
|
||||
|
|
@ -160,10 +159,10 @@ export class OsmConnection {
|
|||
key: string,
|
||||
defaultValue: string = undefined,
|
||||
options?: {
|
||||
documentation?: string
|
||||
prefix?: string
|
||||
}
|
||||
): UIEventSource<T | undefined> {
|
||||
options ??= {prefix: "mapcomplete-"}
|
||||
return <UIEventSource<T>>this.preferencesHandler.GetPreference(key, defaultValue, options)
|
||||
}
|
||||
|
||||
|
|
@ -183,7 +182,6 @@ export class OsmConnection {
|
|||
this.userDetails.ping()
|
||||
console.log("Logged out")
|
||||
this.loadingStatus.setData("not-attempted")
|
||||
this.preferencesHandler.preferences.setData(undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,151 +1,109 @@
|
|||
import { UIEventSource } from "../UIEventSource"
|
||||
import UserDetails, { OsmConnection } from "./OsmConnection"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import { OsmConnection } from "./OsmConnection"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import OSMAuthInstance = OSMAuth.osmAuth
|
||||
|
||||
export class OsmPreferences {
|
||||
/**
|
||||
* A dictionary containing all the preferences. The 'preferenceSources' will be initialized from this
|
||||
* We keep a local copy of them, to init mapcomplete with the previous choices and to be able to get the open changesets right away
|
||||
*/
|
||||
public preferences = LocalStorageSource.GetParsed<Record<string, string>>(
|
||||
"all-osm-preferences",
|
||||
{}
|
||||
)
|
||||
/**
|
||||
* A map containing the individual preference sources
|
||||
* @private
|
||||
*/
|
||||
private readonly preferenceSources = new Map<string, UIEventSource<string>>()
|
||||
private readonly auth: OSMAuthInstance
|
||||
private userDetails: UIEventSource<UserDetails>
|
||||
private longPreferences = {}
|
||||
|
||||
private normalPreferences: Record<string, UIEventSource<string>> = {}
|
||||
private longPreferences: Record<string, UIEventSource<string>> = {}
|
||||
private localStorageInited: Set<string> = new Set()
|
||||
|
||||
private readonly _allPreferences: UIEventSource<Record<string, string>> = new UIEventSource({})
|
||||
public readonly allPreferences: Store<Readonly<Record<string, string>>> = this._allPreferences
|
||||
private readonly _fakeUser: boolean
|
||||
private readonly auth: OSMAuthInstance
|
||||
private readonly osmConnection: OsmConnection
|
||||
|
||||
constructor(auth: OSMAuthInstance, osmConnection: OsmConnection, fakeUser: boolean = false) {
|
||||
this.auth = auth
|
||||
this._fakeUser = fakeUser
|
||||
this.userDetails = osmConnection.userDetails
|
||||
this.osmConnection = osmConnection
|
||||
osmConnection.OnLoggedIn(() => {
|
||||
this.UpdatePreferences(true)
|
||||
this.loadBulkPreferences()
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
private getLongValue(allPrefs: Record<string, string>, key: string): string {
|
||||
const count = Number(allPrefs[key + "-length"])
|
||||
let str = ""
|
||||
for (let i = 0; i < count; i++) {
|
||||
str += allPrefs[key + i]
|
||||
}
|
||||
return str
|
||||
|
||||
}
|
||||
|
||||
private setPreferencesAll(key: string, value: string) {
|
||||
if (this._allPreferences.data[key] !== value) {
|
||||
this._allPreferences.data[key] = value
|
||||
this._allPreferences.ping()
|
||||
}
|
||||
}
|
||||
|
||||
private initPreference(key: string, value: string = "", excludeFromAll: boolean = false): UIEventSource<string> {
|
||||
if (this.normalPreferences[key] !== undefined) {
|
||||
return this.normalPreferences[key]
|
||||
}
|
||||
const pref = this.normalPreferences[key] = new UIEventSource(value, "preference: " + key)
|
||||
if(value && !excludeFromAll){
|
||||
this.setPreferencesAll(key, value)
|
||||
}
|
||||
pref.addCallback(v => {
|
||||
this.UploadPreference(key, v)
|
||||
if(!excludeFromAll){
|
||||
this.setPreferencesAll(key, v)
|
||||
}
|
||||
})
|
||||
return pref
|
||||
}
|
||||
|
||||
private initLongPreference(key: string, initialValue: string): UIEventSource<string> {
|
||||
if (this.longPreferences[key] !== undefined) {
|
||||
return this.longPreferences[key]
|
||||
}
|
||||
const pref = this.longPreferences[key] = new UIEventSource<string>(initialValue, "long-preference-"+key)
|
||||
const maxLength = 255
|
||||
const length = UIEventSource.asInt(this.initPreference(key + "-length", "0", true))
|
||||
if(initialValue){
|
||||
this.setPreferencesAll(key, initialValue)
|
||||
}
|
||||
pref.addCallback(v => {
|
||||
length.set(Math.ceil(v.length / maxLength))
|
||||
let i = 0
|
||||
while (v.length > 0) {
|
||||
this.UploadPreference(key + "-" + i, v.substring(0, maxLength))
|
||||
i++
|
||||
v = v.substring(maxLength)
|
||||
}
|
||||
this.setPreferencesAll(key, v)
|
||||
})
|
||||
return pref
|
||||
}
|
||||
|
||||
private async loadBulkPreferences() {
|
||||
const prefs = await this.getPreferencesDict()
|
||||
const isCombined = /-combined-/
|
||||
for (const key in prefs) {
|
||||
if (key.endsWith("-combined-length")) {
|
||||
const v = this.getLongValue(prefs, key.substring(0, key.length - "-length".length))
|
||||
this.initLongPreference(key, v)
|
||||
}
|
||||
if (key.match(isCombined)) {
|
||||
continue
|
||||
}
|
||||
this.initPreference(key, prefs[key])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OSM preferences can be at most 255 chars
|
||||
* @param key
|
||||
* @param prefix
|
||||
* @constructor
|
||||
* OSM preferences can be at most 255 chars.
|
||||
* This method chains multiple together.
|
||||
* Values written into this key will be erased when the user logs in
|
||||
*/
|
||||
public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
|
||||
if (this.longPreferences[prefix + key] !== undefined) {
|
||||
return this.longPreferences[prefix + key]
|
||||
}
|
||||
|
||||
const source = new UIEventSource<string>(undefined, "long-osm-preference:" + prefix + key)
|
||||
this.longPreferences[prefix + key] = source
|
||||
|
||||
const allStartWith = prefix + key + "-combined"
|
||||
const subOptions = { prefix: "" }
|
||||
// Gives the number of combined preferences
|
||||
const length = this.GetPreference(allStartWith + "-length", "", subOptions)
|
||||
const preferences = this.preferences
|
||||
if ((allStartWith + "-length").length > 255) {
|
||||
throw (
|
||||
"This preference key is too long, it has " +
|
||||
key.length +
|
||||
" characters, but at most " +
|
||||
(255 - "-length".length - "-combined".length - prefix.length) +
|
||||
" characters are allowed"
|
||||
)
|
||||
}
|
||||
|
||||
source.addCallback((str) => {
|
||||
if (str === undefined || str === "") {
|
||||
return
|
||||
}
|
||||
if (str === null) {
|
||||
console.error("Deleting " + allStartWith)
|
||||
const count = parseInt(length.data)
|
||||
for (let i = 0; i < count; i++) {
|
||||
// Delete all the preferences
|
||||
this.GetPreference(allStartWith + "-" + i, "", subOptions).setData("")
|
||||
}
|
||||
this.GetPreference(allStartWith + "-length", "", subOptions).setData("")
|
||||
return
|
||||
}
|
||||
|
||||
let i = 0
|
||||
while (str !== "") {
|
||||
if (str === undefined || str === "undefined") {
|
||||
source.setData(undefined)
|
||||
throw (
|
||||
"Got 'undefined' or a literal string containing 'undefined' for a long preference with name " +
|
||||
key
|
||||
)
|
||||
}
|
||||
if (str === "undefined") {
|
||||
source.setData(undefined)
|
||||
throw (
|
||||
"Got a literal string containing 'undefined' for a long preference with name " +
|
||||
key
|
||||
)
|
||||
}
|
||||
if (i > 100) {
|
||||
throw "This long preference is getting very long... "
|
||||
}
|
||||
this.GetPreference(allStartWith + "-" + i, "", subOptions).setData(
|
||||
str.substr(0, 255)
|
||||
)
|
||||
str = str.substr(255)
|
||||
i++
|
||||
}
|
||||
length.setData("" + i) // We use I, the number of preference fields used
|
||||
})
|
||||
|
||||
|
||||
function updateData(l: number) {
|
||||
if (Object.keys(preferences.data).length === 0) {
|
||||
// The preferences are still empty - they are not yet updated, so we delay updating for now
|
||||
return
|
||||
}
|
||||
const prefsCount = Number(l)
|
||||
if (prefsCount > 100) {
|
||||
throw "Length to long"
|
||||
}
|
||||
let str = ""
|
||||
for (let i = 0; i < prefsCount; i++) {
|
||||
const key = allStartWith + "-" + i
|
||||
if (preferences.data[key] === undefined) {
|
||||
console.warn(
|
||||
"Detected a broken combined preference:",
|
||||
key,
|
||||
"is undefined",
|
||||
preferences
|
||||
)
|
||||
continue
|
||||
}
|
||||
const v = preferences.data[key]
|
||||
if(v === "undefined"){
|
||||
delete preferences.data[key]
|
||||
continue
|
||||
}
|
||||
str += preferences.data[key] ?? ""
|
||||
}
|
||||
|
||||
source.setData(str)
|
||||
}
|
||||
|
||||
length.addCallback((l) => {
|
||||
updateData(Number(l))
|
||||
})
|
||||
this.preferences.addCallbackAndRun(() => {
|
||||
updateData(Number(length.data))
|
||||
})
|
||||
|
||||
return source
|
||||
return this.getPreferenceSeedFromlocal(key, true, undefined, { prefix })
|
||||
}
|
||||
|
||||
public GetPreference(
|
||||
|
|
@ -154,145 +112,108 @@ export class OsmPreferences {
|
|||
options?: {
|
||||
documentation?: string
|
||||
prefix?: string
|
||||
}
|
||||
},
|
||||
) {
|
||||
return this.getPreferenceSeedFromlocal(key, false, defaultValue, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a OSM-preference.
|
||||
* The OSM-preference is cached in local storage and updated from the OSM.org as soon as those values come in.
|
||||
* THis means that values written before being logged in might be erased by the cloud settings
|
||||
* @param key
|
||||
* @param defaultValue
|
||||
* @param options
|
||||
* @constructor
|
||||
*/
|
||||
private getPreferenceSeedFromlocal(
|
||||
key: string,
|
||||
long: boolean,
|
||||
defaultValue: string = undefined,
|
||||
options?: {
|
||||
prefix?: string,
|
||||
saveToLocalStorage?: true | boolean
|
||||
},
|
||||
): UIEventSource<string> {
|
||||
const prefix: string = options?.prefix ?? "mapcomplete-"
|
||||
if (key.startsWith(prefix) && prefix !== "") {
|
||||
console.trace(
|
||||
"A preference was requested which has a duplicate prefix in its key. This is probably a bug"
|
||||
)
|
||||
if (options?.prefix) {
|
||||
key = options.prefix + key
|
||||
}
|
||||
key = prefix + key
|
||||
key = key.replace(/[:/"' {}.%\\]/g, "")
|
||||
if (key.length >= 255) {
|
||||
throw "Preferences: key length to big"
|
||||
|
||||
|
||||
let pref : UIEventSource<string>
|
||||
const localStorage = LocalStorageSource.Get(key)
|
||||
if(localStorage.data === "null" || localStorage.data === "undefined"){
|
||||
localStorage.set(undefined)
|
||||
}
|
||||
const cached = this.preferenceSources.get(key)
|
||||
if (cached !== undefined) {
|
||||
return cached
|
||||
}
|
||||
if (this.userDetails.data.loggedIn && this.preferences.data[key] === undefined) {
|
||||
this.UpdatePreferences()
|
||||
if(long){
|
||||
pref = this.initLongPreference(key, localStorage.data ?? defaultValue)
|
||||
}else{
|
||||
pref = this.initPreference(key, localStorage.data ?? defaultValue)
|
||||
}
|
||||
|
||||
const pref = new UIEventSource<string>(
|
||||
this.preferences.data[key] ?? defaultValue,
|
||||
"osm-preference:" + key
|
||||
)
|
||||
pref.addCallback((v) => {
|
||||
this.UploadPreference(key, v)
|
||||
})
|
||||
if (this.localStorageInited.has(key)) {
|
||||
return pref
|
||||
}
|
||||
|
||||
this.preferences.addCallbackD((allPrefs) => {
|
||||
const v = allPrefs[key]
|
||||
if (v === undefined) {
|
||||
return
|
||||
}
|
||||
pref.setData(v)
|
||||
})
|
||||
|
||||
this.preferenceSources.set(key, pref)
|
||||
if (options?.saveToLocalStorage ?? true) {
|
||||
pref.addCallback(v => localStorage.set(v)) // Keep a local copy
|
||||
}
|
||||
this.localStorageInited.add(key)
|
||||
return pref
|
||||
}
|
||||
|
||||
public ClearPreferences() {
|
||||
let isRunning = false
|
||||
this.preferences.addCallback((prefs) => {
|
||||
console.log("Cleaning preferences...")
|
||||
if (Object.keys(prefs).length == 0) {
|
||||
return
|
||||
}
|
||||
if (isRunning) {
|
||||
return
|
||||
}
|
||||
isRunning = true
|
||||
const prefixes = ["mapcomplete-"]
|
||||
for (const key in prefs) {
|
||||
const matches = prefixes.some((prefix) => key.startsWith(prefix))
|
||||
if (matches) {
|
||||
console.log("Clearing ", key)
|
||||
this.GetPreference(key, "", { prefix: "" }).setData("")
|
||||
}
|
||||
}
|
||||
isRunning = false
|
||||
return
|
||||
console.log("Starting to remove all preferences")
|
||||
this.removeAllWithPrefix("")
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Bulk-downloads all preferences
|
||||
* @private
|
||||
*/
|
||||
private getPreferencesDict(): Promise<Record<string, string>> {
|
||||
return new Promise<Record<string, string>>((resolve, reject) => {
|
||||
this.auth.xhr(
|
||||
{
|
||||
method: "GET",
|
||||
path: "/api/0.6/user/preferences",
|
||||
},
|
||||
(error, value: XMLDocument) => {
|
||||
if (error) {
|
||||
console.log("Could not load preferences", error)
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
const prefs = value.getElementsByTagName("preference")
|
||||
const dict: Record<string, string> = {}
|
||||
for (let i = 0; i < prefs.length; i++) {
|
||||
const pref = prefs[i]
|
||||
const k = pref.getAttribute("k")
|
||||
dict[k] = pref.getAttribute("v")
|
||||
}
|
||||
resolve(dict)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
removeAllWithPrefix(prefix: string) {
|
||||
for (const key in this.preferences.data) {
|
||||
if (key.startsWith(prefix)) {
|
||||
this.GetPreference(key, "", { prefix: "" }).setData(undefined)
|
||||
console.log("Clearing preference", key)
|
||||
}
|
||||
}
|
||||
this.preferences.ping()
|
||||
}
|
||||
|
||||
private UpdatePreferences(forceUpdate?: boolean) {
|
||||
if (this._fakeUser) {
|
||||
return
|
||||
}
|
||||
this.auth.xhr(
|
||||
{
|
||||
method: "GET",
|
||||
path: "/api/0.6/user/preferences",
|
||||
},
|
||||
(error, value: XMLDocument) => {
|
||||
if (error) {
|
||||
console.log("Could not load preferences", error)
|
||||
return
|
||||
}
|
||||
const prefs = value.getElementsByTagName("preference")
|
||||
const seenKeys = new Set<string>()
|
||||
for (let i = 0; i < prefs.length; i++) {
|
||||
const pref = prefs[i]
|
||||
const k = pref.getAttribute("k")
|
||||
this.preferences.data[k] = pref.getAttribute("v")
|
||||
seenKeys.add(k)
|
||||
}
|
||||
if (forceUpdate) {
|
||||
for (const key in this.preferences.data) {
|
||||
if (seenKeys.has(key)) {
|
||||
continue
|
||||
}
|
||||
console.log("Deleting key", key, "as we didn't find it upstream")
|
||||
delete this.preferences.data[key]
|
||||
}
|
||||
}
|
||||
|
||||
// We merge all the preferences: new keys are uploaded
|
||||
// For differing values, the server overrides local changes
|
||||
this.preferenceSources.forEach((preference, key) => {
|
||||
const osmValue = this.preferences.data[key]
|
||||
if (osmValue === undefined && preference.data !== undefined) {
|
||||
// OSM doesn't know this value yet
|
||||
this.UploadPreference(key, preference.data)
|
||||
} else {
|
||||
// OSM does have a value - set it
|
||||
preference.setData(osmValue)
|
||||
}
|
||||
})
|
||||
|
||||
this.preferences.ping()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* UPloads the given k=v to the OSM-server
|
||||
* Deletes it if 'v' is undefined, null or empty
|
||||
*/
|
||||
private UploadPreference(k: string, v: string) {
|
||||
if (!this.userDetails.data.loggedIn) {
|
||||
if (!this.osmConnection.userDetails.data.loggedIn) {
|
||||
console.debug(`Not saving preference ${k}: user not logged in`)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.preferences.data[k] === v) {
|
||||
return
|
||||
}
|
||||
console.debug("Updating preference", k, " to ", Utils.EllipsesAfter(v, 15))
|
||||
if (this._fakeUser) {
|
||||
return
|
||||
}
|
||||
if (v === undefined || v === "") {
|
||||
if (v === undefined || v === "" || v === null) {
|
||||
this.auth.xhr(
|
||||
{
|
||||
method: "DELETE",
|
||||
|
|
@ -304,10 +225,8 @@ export class OsmPreferences {
|
|||
console.warn("Could not remove preference", error)
|
||||
return
|
||||
}
|
||||
delete this.preferences.data[k]
|
||||
this.preferences.ping()
|
||||
console.debug("Preference ", k, "removed!")
|
||||
}
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
|
@ -319,15 +238,38 @@ export class OsmPreferences {
|
|||
headers: { "Content-Type": "text/plain" },
|
||||
content: v,
|
||||
},
|
||||
(error)=> {
|
||||
(error) => {
|
||||
if (error) {
|
||||
console.warn(`Could not set preference "${k}"'`, error)
|
||||
return
|
||||
}
|
||||
this.preferences.data[k] = v
|
||||
this.preferences.ping()
|
||||
console.debug(`Preference ${k} written!`)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
removeAllWithPrefix(prefix: string) {
|
||||
for (const key in this.normalPreferences) {
|
||||
if(key.startsWith(prefix)){
|
||||
this.normalPreferences[key].set(null)
|
||||
}
|
||||
}
|
||||
for (const key in this.longPreferences) {
|
||||
if(key.startsWith(prefix)){
|
||||
this.longPreferences[key].set(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getExistingPreference(key: string, defaultValue: undefined, prefix: string ): UIEventSource<string> {
|
||||
if (prefix) {
|
||||
key = prefix + key
|
||||
}
|
||||
key = key.replace(/[:/"' {}.%\\]/g, "")
|
||||
|
||||
if(this.normalPreferences[key]){
|
||||
return this.normalPreferences[key]
|
||||
}
|
||||
return this.longPreferences[key]
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import GeocodingProvider, {
|
|||
GeocodingUtils,
|
||||
type SearchResult
|
||||
} from "../Geocoding/GeocodingProvider"
|
||||
import { RecentSearch } from "../Geocoding/RecentSearch"
|
||||
import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource"
|
||||
import CombinedSearcher from "../Geocoding/CombinedSearcher"
|
||||
import FilterSearch from "../Geocoding/FilterSearch"
|
||||
|
|
@ -25,7 +24,6 @@ import ShowDataLayer from "../../UI/Map/ShowDataLayer"
|
|||
export default class SearchState {
|
||||
|
||||
public readonly isSearching = new UIEventSource(false)
|
||||
public readonly recentlySearched: RecentSearch
|
||||
public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
|
||||
public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("")
|
||||
public readonly searchIsFocused = new UIEventSource(false)
|
||||
|
|
@ -48,7 +46,6 @@ export default class SearchState {
|
|||
new PhotonSearch() // new NominatimGeocoding(),
|
||||
]
|
||||
|
||||
this.recentlySearched = new RecentSearch(state)
|
||||
const bounds = state.mapProperties.bounds
|
||||
const suggestionsList = this.searchTerm.stabilized(250).mapD(search => {
|
||||
if (search.length === 0) {
|
||||
|
|
|
|||
|
|
@ -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(/>/g, ">")
|
||||
?.replace(/</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])
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue