forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			374 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			374 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
 | 
						|
import Translations from "../i18n/Translations"
 | 
						|
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
 | 
						|
import Combine from "../Base/Combine"
 | 
						|
import { SubtleButton } from "../Base/SubtleButton"
 | 
						|
import Svg from "../../Svg"
 | 
						|
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 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) {
 | 
						|
        super(
 | 
						|
            osmConnection.userDetails.map((ud) => {
 | 
						|
                if (ud.csCount < Constants.userJourney.importHelperUnlock) {
 | 
						|
                    return undefined
 | 
						|
                }
 | 
						|
                return new Combine([
 | 
						|
                    new SubtleButton(undefined, Translations.t.importHelper.title, {
 | 
						|
                        url: "import_helper.html",
 | 
						|
                    }),
 | 
						|
                    new SubtleButton(Svg.note_svg(), Translations.t.importInspector.title, {
 | 
						|
                        url: "import_viewer.html",
 | 
						|
                    }),
 | 
						|
                ])
 | 
						|
            })
 | 
						|
        )
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
class SingleUserSettingsPanel extends EditableTagRendering {
 | 
						|
    constructor(
 | 
						|
        config: TagRenderingConfig,
 | 
						|
        osmConnection: OsmConnection,
 | 
						|
        amendedPrefs: UIEventSource<any>,
 | 
						|
        userInfoFocusedQuestion?: UIEventSource<string>
 | 
						|
    ) {
 | 
						|
        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, "", "").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<Record<string, BaseUIElement>>
 | 
						|
    private readonly userInfoFocusedQuestion?: UIEventSource<string>
 | 
						|
 | 
						|
    constructor(
 | 
						|
        osmConnection: OsmConnection,
 | 
						|
        locationControl: UIEventSource<Loc>,
 | 
						|
        layout: LayoutConfig,
 | 
						|
        isOpened: UIEventSource<boolean>,
 | 
						|
        userInfoFocusedQuestion?: UIEventSource<string>
 | 
						|
    ) {
 | 
						|
        const t = Translations.t.userinfo
 | 
						|
        const imgSize = "h-6 w-6"
 | 
						|
        const ud = osmConnection.userDetails
 | 
						|
        const settings = new UIEventSource<Record<string, BaseUIElement>>({})
 | 
						|
        const usersettingsConfig = new LayerConfig(usersettings, "userinformationpanel")
 | 
						|
 | 
						|
        const amendedPrefs = new UIEventSource<any>({ _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")
 | 
						|
        this.settings = settings
 | 
						|
        this.userInfoFocusedQuestion = userInfoFocusedQuestion
 | 
						|
        const self = this
 | 
						|
        userInfoFocusedQuestion.addCallbackD((_) => {
 | 
						|
            self.focusOnSelectedQuestion()
 | 
						|
        })
 | 
						|
    }
 | 
						|
 | 
						|
    public focusOnSelectedQuestion() {
 | 
						|
        const focusedId = this.userInfoFocusedQuestion.data
 | 
						|
        console.log("Focusing on", focusedId, this.settings.data[focusedId])
 | 
						|
        if (focusedId === undefined) {
 | 
						|
            return
 | 
						|
        }
 | 
						|
        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<Loc>
 | 
						|
            readonly featureSwitchUserbadge: Store<boolean>
 | 
						|
        },
 | 
						|
        options?: {
 | 
						|
            isOpened?: UIEventSource<boolean>
 | 
						|
            userInfoFocusedQuestion?: UIEventSource<string>
 | 
						|
        }
 | 
						|
    ) {
 | 
						|
        const isOpened = options?.isOpened ?? new UIEventSource<boolean>(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()
 | 
						|
    }
 | 
						|
}
 |