diff --git a/Logic/State/UserRelatedState.ts b/Logic/State/UserRelatedState.ts index c4d86fde6f..c56cd2cbc6 100644 --- a/Logic/State/UserRelatedState.ts +++ b/Logic/State/UserRelatedState.ts @@ -2,10 +2,17 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import { OsmConnection } from "../Osm/OsmConnection" import { MangroveIdentity } from "../Web/MangroveReviews" import { Store, Stores, UIEventSource } from "../UIEventSource" -import Locale from "../../UI/i18n/Locale" 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" /** * The part of the state which keeps track of user-related stuff, e.g. the OSM-connection, @@ -30,16 +37,30 @@ export default class UserRelatedState { * The number of seconds that the GPS-locations are stored in memory. * Time in seconds */ - public gpsLocationHistoryRetentionTime = new UIEventSource( + public readonly gpsLocationHistoryRetentionTime = new UIEventSource( 7 * 24 * 60 * 60, "gps_location_retention" ) - constructor(osmConnection: OsmConnection, availableLanguages?: string[]) { + /** + * 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> + public static readonly usersettingsConfig = new LayerConfig( + usersettings, + "userinformationpanel" + ) + + constructor( + osmConnection: OsmConnection, + availableLanguages?: string[], + layout?: LayoutConfig + ) { this.osmConnection = osmConnection { const translationMode: UIEventSource = - this.osmConnection.GetPreference("translation-mode") + this.osmConnection.GetPreference("translation-mode", "false") translationMode.addCallbackAndRunD((mode) => { mode = mode.toLowerCase() if (mode === "true" || mode === "yes") { @@ -73,6 +94,8 @@ export default class UserRelatedState { this.installedUserThemes = this.InitInstalledUserThemes() this.homeLocation = this.initHomeLocation() + + this.preferencesAsTags = this.initAmendedPrefs(layout) } public GetUnofficialTheme(id: string): @@ -211,4 +234,127 @@ export default class UserRelatedState { }) 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): UIEventSource> { + const amendedPrefs = new UIEventSource>({ + _theme: layout?.id, + _backend: this.osmConnection.Backend(), + }) + + 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) + console.log("Setting zenLinks", zenLinks) + 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("_")) { + continue + } + this.osmConnection.GetPreference(key, undefined, { prefix: "" }).setData(tags[key]) + } + }) + + return amendedPrefs + } } diff --git a/Models/FilteredLayer.ts b/Models/FilteredLayer.ts index ea6f4173bf..9679f34470 100644 --- a/Models/FilteredLayer.ts +++ b/Models/FilteredLayer.ts @@ -123,7 +123,6 @@ export default class FilteredLayer { } else { properties = fieldstate } - console.log("Building tagsspec with properties", properties) const missingKeys = option.fields .map((f) => f.name) .filter((key) => properties[key] === undefined) @@ -182,7 +181,6 @@ export default class FilteredLayer { // We calculate the fields const fieldProperties = FilteredLayer.stringToFieldProperties(state.data) const asTags = FilteredLayer.fieldsToTags(filter.options[0], fieldProperties) - console.log("Current field properties:", state.data, fieldProperties, asTags) if (asTags) { needed.push(asTags) } diff --git a/Models/MenuState.ts b/Models/MenuState.ts index fc51e830ca..1d713e98c1 100644 --- a/Models/MenuState.ts +++ b/Models/MenuState.ts @@ -21,6 +21,7 @@ export class MenuState { public readonly highlightedLayerInFilters: UIEventSource = new UIEventSource( undefined ) + public highlightedUserSetting: UIEventSource = new UIEventSource(undefined) constructor() { this.themeViewTabIndex = new UIEventSource(0) this.themeViewTab = this.themeViewTabIndex.sync( @@ -57,6 +58,12 @@ export class MenuState { } } + public openUsersettings(highlightTagRendering?: string) { + this.menuIsOpened.setData(true) + this.menuViewTab.setData("settings") + this.highlightedUserSetting.setData(highlightTagRendering) + } + public closeAll() { this.menuIsOpened.setData(false) this.themeIsOpened.setData(false) diff --git a/Models/ThemeConfig/Conversion/PrepareLayer.ts b/Models/ThemeConfig/Conversion/PrepareLayer.ts index 2c3a4be775..aaaa5d1587 100644 --- a/Models/ThemeConfig/Conversion/PrepareLayer.ts +++ b/Models/ThemeConfig/Conversion/PrepareLayer.ts @@ -805,7 +805,13 @@ export class RewriteSpecial extends DesugaringStep { const param = special[arg.name] if (param === undefined) { errors.push( - `At ${context}: Obligated parameter '${arg.name}' in special rendering of type ${vis.funcName} not found.\n${arg.doc}` + `At ${context}: Obligated parameter '${ + arg.name + }' in special rendering of type ${ + vis.funcName + } not found.\n The full special rendering specification is: '${JSON.stringify( + input + )}'\n ${arg.name}: ${arg.doc}` ) } } diff --git a/Models/ThemeViewState.ts b/Models/ThemeViewState.ts index 76d362de24..24266186b5 100644 --- a/Models/ThemeViewState.ts +++ b/Models/ThemeViewState.ts @@ -101,7 +101,7 @@ export default class ThemeViewState implements SpecialVisualizationState { ), osmConfiguration: <"osm" | "osm-test">this.featureSwitches.featureSwitchApiURL.data, }) - this.userRelatedState = new UserRelatedState(this.osmConnection, layout?.language) + this.userRelatedState = new UserRelatedState(this.osmConnection, layout?.language, layout) this.selectedElement = new UIEventSource(undefined, "Selected element") this.selectedLayer = new UIEventSource(undefined, "Selected layer") this.geolocation = new GeoLocationHandler( diff --git a/UI/Base/LoginToggle.svelte b/UI/Base/LoginToggle.svelte index c14edd1258..e3c2a4ad08 100644 --- a/UI/Base/LoginToggle.svelte +++ b/UI/Base/LoginToggle.svelte @@ -17,6 +17,7 @@ const offlineModes: Partial> = { offline: t.loginFailedOfflineMode, unreachable: t.loginFailedUnreachableMode, + unknown: t.loginFailedUnreachableMode, readonly: t.loginFailedReadonlyMode }; const apiState = state.osmConnection.apiIsOnline; diff --git a/UI/BigComponents/SelectedElementView.svelte b/UI/BigComponents/SelectedElementView.svelte index 9e1f498c6c..493af1d355 100644 --- a/UI/BigComponents/SelectedElementView.svelte +++ b/UI/BigComponents/SelectedElementView.svelte @@ -7,15 +7,17 @@ import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"; import { onDestroy } from "svelte"; - export let selectedElement: Feature; + export let state: SpecialVisualizationState; export let layer: LayerConfig; + export let selectedElement: Feature; export let tags: UIEventSource>; + export let highlightedRendering: UIEventSource = undefined; + let _tags: Record; onDestroy(tags.addCallbackAndRun(tags => { _tags = tags; })); - export let state: SpecialVisualizationState;
@@ -40,7 +42,7 @@ {#each layer.tagRenderings as config (config.id)} {#if config.condition === undefined || config.condition.matchesProperties(_tags)} {#if config.IsKnown(_tags)} - + {/if} {/if} {/each} diff --git a/UI/BigComponents/UserInformation.ts b/UI/BigComponents/UserInformation.ts index bba7a279a9..f5d9a9dc13 100644 --- a/UI/BigComponents/UserInformation.ts +++ b/UI/BigComponents/UserInformation.ts @@ -1,4 +1,3 @@ -import ScrollableFullScreen from "../Base/ScrollableFullScreen" import Translations from "../i18n/Translations" import { OsmConnection } from "../../Logic/Osm/OsmConnection" import Combine from "../Base/Combine" @@ -8,25 +7,13 @@ import { VariableUiElement } from "../Base/VariableUIElement" import Img from "../Base/Img" import { FixedUiElement } from "../Base/FixedUiElement" import Link from "../Base/Link" -import { Store, UIEventSource } from "../../Logic/UIEventSource" +import { UIEventSource } from "../../Logic/UIEventSource" import Loc from "../../Models/Loc" import BaseUIElement from "../BaseUIElement" import Showdown from "showdown" import LanguagePicker from "../LanguagePicker" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import Constants from "../../Models/Constants" -import EditableTagRendering from "../Popup/EditableTagRendering" -import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" -import { SaveButton } from "../Popup/SaveButton" -import { TagUtils } from "../../Logic/Tags/TagUtils" -import usersettings from "../../assets/generated/layers/usersettings.json" -import { LoginToggle } from "../Popup/LoginButton" -import LayerConfig from "../../Models/ThemeConfig/LayerConfig" -import translators from "../../assets/translators.json" -import codeContributors from "../../assets/contributors.json" -import Locale from "../i18n/Locale" -import { Utils } from "../../Utils" -import LinkToWeblate from "../Base/LinkToWeblate" export class ImportViewerLinks extends VariableUiElement { constructor(osmConnection: OsmConnection) { @@ -48,51 +35,6 @@ export class ImportViewerLinks extends VariableUiElement { } } -class SingleUserSettingsPanel extends EditableTagRendering { - constructor( - config: TagRenderingConfig, - osmConnection: OsmConnection, - amendedPrefs: UIEventSource, - userInfoFocusedQuestion?: UIEventSource - ) { - const editMode = new UIEventSource(false) - // Isolate the preferences. They'll be updated explicitely later on anyway - super( - amendedPrefs, - config, - [], - { osmConnection }, - { - answerElementClasses: "p-2", - editMode, - createSaveButton: (store) => - new SaveButton(amendedPrefs, osmConnection).onClick(() => { - const selection = TagUtils.FlattenMultiAnswer( - TagUtils.FlattenAnd(store.data, amendedPrefs.data) - ).asChange(amendedPrefs.data) - for (const kv of selection) { - if (kv.k.startsWith("_")) { - continue - } - osmConnection.GetPreference(kv.k, "", { prefix: "" }).setData(kv.v) - } - - editMode.setData(false) - }), - } - ) - const self = this - this.SetClass("rounded-xl") - userInfoFocusedQuestion.addCallbackAndRun((selected) => { - if (config.id !== selected) { - self.RemoveClass("glowing-shadow") - } else { - self.SetClass("glowing-shadow") - } - }) - } -} - class UserInformationMainPanel extends VariableUiElement { private readonly settings: UIEventSource> private readonly userInfoFocusedQuestion?: UIEventSource @@ -104,210 +46,9 @@ class UserInformationMainPanel extends VariableUiElement { isOpened: UIEventSource, userInfoFocusedQuestion?: UIEventSource ) { - const t = Translations.t.userinfo - const imgSize = "h-6 w-6" - const ud = osmConnection.userDetails const settings = new UIEventSource>({}) - const usersettingsConfig = new LayerConfig(usersettings, "userinformationpanel") - const amendedPrefs = new UIEventSource({ _theme: layout?.id }) - osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => { - for (const k in newPrefs) { - amendedPrefs.data[k] = newPrefs[k] - } - amendedPrefs.ping() - }) - const translationMode = osmConnection.GetPreference("translation-mode") - Locale.language.mapD( - (language) => { - amendedPrefs.data["_language"] = language - const trmode = translationMode.data - if (trmode === "true" || trmode === "mobile") { - 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) - console.log("Setting zenLinks", zenLinks) - 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() - }) - - super( - ud.map((ud) => { - let img: BaseUIElement = Svg.person_ui().SetClass("block") - if (ud.img !== undefined) { - img = new Img(ud.img) - } - img.SetClass("rounded-full h-12 w-12 m-4") - - let description: BaseUIElement = undefined - const editLink = osmConnection.Backend() + "/profile/edit" - if (ud.description) { - const editButton = new Link( - Svg.pencil_svg().SetClass("h-4 w-4"), - editLink, - true - ).SetClass( - "absolute block bg-subtle rounded-full p-2 bottom-2 right-2 w-min self-end" - ) - - const htmlString = new Showdown.Converter() - .makeHtml(ud.description) - .replace(/>/g, ">") - .replace(/</g, "<") - description = new Combine([ - new FixedUiElement(htmlString).SetClass("link-underline"), - editButton, - ]).SetClass("relative w-full m-2") - } else { - description = new Combine([ - t.noDescription, - new SubtleButton(Svg.pencil_svg(), t.noDescriptionCallToAction, { - imgSize, - url: editLink, - newTab: true, - }), - ]).SetClass("w-full m-2") - } - - let panToHome: BaseUIElement - if (ud.home) { - panToHome = new SubtleButton(Svg.home_svg(), t.moveToHome, { - imgSize, - }).onClick(() => { - const home = ud?.home - if (home === undefined) { - return - } - locationControl.setData({ ...home, zoom: 16 }) - isOpened.setData(false) - }) - } - - const settingElements = [] - for (const c of usersettingsConfig.tagRenderings) { - const settingsPanel = new SingleUserSettingsPanel( - c, - osmConnection, - amendedPrefs, - userInfoFocusedQuestion - ).SetClass("block my-4") - settings.data[c.id] = settingsPanel - settingElements.push(settingsPanel) - } - settings.ping() - - return new Combine([ - new Combine([img, description]).SetClass("flex border border-black rounded-md"), - new LanguagePicker( - layout.language, - Translations.t.general.pickLanguage.Clone() - ), - ...settingElements, - new SubtleButton( - Svg.envelope_svg(), - new Combine([ - t.gotoInbox, - ud.unreadMessages == 0 - ? undefined - : t.newMessages.SetClass("alert block"), - ]), - { imgSize, url: `${ud.backend}/messages/inbox`, newTab: true } - ), - new SubtleButton(Svg.gear_svg(), t.gotoSettings, { - imgSize, - url: `${ud.backend}/user/${encodeURIComponent(ud.name)}/account`, - newTab: true, - }), - panToHome, - new ImportViewerLinks(osmConnection), - new SubtleButton(Svg.logout_svg(), Translations.t.general.logout, { - imgSize, - }).onClick(() => { - osmConnection.LogOut() - }), - ]) - }) - ) - this.SetClass("flex flex-col") + super() this.settings = settings this.userInfoFocusedQuestion = userInfoFocusedQuestion const self = this @@ -325,50 +66,3 @@ class UserInformationMainPanel extends VariableUiElement { this.settings.data[focusedId]?.ScrollIntoView() } } - -export default class UserInformationPanel extends ScrollableFullScreen { - private readonly userPanel: UserInformationMainPanel - - constructor( - state: { - readonly layoutToUse: LayoutConfig - readonly osmConnection: OsmConnection - readonly locationControl: UIEventSource - readonly featureSwitchUserbadge: Store - }, - options?: { - isOpened?: UIEventSource - userInfoFocusedQuestion?: UIEventSource - } - ) { - const isOpened = options?.isOpened ?? new UIEventSource(false) - const userPanel = new UserInformationMainPanel( - state.osmConnection, - state.locationControl, - state.layoutToUse, - isOpened, - options?.userInfoFocusedQuestion - ) - super( - () => { - return new VariableUiElement( - state.osmConnection.userDetails.map((ud) => { - if (ud.loggedIn === false) { - return Translations.t.userinfo.titleNotLoggedIn - } - return Translations.t.userinfo.welcome.Subs(ud) - }) - ) - }, - () => new LoginToggle(userPanel, Translations.t.general.getStartedLogin, state), - "userinfo", - isOpened - ) - this.userPanel = userPanel - } - - Activate() { - super.Activate() - this.userPanel?.focusOnSelectedQuestion() - } -} diff --git a/UI/BigComponents/UserProfile.svelte b/UI/BigComponents/UserProfile.svelte new file mode 100644 index 0000000000..1b986ea802 --- /dev/null +++ b/UI/BigComponents/UserProfile.svelte @@ -0,0 +1,48 @@ + + + diff --git a/UI/DefaultGUI.ts b/UI/DefaultGUI.ts index 5f0dba826e..0cc76e64b4 100644 --- a/UI/DefaultGUI.ts +++ b/UI/DefaultGUI.ts @@ -158,30 +158,6 @@ export default class DefaultGUI { const self = this - const userInfoMapControl = Toggle.If(state.featureSwitchUserbadge, () => { - new UserInformationPanel(state, { - isOpened: guiState.userInfoIsOpened, - userInfoFocusedQuestion: guiState.userInfoFocusedQuestion, - }) - - const mapControl = new MapControlButton( - new VariableUiElement( - state.osmConnection.userDetails.map((ud) => { - if (ud?.img === undefined) { - return Svg.person_ui().SetClass("mt-1 block") - } - return new Img(ud?.img) - }) - ).SetClass("block rounded-full overflow-hidden"), - { - dontStyle: true, - } - ).onClick(() => { - self.guiState.userInfoIsOpened.setData(true) - }) - - return new LoginToggle(mapControl, Translations.t.general.loginWithOpenStreetMap, state) - }) const extraLink = Toggle.If( state.featureSwitchExtraLinkEnabled, () => new ExtraLinkButton(state, state.layoutToUse.extraLink) @@ -200,7 +176,7 @@ export default class DefaultGUI { const copyright = new MapControlButton(Svg.copyright_svg()).onClick(() => guiState.copyrightViewIsOpened.setData(true) ) - new Combine([welcomeMessageMapControl, userInfoMapControl, copyright, extraLink]) + new Combine([welcomeMessageMapControl, copyright, extraLink]) .SetClass("flex flex-col") .AttachTo("top-left") diff --git a/UI/Popup/TagRendering/TagRenderingEditable.svelte b/UI/Popup/TagRendering/TagRenderingEditable.svelte index 20f727c81f..675a902a0d 100644 --- a/UI/Popup/TagRendering/TagRenderingEditable.svelte +++ b/UI/Popup/TagRendering/TagRenderingEditable.svelte @@ -15,32 +15,51 @@ export let tags: UIEventSource>; export let selectedElement: Feature; export let state: SpecialVisualizationState; - export let layer: LayerConfig + export let layer: LayerConfig; - export let showQuestionIfUnknown : boolean= false - let editMode = false + export let highlightedRendering: UIEventSource = undefined; + export let showQuestionIfUnknown: boolean = false; + let editMode = false; onDestroy(tags.addCallbackAndRunD(tags => { - editMode = showQuestionIfUnknown && !config.IsKnown(tags) - - })) + editMode = showQuestionIfUnknown && !config.IsKnown(tags); + + })); + + let htmlElem: HTMLElement; + if (highlightedRendering) { + onDestroy(highlightedRendering.addCallbackAndRun(highlighted => { + console.log("Highlighted rendering is", highlighted) + if(htmlElem === undefined){ + return + } + if (config.id === highlighted) { + htmlElem.classList.add("glowing-shadow"); + } else { + htmlElem.classList.remove("glowing-shadow"); + } + })); + } + -{#if config.question} - {#if editMode} - - - - {:else} -
- - -
+
+ {#if config.question} + {#if editMode} + + + + {:else} +
+ + +
+ {/if} + {:else } + {/if} -{:else } - -{/if} +
diff --git a/UI/Popup/TagRendering/TagRenderingQuestion.svelte b/UI/Popup/TagRendering/TagRenderingQuestion.svelte index 011c37aa74..77525cf071 100644 --- a/UI/Popup/TagRendering/TagRenderingQuestion.svelte +++ b/UI/Popup/TagRendering/TagRenderingQuestion.svelte @@ -10,7 +10,6 @@ import { TagsFilter } from "../../../Logic/Tags/TagsFilter"; import FreeformInput from "./FreeformInput.svelte"; import Translations from "../../i18n/Translations.js"; - import FromHtml from "../../Base/FromHtml.svelte"; import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"; import { createEventDispatcher } from "svelte"; import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; @@ -63,6 +62,24 @@ }>(); function onSave() { + + if (layer.source === null) { + /** + * This is a special, priviliged layer. + * We simply apply the tags onto the records + */ + const kv = selectedTags.asChange(tags.data); + for (const { k, v } of kv) { + if (v === undefined) { + delete tags.data[k]; + } else { + tags.data[k] = v; + } + } + tags.ping(); + return; + } + dispatch("saved", { config, applied: selectedTags }); const change = new ChangeTagAction( tags.data.id, @@ -76,6 +93,7 @@ freeformInput.setData(undefined); selectedMapping = 0; selectedTags = undefined; + change.CreateChangeDescriptions().then(changes => state.changes.applyChanges(changes) ).catch(console.error); @@ -93,12 +111,14 @@ {config.id}
- + {#if config.questionhint}
- +
{/if} @@ -152,7 +172,7 @@ {/if} - +
@@ -163,7 +183,7 @@ {:else }
- +
{/if}
diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index a51c03d9c8..c9fb001be4 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -56,16 +56,27 @@ import Maproulette from "../Logic/Maproulette" import SvelteUIElement from "./Base/SvelteUIElement" import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" import QuestionViz from "./Popup/QuestionViz" -import SimpleAddUI from "./BigComponents/SimpleAddUI" import { Feature } from "geojson" import { GeoOperations } from "../Logic/GeoOperations" import CreateNewNote from "./Popup/CreateNewNote.svelte" -import { svelte } from "@sveltejs/vite-plugin-svelte" import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte" +import UserProfile from "./BigComponents/UserProfile.svelte" +import LanguagePicker from "./LanguagePicker" +import Link from "./Base/Link" export default class SpecialVisualizations { public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList() + static undoEncoding(str: string) { + return str + .trim() + .replace(/&LPARENS/g, "(") + .replace(/&RPARENS/g, ")") + .replace(/&LBRACE/g, "{") + .replace(/&RBRACE/g, "}") + .replace(/&COMMA/g, ",") + } + /** * * For a given string, returns a specification what parts are fixed and what parts are special renderings. @@ -115,15 +126,7 @@ export default class SpecialVisualizations { ) const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "") if (argument.length > 0) { - const realArgs = argument.split(",").map((str) => - str - .trim() - .replace(/&LPARENS/g, "(") - .replace(/&RPARENS/g, ")") - .replace(/&LBRACE/g, "{") - .replace(/&RBRACE/g, "}") - .replace(/&COMMA/g, ",") - ) + const realArgs = argument.split(",").map((str) => this.undoEncoding(str)) for (let i = 0; i < realArgs.length; i++) { if (args.length <= i) { args.push(realArgs[i]) @@ -273,6 +276,39 @@ export default class SpecialVisualizations { }) }, }, + { + funcName: "user_profile", + args: [], + docs: "A component showing information about the currently logged in user (username, profile description, profile picture + link to edit them). Mostly meant to be used in the 'user-settings'", + constr(state: SpecialVisualizationState): BaseUIElement { + return new SvelteUIElement(UserProfile, { + osmConnection: state.osmConnection, + }) + }, + }, + { + funcName: "language_picker", + args: [], + docs: "A component to set the language of the user interface", + constr(state: SpecialVisualizationState): BaseUIElement { + return new LanguagePicker( + state.layout.language, + Translations.t.general.pickLanguage.Clone() + ) + }, + }, + { + funcName: "logout", + args: [], + docs: "Shows a button where the user can log out", + constr(state: SpecialVisualizationState): BaseUIElement { + return new SubtleButton(Svg.logout_ui(), Translations.t.general.logout, { + imgSize: "w-6 h-6", + }).onClick(() => { + state.osmConnection.LogOut() + }) + }, + }, new HistogramViz(), new StealViz(), new MinimapViz(), @@ -717,7 +753,7 @@ export default class SpecialVisualizations { docs: "Shows the title of the popup. Useful for some cases, e.g. 'What is phone number of {title()}?'", example: "`What is the phone number of {title()}`, which might automatically become `What is the phone number of XYZ`.", - constr: (state, tagsSource, args, feature) => + constr: (state, tagsSource) => new VariableUiElement( tagsSource.map((tags) => { const layer = state.layout.getMatchingLayer(tags) @@ -933,6 +969,40 @@ export default class SpecialVisualizations { ) }, }, + { + funcName: "link", + docs: "Construct a link. By using the 'special' visualisation notation, translation should be easier", + args: [ + { + name: "text", + doc: "Text to be shown", + required: true, + }, + { + name: "href", + doc: "The URL to link to", + required: true, + }, + ], + constr( + state: SpecialVisualizationState, + tagSource: UIEventSource>, + args: string[], + feature: Feature + ): BaseUIElement { + const [text, href] = args + return new VariableUiElement( + tagSource.map( + (tags) => + new Link( + Utils.SubstituteKeys(text, tags), + Utils.SubstituteKeys(href, tags), + true + ) + ) + ) + }, + }, { funcName: "multi", docs: "Given an embedded tagRendering (read only) and a key, will read the keyname as a JSON-list. Every element of this list will be considered as tags and rendered with the tagRendering", diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index c1654fe26c..36d8952f82 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -87,7 +87,7 @@ export class SubstitutedTranslation extends VariableUiElement { tagsSource.data.id ) return viz.func - .constr(state, tagsSource, proto.args, feature, undefined) + .constr(state, tagsSource, proto.args.map(t => SpecialVisualizations.undoEncoding(t)), feature, undefined) ?.SetStyle(proto.style) } catch (e) { console.error("SPECIALRENDERING FAILED for", tagsSource.data?.id, e) diff --git a/UI/ThemeViewGUI.svelte b/UI/ThemeViewGUI.svelte index 1661bcd500..05e8622e34 100644 --- a/UI/ThemeViewGUI.svelte +++ b/UI/ThemeViewGUI.svelte @@ -2,13 +2,12 @@ import { Store, UIEventSource } from "../Logic/UIEventSource"; import { Map as MlMap } from "maplibre-gl"; import MaplibreMap from "./Map/MaplibreMap.svelte"; - import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; import FeatureSwitchState from "../Logic/State/FeatureSwitchState"; import MapControlButton from "./Base/MapControlButton.svelte"; import ToSvelte from "./Base/ToSvelte.svelte"; import Svg from "../Svg"; import If from "./Base/If.svelte"; - import { GeolocationControl } from "./BigComponents/GeolocationControl.js"; + import { GeolocationControl } from "./BigComponents/GeolocationControl"; import type { Feature } from "geojson"; import SelectedElementView from "./BigComponents/SelectedElementView.svelte"; import LayerConfig from "../Models/ThemeConfig/LayerConfig"; @@ -17,19 +16,21 @@ import ThemeViewState from "../Models/ThemeViewState"; import type { MapProperties } from "../Models/MapProperties"; import Geosearch from "./BigComponents/Geosearch.svelte"; - import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui"; import Translations from "./i18n/Translations"; - import { CogIcon, MenuIcon, EyeIcon } from "@rgossiaux/svelte-heroicons/solid"; + import { CogIcon, EyeIcon, MenuIcon } from "@rgossiaux/svelte-heroicons/solid"; import Tr from "./Base/Tr.svelte"; import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"; import FloatOver from "./Base/FloatOver.svelte"; - import PrivacyPolicy from "./BigComponents/PrivacyPolicy.js"; - import { Utils } from "../Utils.js"; + import PrivacyPolicy from "./BigComponents/PrivacyPolicy"; + import { Utils } from "../Utils"; import Constants from "../Models/Constants"; import TabbedGroup from "./Base/TabbedGroup.svelte"; + import UserRelatedState from "../Logic/State/UserRelatedState"; + import LoginToggle from "./Base/LoginToggle.svelte"; + import LoginButton from "./Base/LoginButton.svelte"; - export let layout: LayoutConfig; - const state = new ThemeViewState(layout); + export let state: ThemeViewState; + let layout = state.layout; let selectedElementTags: Store>> = state.selectedElement.mapD((f) => { @@ -43,6 +44,7 @@ let mapproperties: MapProperties = state.mapProperties; let featureSwitches: FeatureSwitchState = state.featureSwitches; let availableLayers = state.availableLayers; + let userdetails = state.osmConnection.userDetails; @@ -97,7 +99,7 @@ {#if $selectedElement !== undefined && $selectedLayer !== undefined} {selectedElement.setData(undefined)}}> + tags={$selectedElementTags} state={state} /> {/if} @@ -106,7 +108,7 @@ state.guistate.themeIsOpened.setData(false)}> - +
@@ -130,17 +132,18 @@
- -
+ +
- - + +
-
+
{#each layout.layers as layer} - + {/each} @@ -154,50 +157,56 @@ state.guistate.menuIsOpened.setData(false)}> - {state.guistate.menuViewTabIndex.setData(e.detail)} }> - - selected ? "tab-selected" : "tab-unselected"}> -
- -
-
- selected ? "tab-selected" : "tab-unselected"}> -
- - Settings -
-
- selected ? "tab-selected" : "tab-unselected"}> -
- - Get in touch with others -
-
- selected ? "tab-selected" : "tab-unselected"}> -
- - -
-
-
- - - +
+ +
+ +
+ - {Constants.vNumber} - - User settings - - + {Constants.vNumber} +
-
- - new PrivacyPolicy()}> - -
-
+ +
+ + +
+
+ +
+ + +
+ + +
+ +
+
+ +
+ + Get in touch with others +
+ + +
+ + +
+ new PrivacyPolicy()} slot="content3"> +
diff --git a/assets/layers/usersettings/usersettings.json b/assets/layers/usersettings/usersettings.json index 2cc1e9688a..7de6fa1402 100644 --- a/assets/layers/usersettings/usersettings.json +++ b/assets/layers/usersettings/usersettings.json @@ -5,7 +5,12 @@ "de": "Eine spezielle Ebene, die nicht für die Darstellung auf einer Karte gedacht ist, sondern für die Festlegung von Benutzereinstellungen verwendet wird", "nl": "Een speciale lag die niet getoond wordt op de kaart, maar die de instellingen van de gebruiker weergeeft" }, - "title": null, + "title": { + "render": { + "en": "Settings", + "nl": "Instellingen" + } + }, "source": "special", "calculatedTags": [ "_mastodon_candidate_md=feat.properties._description.match(/\\[[^\\]]*\\]\\((.*(mastodon|en.osm.town).*)\\).*/)?.at(1)", @@ -15,6 +20,66 @@ "_mastodon_candidate=feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a" ], "tagRenderings": [ + { + "id": "profile", + "render": { + "*": "{user_profile()}" + } + }, + { + "id": "language_picker", + "render": { + "*": "{language_picker()}" + } + }, + { + "id": "inbox", + "mappings": [ + { + "if": "_unreadMessages=0", + "then": { + "special": { + "type": "link", + "href": "{_backend}/messages/inbox", + "text": { + "en": "Open your inbox", + "nl": "Ga naar je inbox" + } + } + } + }, + { + "if": "_unreadMessages>0", + "then": { + "special": { + "type": "link", + "text": { + "en": "You have {_unreadMessages}
Open your inbox" + }, + "href": "{_backend}/messages/inbox" + } + } + } + ] + }, + { + "id": "settings-link", + "render": { + "special": { + "type": "link", + "text": { + "en": "Open your settings on OpenStreetMap.org" + }, + "href": "{_backend}/account/edit" + } + } + }, + { + "id": "logout", + "render": { + "*": "{logout()}" + } + }, { "id": "picture-license", "description": "This question is not meant to be placed on an OpenStreetMap-element; however it is used in the user information panel to ask which license the user wants", @@ -328,4 +393,4 @@ } ], "mapRendering": null -} \ No newline at end of file +} diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index de04c40312..9ab4f27b5d 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -859,10 +859,6 @@ video { margin-bottom: 0.75rem; } -.mt-1 { - margin-top: 0.25rem; -} - .mr-2 { margin-right: 0.5rem; } @@ -931,6 +927,10 @@ video { margin-top: 2rem; } +.mt-1 { + margin-top: 0.25rem; +} + .ml-3 { margin-left: 0.75rem; } @@ -1063,14 +1063,14 @@ video { height: 2.75rem; } -.h-96 { - height: 24rem; -} - .h-64 { height: 16rem; } +.h-96 { + height: 24rem; +} + .h-0 { height: 0px; } diff --git a/langs/ca.json b/langs/ca.json index 9feb10800b..e2b7af0af6 100644 --- a/langs/ca.json +++ b/langs/ca.json @@ -594,9 +594,7 @@ }, "userinfo": { "gotoInbox": "Obre la teva safata d'entrada", - "gotoSettings": "Aneu a la vostra configuració a OpenStreetMap.org", - "moveToHome": "Mou el mapa a la vostra ubicació de casa", - "welcome": "Benvingut/da {name}" + "gotoSettings": "Aneu a la vostra configuració a OpenStreetMap.org" }, "validation": { "color": { diff --git a/langs/cs.json b/langs/cs.json index 45ac06c1cd..1cdc2226c2 100644 --- a/langs/cs.json +++ b/langs/cs.json @@ -265,10 +265,7 @@ "userinfo": { "gotoInbox": "Otevřít poštu", "gotoSettings": "Přejít do vašich nastavení na OpenStreetMap.org", - "moveToHome": "Přesunout mapu na vaší domovskou polohu", "noDescription": "Na svém profilu zatím nemáte popis", - "noDescriptionCallToAction": "Přidat popis profilu", - "titleNotLoggedIn": "Vítejte", - "welcome": "Vítejte, {name}" + "noDescriptionCallToAction": "Přidat popis profilu" } } diff --git a/langs/de.json b/langs/de.json index 8832c0a418..e9f4b38564 100644 --- a/langs/de.json +++ b/langs/de.json @@ -934,12 +934,8 @@ "userinfo": { "gotoInbox": "Posteingang öffnen", "gotoSettings": "Einstellungen auf OpenStreetMap.org öffnen", - "moveToHome": "Verschieben Sie die Karte an Ihren Heimatstandort", - "newMessages": "Sie haben neue Nachrichten", "noDescription": "Sie haben noch keine Profilbeschreibung", - "noDescriptionCallToAction": "Profilbeschreibung hinzufügen", - "titleNotLoggedIn": "Willkommen", - "welcome": "Willkommen {name}" + "noDescriptionCallToAction": "Profilbeschreibung hinzufügen" }, "validation": { "color": { diff --git a/langs/en.json b/langs/en.json index b2846b5819..1db77d93ba 100644 --- a/langs/en.json +++ b/langs/en.json @@ -958,12 +958,9 @@ "userinfo": { "gotoInbox": "Open your inbox", "gotoSettings": "Go to your settings on OpenStreetMap.org", - "moveToHome": "Move the map to your home location", - "newMessages": "you have new messages", "noDescription": "You don't have a description on your profile yet", "noDescriptionCallToAction": "Add a profile description", - "titleNotLoggedIn": "Welcome", - "welcome": "Welcome {name}" + "notLoggedIn": "You have logged out" }, "validation": { "color": { diff --git a/langs/es.json b/langs/es.json index 71aa45545c..98a7e8cd3e 100644 --- a/langs/es.json +++ b/langs/es.json @@ -713,9 +713,7 @@ "missing": "{count} cadenas sin traducir", "notImmediate": "Las traducciones no se actualizan directamente. Habitualmente esto lleva unos días" }, - "userinfo": { - "welcome": "Bienvenido {name}" - }, + "userinfo": {}, "validation": { "color": { "description": "Un color o código hexadecimal" diff --git a/langs/fr.json b/langs/fr.json index 134c95ed37..01f205b75d 100644 --- a/langs/fr.json +++ b/langs/fr.json @@ -488,9 +488,7 @@ }, "userinfo": { "gotoInbox": "Ouvrir sa boite de réception", - "gotoSettings": "Paramètres sur OpenStreetMap.org", - "moveToHome": "Déplacez la carte vers votre emplacement", - "welcome": "Bienvenue {name}" + "gotoSettings": "Paramètres sur OpenStreetMap.org" }, "validation": { "email": { diff --git a/langs/layers/en.json b/langs/layers/en.json index dfa61a2baf..afb4b5bd86 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -8540,6 +8540,24 @@ } } }, + "inbox": { + "mappings": { + "0": { + "then": { + "special": { + "text": "Open your inbox" + } + } + }, + "1": { + "then": { + "special": { + "text": "You have {_unreadMessages}
Open your inbox" + } + } + } + } + }, "picture-license": { "mappings": { "0": { @@ -8617,6 +8635,9 @@ } } } + }, + "title": { + "render": "Settings" } }, "veterinary": { diff --git a/langs/layers/nl.json b/langs/layers/nl.json index 36a594488b..39ce5ca15b 100644 --- a/langs/layers/nl.json +++ b/langs/layers/nl.json @@ -8265,6 +8265,17 @@ } } }, + "inbox": { + "mappings": { + "0": { + "then": { + "special": { + "text": "Ga naar je inbox" + } + } + } + } + }, "picture-license": { "mappings": { "0": { @@ -8314,6 +8325,9 @@ } } } + }, + "title": { + "render": "Instellingen" } }, "veterinary": { diff --git a/langs/nb_NO.json b/langs/nb_NO.json index 127d2c670c..d3b6fc5af7 100644 --- a/langs/nb_NO.json +++ b/langs/nb_NO.json @@ -692,11 +692,8 @@ "userinfo": { "gotoInbox": "Åpne innboksen din", "gotoSettings": "Gå til innstillingene dine på OpenStreetMap.org", - "newMessages": "du har nye meldinger", "noDescription": "Du har ikke noen profilbeskrivelse enda", - "noDescriptionCallToAction": "Legg til profilbeskrivelse", - "titleNotLoggedIn": "Velkommen", - "welcome": "Velkommen {name}" + "noDescriptionCallToAction": "Legg til profilbeskrivelse" }, "validation": { "color": { diff --git a/langs/nl.json b/langs/nl.json index 036573b7d5..cc38655cbd 100644 --- a/langs/nl.json +++ b/langs/nl.json @@ -938,12 +938,8 @@ "userinfo": { "gotoInbox": "Open je inbox", "gotoSettings": "Ga naar je instellingen op OpenStreetMap.org", - "moveToHome": "Beweeg de kaart naar je thuislocatie", - "newMessages": "je hebt nieuwe berichten", "noDescription": "Je hebt nog geen beschrijving op je profiel", - "noDescriptionCallToAction": "Voeg een profielbeschrijving toe", - "titleNotLoggedIn": "Welkom", - "welcome": "Welkom {name}" + "noDescriptionCallToAction": "Voeg een profielbeschrijving toe" }, "validation": { "color": { diff --git a/langs/pl.json b/langs/pl.json index 57869cc9aa..744b857c6a 100644 --- a/langs/pl.json +++ b/langs/pl.json @@ -192,11 +192,8 @@ "userinfo": { "gotoInbox": "Otwórz swoją skrzynkę odbiorczą", "gotoSettings": "Przejdź do swoich ustawień na OpenStreetMap.org", - "moveToHome": "Przesuń mapę do Twojej lokalizacji domowej", - "newMessages": "masz nowe wiadomości", "noDescription": "Nie masz jeszcze opisu w swoim profilu", - "noDescriptionCallToAction": "Dodaj opis profilu", - "welcome": "Witaj {name}" + "noDescriptionCallToAction": "Dodaj opis profilu" }, "validation": { "color": { diff --git a/langs/ru.json b/langs/ru.json index 11d0735eab..35538e08ed 100644 --- a/langs/ru.json +++ b/langs/ru.json @@ -216,10 +216,7 @@ "translations": { "activateButton": "Помогите перевести MapComplete" }, - "userinfo": { - "titleNotLoggedIn": "Добро пожаловать", - "welcome": "Добро пожаловать, {name}" - }, + "userinfo": {}, "validation": { "nat": { "notANumber": "Введите число" diff --git a/test.ts b/test.ts index 488e19a817..6c8590cccf 100644 --- a/test.ts +++ b/test.ts @@ -7,17 +7,24 @@ import ThemeViewState from "./Models/ThemeViewState" import Combine from "./UI/Base/Combine" import SpecialVisualizations from "./UI/SpecialVisualizations" import AddNewPoint from "./UI/Popup/AddNewPoint/AddNewPoint.svelte" +import UserProfile from "./UI/BigComponents/UserProfile.svelte" async function main() { new FixedUiElement("").AttachTo("extradiv") const layout = new LayoutConfig(theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data) - const main = new SvelteUIElement(ThemeViewGUI, { layout }) + const state = new ThemeViewState(layout) + + const main = new SvelteUIElement(ThemeViewGUI, { state }) + state.guistate.menuIsOpened.setData(true) + state.guistate.menuViewTab.setData("settings") main.AttachTo("maindiv") } async function testspecial() { const layout = new LayoutConfig(theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data) const state = new ThemeViewState(layout) + + state.guistate.openUsersettings("picture-license") const all = SpecialVisualizations.specialVisualizations.map((s) => SpecialVisualizations.renderExampleOfSpecial(state, s) ) @@ -27,12 +34,7 @@ async function testspecial() { async function test() { const layout = new LayoutConfig(theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data) const state = new ThemeViewState(layout) - state.featureSwitches.featureSwitchIsTesting.setData(true) - new SvelteUIElement(AddNewPoint, { - state, - coordinate: { lon: 3.22001, lat: 51.21576 }, - }).AttachTo("maindiv") - //*/ + new SvelteUIElement(UserProfile, { osmConnection: state.osmConnection }).AttachTo("maindiv") } /*