diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index 8971f199a..04d6db059 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -220,8 +220,8 @@ export class OsmConnection { public closeNote(id: number | string, text?: string): Promise { let textSuffix = "" - if((text ?? "") !== "" ){ - textSuffix = "?text="+encodeURIComponent(text) + if ((text ?? "") !== "") { + textSuffix = "?text=" + encodeURIComponent(text) } return new Promise((ok, error) => { this.auth.xhr({ @@ -241,8 +241,8 @@ export class OsmConnection { public reopenNote(id: number | string, text?: string): Promise { let textSuffix = "" - if((text ?? "") !== "" ){ - textSuffix = "?text="+encodeURIComponent(text) + if ((text ?? "") !== "") { + textSuffix = "?text=" + encodeURIComponent(text) } return new Promise((ok, error) => { this.auth.xhr({ diff --git a/Models/Constants.ts b/Models/Constants.ts index 343b9fa45..8b437b79f 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -24,7 +24,7 @@ export default class Constants { */ public static readonly priviliged_layers: string[] = [...Constants.added_by_default, "type_node", "notes", ...Constants.no_include] - + // The user journey states thresholds when a new feature gets unlocked public static userJourney = { moreScreenUnlock: 1, diff --git a/UI/Popup/LoginButton.ts b/UI/Popup/LoginButton.ts new file mode 100644 index 000000000..1ad73f2bb --- /dev/null +++ b/UI/Popup/LoginButton.ts @@ -0,0 +1,26 @@ +import {SubtleButton} from "../Base/SubtleButton"; +import BaseUIElement from "../BaseUIElement"; +import Svg from "../../Svg"; +import {OsmConnection} from "../../Logic/Osm/OsmConnection"; +import Toggle from "../Input/Toggle"; + +export default class LoginButton extends SubtleButton { + + constructor(text: BaseUIElement | string, state: { + osmConnection: OsmConnection + }) { + super(Svg.osm_logo_svg(), text); + this.onClick(() => { + state.osmConnection.AttemptLogin() + }) + } + +} + +export class LoginToggle extends Toggle { + constructor(el, text: BaseUIElement | string, state: { + osmConnection: OsmConnection + }) { + super(el, new LoginButton(text, state), state.osmConnection.isLoggedIn) + } +} \ No newline at end of file diff --git a/UI/Popup/NoteCommentElement.ts b/UI/Popup/NoteCommentElement.ts new file mode 100644 index 000000000..93957a1bd --- /dev/null +++ b/UI/Popup/NoteCommentElement.ts @@ -0,0 +1,96 @@ +import Combine from "../Base/Combine"; +import BaseUIElement from "../BaseUIElement"; +import Svg from "../../Svg"; +import Link from "../Base/Link"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import Translations from "../i18n/Translations"; +import {Utils} from "../../Utils"; +import Img from "../Base/Img"; +import {ImageCarousel} from "../Image/ImageCarousel"; +import {SlideShow} from "../Image/SlideShow"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {OsmConnection} from "../../Logic/Osm/OsmConnection"; + +export default class NoteCommentElement extends Combine { + + + constructor(comment: { + "date": string, + "uid": number, + "user": string, + "user_url": string, + "action": "closed" | "opened" | "reopened" | "commented", + "text": string, "html": string + }) { + const t = Translations.t.notes; + + let actionIcon: BaseUIElement = undefined; + if (comment.action === "opened" || comment.action === "reopened") { + actionIcon = Svg.note_svg() + } else if (comment.action === "closed") { + actionIcon = Svg.resolved_svg() + } else { + actionIcon = Svg.addSmall_svg() + } + + let user: BaseUIElement + if (comment.user === undefined) { + user = t.anonymous + } else { + user = new Link(comment.user, comment.user_url ?? "", true) + } + + + const htmlElement = document.createElement("div") + htmlElement.innerHTML = comment.html + const images = Array.from(htmlElement.getElementsByTagName("a")) + .map(link => link.href) + .filter(link => { + link = link.toLowerCase() + const lastDotIndex = link.lastIndexOf('.') + const extension = link.substring(lastDotIndex + 1, link.length) + return Utils.imageExtensions.has(extension) + }) + let imagesEl: BaseUIElement = undefined; + if (images.length > 0) { + const imageEls = images.map(i => new Img(i) + .SetClass("w-full block") + .SetStyle("min-width: 50px; background: grey;")); + imagesEl = new SlideShow(new UIEventSource(imageEls)) + } + + super([ + new Combine([ + actionIcon.SetClass("mr-4 w-6").SetStyle("flex-shrink: 0"), + new FixedUiElement(comment.html).SetClass("flex flex-col").SetStyle("margin: 0"), + ]).SetClass("flex"), + imagesEl, + new Combine([user.SetClass("mr-2"), comment.date]).SetClass("flex justify-end subtle") + ]) + this.SetClass("flex flex-col") + + } + + public static addCommentTo(txt: string, tags: UIEventSource, state: {osmConnection: OsmConnection}){ + const comments: any[] = JSON.parse(tags.data["comments"]) + const username = state.osmConnection.userDetails.data.name + + var urlRegex = /(https?:\/\/[^\s]+)/g; + const html = txt.replace(urlRegex, function(url) { + return '' + url + ''; + }) + + comments.push({ + "date": new Date().toISOString(), + "uid": state.osmConnection.userDetails.data.uid, + "user": username, + "user_url": "https://www.openstreetmap.org/user/" + username, + "action": "commented", + "text": txt, + "html": html + }) + tags.data["comments"] = JSON.stringify(comments) + tags.ping() + } + +} \ No newline at end of file diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 9a8afa609..1ba95da94 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -41,7 +41,10 @@ import {OpenIdEditor} from "./BigComponents/CopyrightPanel"; import Toggle from "./Input/Toggle"; import Img from "./Base/Img"; import ValidatedTextField from "./Input/ValidatedTextField"; -import Link from "./Base/Link"; +import NoteCommentElement from "./Popup/NoteCommentElement"; +import ImgurUploader from "../Logic/ImageProviders/ImgurUploader"; +import FileSelectorButton from "./Input/FileSelectorButton"; +import {LoginToggle} from "./Popup/LoginButton"; export interface SpecialVisualization { funcName: string, @@ -693,11 +696,11 @@ export default class SpecialVisualizations { tags.ping() }) }) - return new Toggle( + return new LoginToggle( new Toggle( t.isClosed.SetClass("thanks"), closeButton, isClosed - ) + ), t.loginToClose, state) } }, { @@ -721,24 +724,17 @@ export default class SpecialVisualizations { .onClick(async () => { const id = tags.data[args[1] ?? "id"] + if ((txt.data ?? "") == "") { + return; + } + if (isClosed.data) { await state.osmConnection.reopenNote(id, txt.data) await state.osmConnection.closeNote(id) } else { await state.osmConnection.addCommentToNode(id, txt.data) } - const comments: any[] = JSON.parse(tags.data["comments"]) - const username = state.osmConnection.userDetails.data.name - comments.push({ - "date": new Date().toISOString(), - "uid": state.osmConnection.userDetails.data.uid, - "user": username, - "user_url": "https://www.openstreetmap.org/user/" + username, - "action": "commented", - "text": txt.data - }) - tags.data["comments"] = JSON.stringify(comments) - tags.ping() + NoteCommentElement.addCommentTo(txt.data, tags, state) txt.setData("") }) @@ -779,13 +775,15 @@ export default class SpecialVisualizations { }) const isClosed = tags.map(tags => (tags["closed_at"] ?? "") !== ""); - const stateButtons = new Toggle(reopen, close, isClosed) + const stateButtons = new Toggle(new Toggle(reopen, close, isClosed), undefined, state.osmConnection.isLoggedIn) - return new Combine([ - new Title("Add a comment"), - textField, - new Combine([addCommentButton.SetClass("mr-2"), stateButtons]).SetClass("flex justify-end") - ]).SetClass("border-2 border-black rounded-xl p-4 block"); + return new LoginToggle( + new Combine([ + new Title("Add a comment"), + textField, + new Combine([addCommentButton.SetClass("mr-2"), stateButtons]).SetClass("flex justify-end") + ]).SetClass("border-2 border-black rounded-xl p-4 block"), + t.loginToAddComment, state) } }, { @@ -798,52 +796,53 @@ export default class SpecialVisualizations { defaultValue: "comments" } ] - , constr: (state, tags, args) => { - const t = Translations.t.notes; - return new VariableUiElement( + , constr: (state, tags, args) => + new VariableUiElement( tags.map(tags => tags[args[0]]) .map(commentsStr => { - const comments: - { - "date": string, - "uid": number, - "user": string, - "user_url": string, - "action": "closed" | "opened" | "reopened" | "commented", - "text": string, "html": string - }[] = JSON.parse(commentsStr) - - + const comments: any[] = JSON.parse(commentsStr) return new Combine(comments .filter(c => c.text !== "") - .map(c => { - let actionIcon: BaseUIElement = undefined; - if (c.action === "opened" || c.action === "reopened") { - actionIcon = Svg.note_svg() - } else if (c.action === "closed") { - actionIcon = Svg.resolved_svg() - } else { - actionIcon = Svg.addSmall_svg() - } - - let user: BaseUIElement - if (c.user === undefined) { - user = t.anonymous - } else { - user = new Link(c.user, c.user_url ?? "", true) - } - - return new Combine([new Combine([ - actionIcon.SetClass("mr-4 w-6").SetStyle("flex-shrink: 0"), - new FixedUiElement(c.html).SetClass("flex flex-col").SetStyle("margin: 0"), - ]).SetClass("flex"), - new Combine([user.SetClass("mr-2"), c.date]).SetClass("flex justify-end subtle") - ]).SetClass("flex flex-col") - - })).SetClass("flex flex-col") + .map(c => new NoteCommentElement(c))).SetClass("flex flex-col") }) ) + }, + { + funcName: "add_image_to_note", + docs: "Adds an image to a node", + args: [{ + name: "Id-key", + doc: "The property name where the ID of the note to close can be found", + defaultValue: "id" + }], + constr: (state, tags, args) => { + const isUploading = new UIEventSource(false); + const t = Translations.t.notes; + const id = tags.data[args[0] ?? "id"] + + const uploader = new ImgurUploader(url => { + isUploading.setData(false) + state.osmConnection.addCommentToNode(id, url) + NoteCommentElement.addCommentTo(url, tags, state) + + }) + + const label = new Combine([ + Svg.camera_plus_ui().SetClass("block w-12 h-12 p-1 text-4xl "), + "Add image to node. Your image will be published in the public domain." + ]).SetClass("p-2 border-4 border-black rounded-full font-bold h-full align-middle w-full flex justify-center") + + const fileSelector = new FileSelectorButton(label) + fileSelector.GetValue().addCallback(filelist => { + isUploading.setData(true) + uploader.uploadMany("Image for osm.org/note/" + id, "CC0", filelist) + + }) + return new LoginToggle( new Toggle( + Translations.t.image.uploadingPicture.SetClass("alert"), + fileSelector, isUploading), t.loginToAddPicture, state) } + } ] diff --git a/Utils.ts b/Utils.ts index b3d1656f4..949645408 100644 --- a/Utils.ts +++ b/Utils.ts @@ -21,6 +21,7 @@ Remark that the syntax is slightly different then expected; it uses '$' to note Note that these values can be prepare with javascript in the theme by using a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript) ` + public static readonly imageExtensions = new Set(["jpg","png","svg","jpeg",".gif"]) public static readonly special_visualizations_importRequirementDocs = `#### Importing a dataset into OpenStreetMap: requirements diff --git a/assets/themes/notes/notes.json b/assets/themes/notes/notes.json index 880c6a66e..f6ed48a35 100644 --- a/assets/themes/notes/notes.json +++ b/assets/themes/notes/notes.json @@ -57,6 +57,10 @@ "id": "conversation", "render": "{visualize_note_comments()}" }, + { + "id": "add_image", + "render": "{add_image_to_note()}" + }, { "id": "comment", "render": "{add_note_comment()}" diff --git a/langs/en.json b/langs/en.json index a9483281e..0a6e3f216 100644 --- a/langs/en.json +++ b/langs/en.json @@ -431,6 +431,9 @@ "closeNote": "Close note", "reopenNote": "Reopen note", "reopenNoteAndComment": "Reopen note and comment", - "anonymous": "Anonymous user" + "anonymous": "Anonymous user", + "loginToAddComment": "Login to add a comment", + "loginToAddPicture": "Login to add a picture", + "loginToClose": "Login to close this note" } }