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> {
|
||||
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<any> {
|
||||
let textSuffix = ""
|
||||
if((text ?? "") !== "" ){
|
||||
textSuffix = "?text="+encodeURIComponent(text)
|
||||
if ((text ?? "") !== "") {
|
||||
textSuffix = "?text=" + encodeURIComponent(text)
|
||||
}
|
||||
return new Promise((ok, error) => {
|
||||
this.auth.xhr({
|
||||
|
|
|
@ -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,
|
||||
|
|
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 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)
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
|
|
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)
|
||||
`
|
||||
public static readonly imageExtensions = new Set(["jpg","png","svg","jpeg",".gif"])
|
||||
|
||||
public static readonly special_visualizations_importRequirementDocs = `#### Importing a dataset into OpenStreetMap: requirements
|
||||
|
||||
|
|
|
@ -57,6 +57,10 @@
|
|||
"id": "conversation",
|
||||
"render": "{visualize_note_comments()}"
|
||||
},
|
||||
{
|
||||
"id": "add_image",
|
||||
"render": "{add_image_to_note()}"
|
||||
},
|
||||
{
|
||||
"id": "comment",
|
||||
"render": "{add_note_comment()}"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue