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, "", {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<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()
|
|
}
|
|
}
|