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…
	
	Add table
		Add a link
		
	
		Reference in a new issue