Add switches to enable some more privacy, fix all errors in osmAuth

This commit is contained in:
Pieter Vander Vennet 2024-05-06 18:58:19 +02:00
parent a856d8edc9
commit fbf23b6e18
6 changed files with 128 additions and 132 deletions

View file

@ -484,6 +484,41 @@
} }
] ]
}, },
{
"id": "more_privacy_theme_override",
"mappings": [
{
"if": "__featureSwitchMorePrivacy=true",
"then": {
"en": "This theme is sensitive. Making changes will not indicate if you were nearby explicitly."
}
}
]
},
{
"id": "more_privacy",
"question":
{
"en": "When making changes, should a rough indication be given how far away you were from the object?"
},
"questionHint": {
"en": "If you make a change to one or more objects and you enabled your location, a rough indication of where you made will be saved: it is indicated if you were closer then 25m, 500m, 5km or further away then 5km. This helps mappers understand your context when making changes, but gives an indication of where you were at this time. "
},
"mappings": [
{
"if": "mapcomplete-more_privacy=yes",
"then": {
"en": "When making changes to OpenStreetMap, do not indicate how far away you were from the changed objects."
}
},
{
"if": "mapcomplete-more_privacy=no",
"then": {
"en": "When making changes to OpenStreetMap, roughly indicate how far away you were from the changed objects. This helps other contributors to understand how you made the change"
}
}
]
},
{ {
"id": "mangrove-keys", "id": "mangrove-keys",
"render": { "render": {

29
package-lock.json generated
View file

@ -55,7 +55,7 @@
"monaco-editor": "^0.46.0", "monaco-editor": "^0.46.0",
"npm": "^10.7.0", "npm": "^10.7.0",
"opening_hours": "^3.6.0", "opening_hours": "^3.6.0",
"osm-auth": "^2.2.0", "osm-auth": "^2.5.0",
"osmtogeojson": "^3.0.0-beta.5", "osmtogeojson": "^3.0.0-beta.5",
"panzoom": "^9.4.3", "panzoom": "^9.4.3",
"papaparse": "^5.3.1", "papaparse": "^5.3.1",
@ -14631,13 +14631,11 @@
} }
}, },
"node_modules/osm-auth": { "node_modules/osm-auth": {
"version": "2.2.0", "version": "2.5.0",
"license": "ISC", "resolved": "https://registry.npmjs.org/osm-auth/-/osm-auth-2.5.0.tgz",
"dependencies": { "integrity": "sha512-w3NnYbt+0PIih2Kwr1sLfQWehdLbcA3gZNJhX4VOBfeRtvm30iZA3nURphuZDokZ8Kmdv4LWB+AiIng2b+KvIA==",
"store": "~2.0.12"
},
"engines": { "engines": {
"node": ">=16" "node": ">=18.18"
} }
}, },
"node_modules/osm-polygon-features": { "node_modules/osm-polygon-features": {
@ -17081,13 +17079,6 @@
"version": "3.3.2", "version": "3.3.2",
"license": "MIT" "license": "MIT"
}, },
"node_modules/store": {
"version": "2.0.12",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/stream-to-string": { "node_modules/stream-to-string": {
"version": "1.2.1", "version": "1.2.1",
"license": "MIT", "license": "MIT",
@ -29202,10 +29193,9 @@
} }
}, },
"osm-auth": { "osm-auth": {
"version": "2.2.0", "version": "2.5.0",
"requires": { "resolved": "https://registry.npmjs.org/osm-auth/-/osm-auth-2.5.0.tgz",
"store": "~2.0.12" "integrity": "sha512-w3NnYbt+0PIih2Kwr1sLfQWehdLbcA3gZNJhX4VOBfeRtvm30iZA3nURphuZDokZ8Kmdv4LWB+AiIng2b+KvIA=="
}
}, },
"osm-polygon-features": { "osm-polygon-features": {
"version": "0.9.2" "version": "0.9.2"
@ -30803,9 +30793,6 @@
"std-env": { "std-env": {
"version": "3.3.2" "version": "3.3.2"
}, },
"store": {
"version": "2.0.12"
},
"stream-to-string": { "stream-to-string": {
"version": "1.2.1", "version": "1.2.1",
"requires": { "requires": {

View file

@ -173,7 +173,7 @@
"monaco-editor": "^0.46.0", "monaco-editor": "^0.46.0",
"npm": "^10.7.0", "npm": "^10.7.0",
"opening_hours": "^3.6.0", "opening_hours": "^3.6.0",
"osm-auth": "^2.2.0", "osm-auth": "^2.5.0",
"osmtogeojson": "^3.0.0-beta.5", "osmtogeojson": "^3.0.0-beta.5",
"panzoom": "^9.4.3", "panzoom": "^9.4.3",
"papaparse": "^5.3.1", "papaparse": "^5.3.1",

View file

@ -1,4 +1,3 @@
// @ts-ignore
import { osmAuth } from "osm-auth" import { osmAuth } from "osm-auth"
import { Store, Stores, UIEventSource } from "../UIEventSource" import { Store, Stores, UIEventSource } from "../UIEventSource"
import { OsmPreferences } from "./OsmPreferences" import { OsmPreferences } from "./OsmPreferences"
@ -6,7 +5,18 @@ import { Utils } from "../../Utils"
import { LocalStorageSource } from "../Web/LocalStorageSource" import { LocalStorageSource } from "../Web/LocalStorageSource"
import { AuthConfig } from "./AuthConfig" import { AuthConfig } from "./AuthConfig"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import OSMAuthInstance = OSMAuth.OSMAuthInstance
interface OsmUserInfo {
id: number
display_name: string
account_created: string
description: string
contributor_terms: { agreed: boolean }
roles: []
changesets: { count: number }
traces: { count: number }
blocks: { received: { count: number; active: number } }
}
export default class UserDetails { export default class UserDetails {
public loggedIn = false public loggedIn = false
@ -31,7 +41,7 @@ export default class UserDetails {
export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable" export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable"
export class OsmConnection { export class OsmConnection {
public auth: OSMAuthInstance public auth: osmAuth
public userDetails: UIEventSource<UserDetails> public userDetails: UIEventSource<UserDetails>
public isLoggedIn: Store<boolean> public isLoggedIn: Store<boolean>
public gpxServiceIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>( public gpxServiceIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
@ -49,7 +59,7 @@ export class OsmConnection {
private readonly _dryRun: Store<boolean> private readonly _dryRun: Store<boolean>
private readonly fakeUser: boolean private readonly fakeUser: boolean
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [] private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []
private readonly _iframeMode: Boolean | boolean private readonly _iframeMode: boolean
private readonly _singlePage: boolean private readonly _singlePage: boolean
private isChecking = false private isChecking = false
private readonly _doCheckRegularly private readonly _doCheckRegularly
@ -99,20 +109,19 @@ export class OsmConnection {
ud.languages = ["en"] ud.languages = ["en"]
this.loadingStatus.setData("logged-in") this.loadingStatus.setData("logged-in")
} }
const self = this
this.UpdateCapabilities() this.UpdateCapabilities()
this.isLoggedIn = this.userDetails.map( this.isLoggedIn = this.userDetails.map(
(user) => (user) =>
user.loggedIn && user.loggedIn &&
(self.apiIsOnline.data === "unknown" || self.apiIsOnline.data === "online"), (this.apiIsOnline.data === "unknown" || this.apiIsOnline.data === "online"),
[this.apiIsOnline] [this.apiIsOnline]
) )
this.isLoggedIn.addCallback((isLoggedIn) => { this.isLoggedIn.addCallback((isLoggedIn) => {
if (self.userDetails.data.loggedIn == false && isLoggedIn == true) { if (this.userDetails.data.loggedIn == false && isLoggedIn == true) {
// We have an inconsistency: the userdetails say we _didn't_ log in, but this actor says we do // We have an inconsistency: the userdetails say we _didn't_ log in, but this actor says we do
// This means someone attempted to toggle this; so we attempt to login! // This means someone attempted to toggle this; so we attempt to login!
self.AttemptLogin() this.AttemptLogin()
} }
}) })
@ -120,17 +129,16 @@ export class OsmConnection {
this.updateAuthObject() this.updateAuthObject()
if (!this.fakeUser) { if (!this.fakeUser) {
self.CheckForMessagesContinuously() this.CheckForMessagesContinuously()
} }
this.preferencesHandler = new OsmPreferences(this.auth, this, this.fakeUser) this.preferencesHandler = new OsmPreferences(this.auth, this, this.fakeUser)
if (options.oauth_token?.data !== undefined) { if (options.oauth_token?.data !== undefined) {
console.log(options.oauth_token.data) console.log(options.oauth_token.data)
const self = this
this.auth.bootstrapToken(options.oauth_token.data, (err, result) => { this.auth.bootstrapToken(options.oauth_token.data, (err, result) => {
console.log("Bootstrap token called back", err, result) console.log("Bootstrap token called back", err, result)
self.AttemptLogin() this.AttemptLogin()
}) })
options.oauth_token.setData(undefined) options.oauth_token.setData(undefined)
@ -142,15 +150,15 @@ export class OsmConnection {
} }
} }
public GetPreference( public GetPreference<T extends string = string>(
key: string, key: string,
defaultValue: string = undefined, defaultValue: string = undefined,
options?: { options?: {
documentation?: string documentation?: string
prefix?: string prefix?: string
} }
): UIEventSource<string> { ): UIEventSource<T | undefined> {
return this.preferencesHandler.GetPreference(key, defaultValue, options) return <UIEventSource<T>>this.preferencesHandler.GetPreference(key, defaultValue, options)
} }
public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> { public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
@ -192,7 +200,7 @@ export class OsmConnection {
console.log("AttemptLogin called, but ignored as fakeUser is set") console.log("AttemptLogin called, but ignored as fakeUser is set")
return return
} }
const self = this
console.log("Trying to log in...") console.log("Trying to log in...")
this.updateAuthObject() this.updateAuthObject()
@ -202,33 +210,33 @@ export class OsmConnection {
this.auth.xhr( this.auth.xhr(
{ {
method: "GET", method: "GET",
path: "/api/0.6/user/details", path: "/api/0.6/user/details"
}, },
function (err, details: XMLDocument) { (err, details: XMLDocument) => {
if (err != null) { if (err != null) {
console.log("Could not login due to:", err) console.log("Could not login due to:", err)
self.loadingStatus.setData("error") this.loadingStatus.setData("error")
if (err.status == 401) { if (err.status == 401) {
console.log("Clearing tokens...") console.log("Clearing tokens...")
// Not authorized - our token probably got revoked // Not authorized - our token probably got revoked
self.auth.logout() this.auth.logout()
self.LogOut() this.LogOut()
} else { } else {
console.log("Other error. Status:", err.status) console.log("Other error. Status:", err.status)
self.apiIsOnline.setData("unreachable") this.apiIsOnline.setData("unreachable")
} }
return return
} }
if (details == null) { if (details == null) {
self.loadingStatus.setData("error") this.loadingStatus.setData("error")
return return
} }
// details is an XML DOM of user details // details is an XML DOM of user details
let userInfo = details.getElementsByTagName("user")[0] const userInfo = details.getElementsByTagName("user")[0]
let data = self.userDetails.data const data = this.userDetails.data
data.loggedIn = true data.loggedIn = true
console.log("Login completed, userinfo is ", userInfo) console.log("Login completed, userinfo is ", userInfo)
data.name = userInfo.getAttribute("display_name") data.name = userInfo.getAttribute("display_name")
@ -261,18 +269,18 @@ export class OsmConnection {
data.home = { lat: lat, lon: lon } data.home = { lat: lat, lon: lon }
} }
self.loadingStatus.setData("logged-in") this.loadingStatus.setData("logged-in")
const messages = userInfo const messages = userInfo
.getElementsByTagName("messages")[0] .getElementsByTagName("messages")[0]
.getElementsByTagName("received")[0] .getElementsByTagName("received")[0]
data.unreadMessages = parseInt(messages.getAttribute("unread")) data.unreadMessages = parseInt(messages.getAttribute("unread"))
data.totalMessages = parseInt(messages.getAttribute("count")) data.totalMessages = parseInt(messages.getAttribute("count"))
self.userDetails.ping() this.userDetails.ping()
for (const action of self._onLoggedIn) { for (const action of this._onLoggedIn) {
action(self.userDetails.data) action(this.userDetails.data)
} }
self._onLoggedIn = [] this._onLoggedIn = []
} }
) )
} }
@ -289,11 +297,11 @@ export class OsmConnection {
public async interact( public async interact(
path: string, path: string,
method: "GET" | "POST" | "PUT" | "DELETE", method: "GET" | "POST" | "PUT" | "DELETE",
header?: Record<string, string | number>, header?: Record<string, string>,
content?: string, content?: string,
allowAnonymous: boolean = false allowAnonymous: boolean = false
): Promise<string> { ): Promise<string> {
let connection: OSMAuthInstance = this.auth const connection: osmAuth = this.auth
if (allowAnonymous && !this.auth.authenticated()) { if (allowAnonymous && !this.auth.authenticated()) {
const possibleResult = await Utils.downloadAdvanced( const possibleResult = await Utils.downloadAdvanced(
`${this.Backend()}/api/0.6/${path}`, `${this.Backend()}/api/0.6/${path}`,
@ -310,15 +318,13 @@ export class OsmConnection {
return new Promise((ok, error) => { return new Promise((ok, error) => {
connection.xhr( connection.xhr(
<any>{ {
method, method,
options: { headers: header,
header,
},
content, content,
path: `/api/0.6/${path}`, path: `/api/0.6/${path}`
}, },
function (err, response) { function(err, response) {
if (err !== null) { if (err !== null) {
error(err) error(err)
} else { } else {
@ -329,32 +335,32 @@ export class OsmConnection {
}) })
} }
public async post( public async post<T extends string>(
path: string, path: string,
content?: string, content?: string,
header?: Record<string, string | number>, header?: Record<string, string>,
allowAnonymous: boolean = false allowAnonymous: boolean = false
): Promise<any> { ): Promise<T> {
return await this.interact(path, "POST", header, content, allowAnonymous) return <T> await this.interact(path, "POST", header, content, allowAnonymous)
} }
public async put( public async put<T extends string>(
path: string, path: string,
content?: string, content?: string,
header?: Record<string, string | number> header?: Record<string, string>
): Promise<any> { ): Promise<T> {
return await this.interact(path, "PUT", header, content) return <T> await this.interact(path, "PUT", header, content)
} }
public async get( public async get(
path: string, path: string,
header?: Record<string, string | number>, header?: Record<string, string>,
allowAnonymous: boolean = false allowAnonymous: boolean = false
): Promise<string> { ): Promise<string> {
return await this.interact(path, "GET", header, undefined, allowAnonymous) return await this.interact(path, "GET", header, undefined, allowAnonymous)
} }
public closeNote(id: number | string, text?: string): Promise<void> { public closeNote(id: number | string, text?: string): Promise<string> {
let textSuffix = "" let textSuffix = ""
if ((text ?? "") !== "") { if ((text ?? "") !== "") {
textSuffix = "?text=" + encodeURIComponent(text) textSuffix = "?text=" + encodeURIComponent(text)
@ -362,17 +368,17 @@ export class OsmConnection {
if (this._dryRun.data) { if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text) console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text)
return new Promise((ok) => { return new Promise((ok) => {
ok() ok("")
}) })
} }
return this.post(`notes/${id}/close${textSuffix}`) return this.post(`notes/${id}/close${textSuffix}`)
} }
public reopenNote(id: number | string, text?: string): Promise<void> { public reopenNote(id: number | string, text?: string): Promise<string> {
if (this._dryRun.data) { if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text) console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text)
return new Promise((ok) => { return new Promise(resolve => {
ok() resolve("")
}) })
} }
let textSuffix = "" let textSuffix = ""
@ -398,7 +404,7 @@ export class OsmConnection {
"notes.json", "notes.json",
content, content,
{ {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
}, },
true true
) )
@ -439,7 +445,7 @@ export class OsmConnection {
file: gpx, file: gpx,
description: options.description, description: options.description,
tags: options.labels?.join(",") ?? "", tags: options.labels?.join(",") ?? "",
visibility: options.visibility, visibility: options.visibility
} }
if (!contents.description) { if (!contents.description) {
@ -447,9 +453,9 @@ export class OsmConnection {
} }
const extras = { const extras = {
file: file:
'; filename="' + "; filename=\"" +
(options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) + (options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) +
'"\r\nContent-Type: application/gpx+xml', "\"\r\nContent-Type: application/gpx+xml"
} }
const boundary = "987654" const boundary = "987654"
@ -457,7 +463,7 @@ export class OsmConnection {
let body = "" let body = ""
for (const key in contents) { for (const key in contents) {
body += "--" + boundary + "\r\n" body += "--" + boundary + "\r\n"
body += 'Content-Disposition: form-data; name="' + key + '"' body += "Content-Disposition: form-data; name=\"" + key + "\""
if (extras[key] !== undefined) { if (extras[key] !== undefined) {
body += extras[key] body += extras[key]
} }
@ -468,7 +474,7 @@ export class OsmConnection {
const response = await this.post("gpx/create", body, { const response = await this.post("gpx/create", body, {
"Content-Type": "multipart/form-data; boundary=" + boundary, "Content-Type": "multipart/form-data; boundary=" + boundary,
"Content-Length": body.length, "Content-Length": ""+body.length
}) })
const parsed = JSON.parse(response) const parsed = JSON.parse(response)
console.log("Uploaded GPX track", parsed) console.log("Uploaded GPX track", parsed)
@ -491,9 +497,9 @@ export class OsmConnection {
{ {
method: "POST", method: "POST",
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`, path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`
}, },
function (err, _) { function(err) {
if (err !== null) { if (err !== null) {
error(err) error(err)
} else { } else {
@ -508,7 +514,7 @@ export class OsmConnection {
* To be called by land.html * To be called by land.html
*/ */
public finishLogin(callback: (previousURL: string) => void) { public finishLogin(callback: (previousURL: string) => void) {
this.auth.authenticate(function () { this.auth.authenticate(function() {
// Fully authed at this point // Fully authed at this point
console.log("Authentication successful!") console.log("Authentication successful!")
const previousLocation = LocalStorageSource.Get("location_before_login") const previousLocation = LocalStorageSource.Get("location_before_login")
@ -517,28 +523,6 @@ export class OsmConnection {
} }
private updateAuthObject() { private updateAuthObject() {
let pwaStandAloneMode = false
try {
if (Utils.runningFromConsole) {
pwaStandAloneMode = true
} else if (
window.matchMedia("(display-mode: standalone)").matches ||
window.matchMedia("(display-mode: fullscreen)").matches
) {
pwaStandAloneMode = true
}
} catch (e) {
console.warn(
"Detecting standalone mode failed",
e,
". Assuming in browser and not worrying furhter"
)
}
const standalone = this._iframeMode || pwaStandAloneMode || !this._singlePage
// In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway...
// Same for an iframe...
this.auth = new osmAuth({ this.auth = new osmAuth({
client_id: this._oauth_config.oauth_client_id, client_id: this._oauth_config.oauth_client_id,
url: this._oauth_config.url, url: this._oauth_config.url,
@ -546,23 +530,22 @@ export class OsmConnection {
redirect_uri: Utils.runningFromConsole redirect_uri: Utils.runningFromConsole
? "https://mapcomplete.org/land.html" ? "https://mapcomplete.org/land.html"
: window.location.protocol + "//" + window.location.host + "/land.html", : window.location.protocol + "//" + window.location.host + "/land.html",
singlepage: true, singlepage: true, // We always use 'singlePage', it is the most stable - including in PWA
auto: true, auto: true
}) })
} }
private CheckForMessagesContinuously() { private CheckForMessagesContinuously() {
const self = this
if (this.isChecking) { if (this.isChecking) {
return return
} }
Stores.Chronic(3 * 1000).addCallback((_) => { Stores.Chronic(3 * 1000).addCallback(() => {
if (!(self.apiIsOnline.data === "unreachable" || self.apiIsOnline.data === "offline")) { if (!(this.apiIsOnline.data === "unreachable" || this.apiIsOnline.data === "offline")) {
return return
} }
try { try {
console.log("Api is offline - trying to reconnect...") console.log("Api is offline - trying to reconnect...")
self.AttemptLogin() this.AttemptLogin()
} catch (e) { } catch (e) {
console.log("Could not login due to", e) console.log("Could not login due to", e)
} }
@ -571,10 +554,10 @@ export class OsmConnection {
if (!this._doCheckRegularly) { if (!this._doCheckRegularly) {
return return
} }
Stores.Chronic(60 * 5 * 1000).addCallback((_) => { Stores.Chronic(60 * 5 * 1000).addCallback(() => {
if (self.isLoggedIn.data) { if (this.isLoggedIn.data) {
try { try {
self.AttemptLogin() this.AttemptLogin()
} catch (e) { } catch (e) {
console.log("Could not login due to", e) console.log("Could not login due to", e)
} }
@ -592,19 +575,9 @@ export class OsmConnection {
}) })
} }
private readonly _userInfoCache: Record<number, any> = {} private readonly _userInfoCache: Record<number, OsmUserInfo> = {}
public async getInformationAboutUser(id: number): Promise<{ public async getInformationAboutUser(id: number): Promise<OsmUserInfo> {
id: number
display_name: string
account_created: string
description: string
contributor_terms: { agreed: boolean }
roles: []
changesets: { count: number }
traces: { count: number }
blocks: { received: { count: number; active: number } }
}> {
if (id === undefined) { if (id === undefined) {
return undefined return undefined
} }

View file

@ -43,6 +43,7 @@ export default class UserRelatedState {
public readonly fixateNorth: UIEventSource<undefined | "yes"> public readonly fixateNorth: UIEventSource<undefined | "yes">
public readonly a11y: UIEventSource<undefined | "always" | "never" | "default"> public readonly a11y: UIEventSource<undefined | "always" | "never" | "default">
public readonly homeLocation: FeatureSource public readonly homeLocation: FeatureSource
public readonly morePrivacy: UIEventSource<undefined | "yes" | "no">
/** /**
* The language as saved into the preferences of the user, if logged in. * The language as saved into the preferences of the user, if logged in.
* Note that this is _different_ from the languages a user can set via the osm.org interface here: https://www.openstreetmap.org/preferences * Note that this is _different_ from the languages a user can set via the osm.org interface here: https://www.openstreetmap.org/preferences
@ -106,12 +107,12 @@ export default class UserRelatedState {
}) })
) )
this.language = this.osmConnection.GetPreference("language") this.language = this.osmConnection.GetPreference("language")
this.showTags = <UIEventSource<any>>this.osmConnection.GetPreference("show_tags") this.showTags = this.osmConnection.GetPreference("show_tags")
this.showCrosshair = <UIEventSource<any>>this.osmConnection.GetPreference("show_crosshair") this.showCrosshair = this.osmConnection.GetPreference("show_crosshair")
this.fixateNorth = <UIEventSource<"yes">>this.osmConnection.GetPreference("fixate-north") this.fixateNorth = this.osmConnection.GetPreference("fixate-north")
this.a11y = <UIEventSource<"always" | "never" | "default">>( this.morePrivacy = this.osmConnection.GetPreference("more_privacy", "no")
this.osmConnection.GetPreference("a11y")
) this.a11y = this.osmConnection.GetPreference("a11y")
this.mangroveIdentity = new MangroveIdentity( this.mangroveIdentity = new MangroveIdentity(
this.osmConnection.GetLongPreference("identity", "mangrove"), this.osmConnection.GetLongPreference("identity", "mangrove"),

View file

@ -564,5 +564,5 @@ export interface LayerConfigJson {
* ifunset: Write 'change_within_x_m' as usual and if GPS is enabled * ifunset: Write 'change_within_x_m' as usual and if GPS is enabled
* iftrue: Do not write 'change_within_x_m' and do not indicate that this was done by survey * iftrue: Do not write 'change_within_x_m' and do not indicate that this was done by survey
*/ */
enableMorePrivacy: boolean enableMorePrivacy?: boolean
} }