forked from MapComplete/MapComplete
Add image support in notes
This commit is contained in:
parent
e8d1d5422e
commit
b15eaff55e
8 changed files with 195 additions and 66 deletions
|
@ -220,8 +220,8 @@ export class OsmConnection {
|
||||||
|
|
||||||
public closeNote(id: number | string, text?: string): Promise<any> {
|
public closeNote(id: number | string, text?: string): Promise<any> {
|
||||||
let textSuffix = ""
|
let textSuffix = ""
|
||||||
if((text ?? "") !== "" ){
|
if ((text ?? "") !== "") {
|
||||||
textSuffix = "?text="+encodeURIComponent(text)
|
textSuffix = "?text=" + encodeURIComponent(text)
|
||||||
}
|
}
|
||||||
return new Promise((ok, error) => {
|
return new Promise((ok, error) => {
|
||||||
this.auth.xhr({
|
this.auth.xhr({
|
||||||
|
@ -241,8 +241,8 @@ export class OsmConnection {
|
||||||
|
|
||||||
public reopenNote(id: number | string, text?: string): Promise<any> {
|
public reopenNote(id: number | string, text?: string): Promise<any> {
|
||||||
let textSuffix = ""
|
let textSuffix = ""
|
||||||
if((text ?? "") !== "" ){
|
if ((text ?? "") !== "") {
|
||||||
textSuffix = "?text="+encodeURIComponent(text)
|
textSuffix = "?text=" + encodeURIComponent(text)
|
||||||
}
|
}
|
||||||
return new Promise((ok, error) => {
|
return new Promise((ok, error) => {
|
||||||
this.auth.xhr({
|
this.auth.xhr({
|
||||||
|
|
26
UI/Popup/LoginButton.ts
Normal file
26
UI/Popup/LoginButton.ts
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
96
UI/Popup/NoteCommentElement.ts
Normal file
96
UI/Popup/NoteCommentElement.ts
Normal file
|
@ -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<BaseUIElement[]>(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<any>, 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 '<a href="' + url + '">' + url + '</a>';
|
||||||
|
})
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -41,7 +41,10 @@ import {OpenIdEditor} from "./BigComponents/CopyrightPanel";
|
||||||
import Toggle from "./Input/Toggle";
|
import Toggle from "./Input/Toggle";
|
||||||
import Img from "./Base/Img";
|
import Img from "./Base/Img";
|
||||||
import ValidatedTextField from "./Input/ValidatedTextField";
|
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 {
|
export interface SpecialVisualization {
|
||||||
funcName: string,
|
funcName: string,
|
||||||
|
@ -693,11 +696,11 @@ export default class SpecialVisualizations {
|
||||||
tags.ping()
|
tags.ping()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return new Toggle(
|
return new LoginToggle( new Toggle(
|
||||||
t.isClosed.SetClass("thanks"),
|
t.isClosed.SetClass("thanks"),
|
||||||
closeButton,
|
closeButton,
|
||||||
isClosed
|
isClosed
|
||||||
)
|
), t.loginToClose, state)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -721,24 +724,17 @@ export default class SpecialVisualizations {
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
const id = tags.data[args[1] ?? "id"]
|
const id = tags.data[args[1] ?? "id"]
|
||||||
|
|
||||||
|
if ((txt.data ?? "") == "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isClosed.data) {
|
if (isClosed.data) {
|
||||||
await state.osmConnection.reopenNote(id, txt.data)
|
await state.osmConnection.reopenNote(id, txt.data)
|
||||||
await state.osmConnection.closeNote(id)
|
await state.osmConnection.closeNote(id)
|
||||||
} else {
|
} else {
|
||||||
await state.osmConnection.addCommentToNode(id, txt.data)
|
await state.osmConnection.addCommentToNode(id, txt.data)
|
||||||
}
|
}
|
||||||
const comments: any[] = JSON.parse(tags.data["comments"])
|
NoteCommentElement.addCommentTo(txt.data, tags, state)
|
||||||
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()
|
|
||||||
txt.setData("")
|
txt.setData("")
|
||||||
|
|
||||||
})
|
})
|
||||||
|
@ -779,13 +775,15 @@ export default class SpecialVisualizations {
|
||||||
})
|
})
|
||||||
|
|
||||||
const isClosed = tags.map(tags => (tags["closed_at"] ?? "") !== "");
|
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([
|
return new LoginToggle(
|
||||||
|
new Combine([
|
||||||
new Title("Add a comment"),
|
new Title("Add a comment"),
|
||||||
textField,
|
textField,
|
||||||
new Combine([addCommentButton.SetClass("mr-2"), stateButtons]).SetClass("flex justify-end")
|
new Combine([addCommentButton.SetClass("mr-2"), stateButtons]).SetClass("flex justify-end")
|
||||||
]).SetClass("border-2 border-black rounded-xl p-4 block");
|
]).SetClass("border-2 border-black rounded-xl p-4 block"),
|
||||||
|
t.loginToAddComment, state)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -798,52 +796,53 @@ export default class SpecialVisualizations {
|
||||||
defaultValue: "comments"
|
defaultValue: "comments"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
, constr: (state, tags, args) => {
|
, constr: (state, tags, args) =>
|
||||||
const t = Translations.t.notes;
|
new VariableUiElement(
|
||||||
return new VariableUiElement(
|
|
||||||
tags.map(tags => tags[args[0]])
|
tags.map(tags => tags[args[0]])
|
||||||
.map(commentsStr => {
|
.map(commentsStr => {
|
||||||
const comments:
|
const comments: any[] = JSON.parse(commentsStr)
|
||||||
{
|
|
||||||
"date": string,
|
|
||||||
"uid": number,
|
|
||||||
"user": string,
|
|
||||||
"user_url": string,
|
|
||||||
"action": "closed" | "opened" | "reopened" | "commented",
|
|
||||||
"text": string, "html": string
|
|
||||||
}[] = JSON.parse(commentsStr)
|
|
||||||
|
|
||||||
|
|
||||||
return new Combine(comments
|
return new Combine(comments
|
||||||
.filter(c => c.text !== "")
|
.filter(c => c.text !== "")
|
||||||
.map(c => {
|
.map(c => new NoteCommentElement(c))).SetClass("flex flex-col")
|
||||||
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")
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
1
Utils.ts
1
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)
|
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
|
public static readonly special_visualizations_importRequirementDocs = `#### Importing a dataset into OpenStreetMap: requirements
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,10 @@
|
||||||
"id": "conversation",
|
"id": "conversation",
|
||||||
"render": "{visualize_note_comments()}"
|
"render": "{visualize_note_comments()}"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "add_image",
|
||||||
|
"render": "{add_image_to_note()}"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "comment",
|
"id": "comment",
|
||||||
"render": "{add_note_comment()}"
|
"render": "{add_note_comment()}"
|
||||||
|
|
|
@ -431,6 +431,9 @@
|
||||||
"closeNote": "Close note",
|
"closeNote": "Close note",
|
||||||
"reopenNote": "Reopen note",
|
"reopenNote": "Reopen note",
|
||||||
"reopenNoteAndComment": "Reopen note and comment",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue