forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			394 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			394 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
 | |
| import {OsmConnection} from "../Osm/OsmConnection"
 | |
| import {MangroveIdentity} from "../Web/MangroveReviews"
 | |
| import {Store, Stores, UIEventSource} from "../UIEventSource"
 | |
| import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
 | |
| import {FeatureSource} from "../FeatureSource/FeatureSource"
 | |
| import {Feature} from "geojson"
 | |
| import {Utils} from "../../Utils"
 | |
| import translators from "../../assets/translators.json"
 | |
| import codeContributors from "../../assets/contributors.json"
 | |
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
 | |
| import {LayerConfigJson} from "../../Models/ThemeConfig/Json/LayerConfigJson"
 | |
| import usersettings from "../../assets/generated/layers/usersettings.json"
 | |
| import Locale from "../../UI/i18n/Locale"
 | |
| import LinkToWeblate from "../../UI/Base/LinkToWeblate"
 | |
| import FeatureSwitchState from "./FeatureSwitchState"
 | |
| import Constants from "../../Models/Constants";
 | |
| 
 | |
| /**
 | |
|  * The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,
 | |
|  * which layers they enabled, ...
 | |
|  */
 | |
| export default class UserRelatedState {
 | |
|     public static readonly usersettingsConfig = UserRelatedState.initUserRelatedState()
 | |
|     public static readonly availableUserSettingsIds: string[] =
 | |
|         UserRelatedState.usersettingsConfig?.tagRenderings?.map((tr) => tr.id) ?? []
 | |
|     /**
 | |
|      The user credentials
 | |
|      */
 | |
|     public osmConnection: OsmConnection
 | |
|     /**
 | |
|      * The key for mangrove
 | |
|      */
 | |
|     public readonly mangroveIdentity: MangroveIdentity
 | |
|     public readonly installedUserThemes: Store<string[]>
 | |
|     public readonly showAllQuestionsAtOnce: UIEventSource<boolean>
 | |
|     public readonly showTags: UIEventSource<"no" | undefined | "always" | "yes">;
 | |
|     public readonly homeLocation: FeatureSource
 | |
|     /**
 | |
|      * The number of seconds that the GPS-locations are stored in memory.
 | |
|      * Time in seconds
 | |
|      */
 | |
|     public readonly gpsLocationHistoryRetentionTime = new UIEventSource(
 | |
|         7 * 24 * 60 * 60,
 | |
|         "gps_location_retention"
 | |
|     )
 | |
|     /**
 | |
|      * Preferences as tags exposes many preferences and state properties as record.
 | |
|      * This is used to bridge the internal state with the usersettings.json layerconfig file
 | |
|      */
 | |
|     public readonly preferencesAsTags: UIEventSource<Record<string, string>>
 | |
| 
 | |
|     constructor(
 | |
|         osmConnection: OsmConnection,
 | |
|         availableLanguages?: string[],
 | |
|         layout?: LayoutConfig,
 | |
|         featureSwitches?: FeatureSwitchState
 | |
|     ) {
 | |
|         this.osmConnection = osmConnection
 | |
|         {
 | |
|             const translationMode: UIEventSource<undefined | "true" | "false" | "mobile" | string> =
 | |
|                 this.osmConnection.GetPreference("translation-mode", "false")
 | |
|             translationMode.addCallbackAndRunD((mode) => {
 | |
|                 mode = mode.toLowerCase()
 | |
|                 if (mode === "true" || mode === "yes") {
 | |
|                     Locale.showLinkOnMobile.setData(false)
 | |
|                     Locale.showLinkToWeblate.setData(true)
 | |
|                 } else if (mode === "false" || mode === "no") {
 | |
|                     Locale.showLinkToWeblate.setData(false)
 | |
|                 } else if (mode === "mobile") {
 | |
|                     Locale.showLinkOnMobile.setData(true)
 | |
|                     Locale.showLinkToWeblate.setData(true)
 | |
|                 } else {
 | |
|                     Locale.showLinkOnMobile.setData(false)
 | |
|                     Locale.showLinkToWeblate.setData(false)
 | |
|                 }
 | |
|             })
 | |
|         }
 | |
| 
 | |
|         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.showTags = <UIEventSource<any>>this.osmConnection.GetPreference("show_tags")
 | |
| 
 | |
|         this.mangroveIdentity = new MangroveIdentity(
 | |
|             this.osmConnection.GetLongPreference("identity", "mangrove")
 | |
|         )
 | |
| 
 | |
|         this.InitializeLanguage(availableLanguages)
 | |
| 
 | |
|         this.installedUserThemes = this.InitInstalledUserThemes()
 | |
| 
 | |
|         this.homeLocation = this.initHomeLocation()
 | |
| 
 | |
|         this.preferencesAsTags = this.initAmendedPrefs(layout, featureSwitches)
 | |
|     }
 | |
| 
 | |
|     private static initUserRelatedState(): LayerConfig {
 | |
|         try{
 | |
| 
 | |
|         return new LayerConfig(
 | |
|             <LayerConfigJson>usersettings,
 | |
|             "userinformationpanel"
 | |
|         )
 | |
|         }catch(e){
 | |
|             return undefined
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     public GetUnofficialTheme(id: string):
 | |
|         | {
 | |
|         id: string
 | |
|         icon: string
 | |
|         title: any
 | |
|         shortDescription: any
 | |
|         definition?: any
 | |
|         isOfficial: boolean
 | |
|     }
 | |
|         | undefined {
 | |
|         console.log("GETTING UNOFFICIAL THEME")
 | |
|         const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id)
 | |
|         const str = pref.data
 | |
| 
 | |
|         if (str === undefined || str === "undefined" || str === "") {
 | |
|             pref.setData(null)
 | |
|             return undefined
 | |
|         }
 | |
| 
 | |
|         try {
 | |
|             const value: {
 | |
|                 id: string
 | |
|                 icon: string
 | |
|                 title: any
 | |
|                 shortDescription: any
 | |
|                 definition?: any
 | |
|                 isOfficial: boolean
 | |
|             } = JSON.parse(str)
 | |
|             value.isOfficial = false
 | |
|             return value
 | |
|         } catch (e) {
 | |
|             console.warn(
 | |
|                 "Removing theme " +
 | |
|                 id +
 | |
|                 " as it could not be parsed from the preferences; the content is:",
 | |
|                 str
 | |
|             )
 | |
|             pref.setData(null)
 | |
|             return undefined
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     public markLayoutAsVisited(layout: LayoutConfig) {
 | |
|         if (!layout) {
 | |
|             console.error("Trying to mark a layout as visited, but ", layout, " got passed")
 | |
|             return
 | |
|         }
 | |
|         if (layout.hideFromOverview) {
 | |
|             this.osmConnection.isLoggedIn.addCallbackAndRunD((loggedIn) => {
 | |
|                 if (loggedIn) {
 | |
|                     this.osmConnection
 | |
|                         .GetPreference("hidden-theme-" + layout?.id + "-enabled")
 | |
|                         .setData("true")
 | |
|                     return true
 | |
|                 }
 | |
|             })
 | |
|         }
 | |
|         if (!layout.official) {
 | |
|             this.osmConnection.GetLongPreference("unofficial-theme-" + layout.id).setData(
 | |
|                 JSON.stringify({
 | |
|                     id: layout.id,
 | |
|                     icon: layout.icon,
 | |
|                     title: layout.title.translations,
 | |
|                     shortDescription: layout.shortDescription.translations,
 | |
|                     definition: layout["definition"],
 | |
|                 })
 | |
|             )
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private InitializeLanguage(availableLanguages?: string[]) {
 | |
|         Locale.language.syncWith(this.osmConnection.GetPreference("language"))
 | |
|         Locale.language.addCallback((currentLanguage) => {
 | |
|             if (Locale.showLinkToWeblate.data) {
 | |
|                 return true // Disable auto switching as we are in translators mode
 | |
|             }
 | |
|             if (availableLanguages?.indexOf(currentLanguage) < 0) {
 | |
|                 console.log(
 | |
|                     "Resetting language to",
 | |
|                     availableLanguages[0],
 | |
|                     "as",
 | |
|                     currentLanguage,
 | |
|                     " is unsupported"
 | |
|                 )
 | |
|                 // The current language is not supported -> switch to a supported one
 | |
|                 Locale.language.setData(availableLanguages[0])
 | |
|             }
 | |
|         })
 | |
|         Locale.language.ping()
 | |
|     }
 | |
| 
 | |
|     private InitInstalledUserThemes(): Store<string[]> {
 | |
|         const prefix = "mapcomplete-unofficial-theme-"
 | |
|         const postfix = "-combined-length"
 | |
|         return this.osmConnection.preferencesHandler.preferences.map((prefs) =>
 | |
|             Object.keys(prefs)
 | |
|                 .filter((k) => k.startsWith(prefix) && k.endsWith(postfix))
 | |
|                 .map((k) => k.substring(prefix.length, k.length - postfix.length))
 | |
|         )
 | |
|     }
 | |
| 
 | |
|     private initHomeLocation(): FeatureSource {
 | |
|         const empty = []
 | |
|         const feature: Store<Feature[]> = Stores.ListStabilized(
 | |
|             this.osmConnection.userDetails.map((userDetails) => {
 | |
|                 if (userDetails === undefined) {
 | |
|                     return undefined
 | |
|                 }
 | |
|                 const home = userDetails.home
 | |
|                 if (home === undefined) {
 | |
|                     return undefined
 | |
|                 }
 | |
|                 return [home.lon, home.lat]
 | |
|             })
 | |
|         ).map((homeLonLat) => {
 | |
|             if (homeLonLat === undefined) {
 | |
|                 return empty
 | |
|             }
 | |
|             return [
 | |
|                 <Feature>{
 | |
|                     type: "Feature",
 | |
|                     properties: {
 | |
|                         id: "home",
 | |
|                         "user:home": "yes",
 | |
|                         _lon: homeLonLat[0],
 | |
|                         _lat: homeLonLat[1],
 | |
|                     },
 | |
|                     geometry: {
 | |
|                         type: "Point",
 | |
|                         coordinates: homeLonLat,
 | |
|                     },
 | |
|                 },
 | |
|             ]
 | |
|         })
 | |
|         return new StaticFeatureSource(feature)
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Initialize the 'amended preferences'.
 | |
|      * This is inherently a dirty and chaotic method, as it shoves many properties into this EventSourcd
 | |
|      * */
 | |
|     private initAmendedPrefs(
 | |
|         layout?: LayoutConfig,
 | |
|         featureSwitches?: FeatureSwitchState
 | |
|     ): UIEventSource<Record<string, string>> {
 | |
|         const amendedPrefs = new UIEventSource<Record<string, string>>({
 | |
|             _theme: layout?.id,
 | |
|             _backend: this.osmConnection.Backend(),
 | |
|             _applicationOpened: new Date().toISOString(),
 | |
|             _supports_sharing: "yes" // TODO window.navigator.share ? "yes" : "no"
 | |
|         })
 | |
| 
 | |
|         for (const key in Constants.userJourney) {
 | |
|             amendedPrefs.data["__userjourney_" + key] = Constants.userJourney[key]
 | |
|         }
 | |
| 
 | |
|         const osmConnection = this.osmConnection
 | |
|         osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => {
 | |
|             for (const k in newPrefs) {
 | |
|                 amendedPrefs.data[k] = newPrefs[k]
 | |
|             }
 | |
|             amendedPrefs.ping()
 | |
|         })
 | |
|         const usersettingsConfig = UserRelatedState.usersettingsConfig
 | |
|         const translationMode = osmConnection.GetPreference("translation-mode")
 | |
|         Locale.language.mapD(
 | |
|             (language) => {
 | |
|                 amendedPrefs.data["_language"] = language
 | |
|                 const trmode = translationMode.data
 | |
|                 if ((trmode === "true" || trmode === "mobile") && layout !== undefined) {
 | |
|                     const missing = layout.missingTranslations()
 | |
|                     const total = missing.total
 | |
| 
 | |
|                     const untranslated = missing.untranslated.get(language) ?? []
 | |
|                     const hasMissingTheme = untranslated.some((k) => k.startsWith("themes:"))
 | |
|                     const missingLayers = Utils.Dedup(
 | |
|                         untranslated
 | |
|                             .filter((k) => k.startsWith("layers:"))
 | |
|                             .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
 | |
|                                 ),
 | |
|                             }
 | |
|                             : undefined,
 | |
|                         ...missingLayers.map((id) => ({
 | |
|                             id: "layer:" + id,
 | |
|                             link: LinkToWeblate.hrefToWeblateZen(language, "layers", id),
 | |
|                         })),
 | |
|                     ])
 | |
|                     const untranslated_count = untranslated.length
 | |
|                     amendedPrefs.data["_translation_total"] = "" + total
 | |
|                     amendedPrefs.data["_translation_translated_count"] =
 | |
|                         "" + (total - untranslated_count)
 | |
|                     amendedPrefs.data["_translation_percentage"] =
 | |
|                         "" + Math.floor((100 * (total - untranslated_count)) / total)
 | |
|                     amendedPrefs.data["_translation_links"] = JSON.stringify(zenLinks)
 | |
|                 }
 | |
|                 amendedPrefs.ping()
 | |
|             },
 | |
|             [translationMode]
 | |
|         )
 | |
|         osmConnection.userDetails.addCallback((userDetails) => {
 | |
|             for (const k in userDetails) {
 | |
|                 amendedPrefs.data["_" + k] = "" + userDetails[k]
 | |
|             }
 | |
| 
 | |
|             for (const [name, code, _] of usersettingsConfig.calculatedTags) {
 | |
|                 try {
 | |
|                     let result = new Function("feat", "return " + code + ";")({
 | |
|                         properties: amendedPrefs.data,
 | |
|                     })
 | |
|                     if (result !== undefined && result !== "" && result !== null) {
 | |
|                         if (typeof result !== "string") {
 | |
|                             result = JSON.stringify(result)
 | |
|                         }
 | |
|                         amendedPrefs.data[name] = result
 | |
|                     }
 | |
|                 } catch (e) {
 | |
|                     console.error(
 | |
|                         "Calculating a tag for userprofile-settings failed for variable",
 | |
|                         name,
 | |
|                         e
 | |
|                     )
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             const simplifiedName = userDetails.name.toLowerCase().replace(/\s+/g, "")
 | |
|             const isTranslator = translators.contributors.find(
 | |
|                 (c: { contributor: string; commits: number }) => {
 | |
|                     const replaced = c.contributor.toLowerCase().replace(/\s+/g, "")
 | |
|                     return replaced === simplifiedName
 | |
|                 }
 | |
|             )
 | |
|             if (isTranslator) {
 | |
|                 amendedPrefs.data["_translation_contributions"] = "" + isTranslator.commits
 | |
|             }
 | |
|             const isCodeContributor = codeContributors.contributors.find(
 | |
|                 (c: { contributor: string; commits: number }) => {
 | |
|                     const replaced = c.contributor.toLowerCase().replace(/\s+/g, "")
 | |
|                     return replaced === simplifiedName
 | |
|                 }
 | |
|             )
 | |
|             if (isCodeContributor) {
 | |
|                 amendedPrefs.data["_code_contributions"] = "" + isCodeContributor.commits
 | |
|             }
 | |
|             amendedPrefs.ping()
 | |
|         })
 | |
| 
 | |
|         amendedPrefs.addCallbackD((tags) => {
 | |
|             for (const key in tags) {
 | |
|                 if (key.startsWith("_") || key === "mapcomplete-language") {
 | |
|                     // Language is managed seperately
 | |
|                     continue
 | |
|                 }
 | |
|                 this.osmConnection.GetPreference(key, undefined, {prefix: ""}).setData(tags[key])
 | |
|             }
 | |
|         })
 | |
| 
 | |
|         for (const key in featureSwitches) {
 | |
|             if (featureSwitches[key].addCallbackAndRun) {
 | |
|                 featureSwitches[key].addCallbackAndRun((v) => {
 | |
|                     const oldV = amendedPrefs.data["__" + key]
 | |
|                     if (oldV === v) {
 | |
|                         return
 | |
|                     }
 | |
|                     amendedPrefs.data["__" + key] = "" + v
 | |
|                     amendedPrefs.ping()
 | |
|                 })
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return amendedPrefs
 | |
|     }
 | |
| }
 |