forked from MapComplete/MapComplete
		
	UX: add proper error message when loading an image fails + add detection for strict tracking protection to show an error message for that; fix #2408
This commit is contained in:
		
							parent
							
								
									6f66556cf3
								
							
						
					
					
						commit
						45c0f1a8d6
					
				
					 6 changed files with 77 additions and 13 deletions
				
			
		|  | @ -561,6 +561,8 @@ | |||
|         "addPicture": "Take a picture", | ||||
|         "doDelete": "Remove image", | ||||
|         "isDeleted": "Deleted", | ||||
|         "loadingFailed": "Loading this image failed", | ||||
|         "mapillaryTrackingProtection": "Strict tracking protection blocks loading images from Mapillary, as Mapillary is owned by Facebook/Meta. Disable strict tracking protection if you want to see this image.", | ||||
|         "nearby": { | ||||
|             "close": "Collapse panel with nearby images", | ||||
|             "failed": "Fetching images from {service} failed", | ||||
|  | @ -590,8 +592,11 @@ | |||
|         }, | ||||
|         "pleaseLogin": "Please log in to add a picture", | ||||
|         "processing": "The server is processing your image", | ||||
|         "reported": "This image is reported and might contain harmful content", | ||||
|         "respectPrivacy": "Do not upload from Google Maps, Google Streetview or other copyrighted sources.", | ||||
|         "selectFile": "Select a picture from your device", | ||||
|         "showAnyway": "Show picture anyway", | ||||
|         "strictProtectionDetected": "Strict tracking protection (or another content blocker) was detected and might have blocked access to this image.", | ||||
|         "toBig": "Your image is too large as it is {actual_size}. Please use images of at most {max_size}", | ||||
|         "unlink": { | ||||
|             "button": "Unlink picture", | ||||
|  |  | |||
|  | @ -5513,7 +5513,7 @@ h2.group { | |||
|   border-radius: 1em; | ||||
|   margin: 0.25em; | ||||
|   text-align: center; | ||||
|   padding: 0.15em 0.3em; | ||||
|   padding: 0.15em 0.7em; | ||||
|   border: 2px dotted #ff9143; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import Constants from "../../Models/Constants" | |||
| import SvelteUIElement from "../../UI/Base/SvelteUIElement" | ||||
| import MapillaryIcon from "./MapillaryIcon.svelte" | ||||
| import { Feature, Point } from "geojson" | ||||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| 
 | ||||
| export class Mapillary extends ImageProvider { | ||||
|     public static readonly singleton = new Mapillary() | ||||
|  | @ -257,4 +258,34 @@ export class Mapillary extends ImageProvider { | |||
|         } | ||||
|         return Mapillary.createLink(location, 17, image.id) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Returns true if we are in firefox strict mode (or if we are offline) | ||||
|      * @private | ||||
|      */ | ||||
|     private static async checkStrictMode(): Promise<boolean> { | ||||
|         try { | ||||
|             const result = await fetch("https://scontent-bru2-1.xx.fbcdn.net/m1/v/t6/Xn8-ISUUYQyBD9FyACzPFRGZnBJRqIFmnQ_yd7FU6vxFYwD21fvAcZwDQoMzsScxcQyCWeBviKpWO4nX8yf--neJDvVjC4JlQtfBYb6TrpXQTniyafSFeZeePT_NVx3H6gMjceEvXHyvBqOOcCB_xQ?stp=c2048.2048.2000.988a_s1000x1000&_nc_gid=E2oHnrAtHutVvjaIm9qDLg&_nc_oc=AdkcScR9HuKt1X_K5-GrUeR5Paj8d7MsNFFYEBSmgc0IiBey_wS3RiNJpflWIKaQzNE&ccb=10-5&oh=00_AfNJ1Ki1IeGdUMxdFUc3ZX9VYIVFxVfXZ9MUATU3vj_RJw&oe=686AF002&_nc_sid=201bca") | ||||
|             console.log("Not blocked, got a forbidden", result.status) | ||||
|             return false | ||||
|         } catch (e) { | ||||
|             console.log("Mapillary blocked - probably a scriptblocker") | ||||
|             return true | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static _isInStrictMode: UIEventSource<boolean> = undefined | ||||
| 
 | ||||
|     /** | ||||
|      * Creates a store which contains true if strict tracking protection is probably enabled. | ||||
|      * This will attempt to fetch a bit of content from mapillary - as that is probably the main culprit | ||||
|      * Loads lazy, so will only attempt to fetch the _first time_ this method is called | ||||
|      */ | ||||
|     public static isInStrictMode(): Store<boolean> { | ||||
|         if (this._isInStrictMode === undefined) { | ||||
|             this._isInStrictMode = UIEventSource.FromPromise(this.checkStrictMode()) | ||||
|         } | ||||
|         return this._isInStrictMode | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| </script> | ||||
| 
 | ||||
| <div class="relative h-80 w-60"> | ||||
|   <div class="h-full w-full animate-pulse bg-gray-400" /> | ||||
|   <div class="h-full w-full animate-pulse interactive" /> | ||||
|   <div class="absolute top-0 flex h-full w-full items-center justify-center"> | ||||
|     <Loading /> | ||||
|   </div> | ||||
|  |  | |||
|  | @ -23,6 +23,7 @@ | |||
|   import ThemeViewState from "../../Models/ThemeViewState" | ||||
|   import Panorama360 from "../../assets/svg/Panorama360.svelte" | ||||
|   import { ExternalLinkIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import { ExclamationTriangle as TriangleOutline } from "@babeard/svelte-heroicons/outline/ExclamationTriangle" | ||||
| 
 | ||||
|   export let image: Partial<ProvidedImage> & { id: string; url: string } | ||||
|   let fallbackImage: string = undefined | ||||
|  | @ -41,21 +42,32 @@ | |||
|     | Store<Feature<Geometry, HotspotProperties>[]> = [] | ||||
| 
 | ||||
|   let loaded = false | ||||
|   let error = false | ||||
|   let ignoreHidden = false | ||||
|   let isInStrictMode = new UIEventSource(false) | ||||
| 
 | ||||
|   function onError() { | ||||
|     error = true | ||||
|     Mapillary.isInStrictMode().addCallbackAndRunD(isStrict => { | ||||
|       isInStrictMode.set(isStrict) | ||||
|       return true // unregister | ||||
|     }) | ||||
|   } | ||||
|   let visitUrl = image.provider?.visitUrl(image) | ||||
|   let showBigPreview = new UIEventSource(false) | ||||
|   onDestroy( | ||||
|     showBigPreview.addCallbackAndRun((shown) => { | ||||
|       state?.guistate?.setPreviewedImage(shown ? image : undefined) | ||||
|     }) | ||||
|     }), | ||||
|   ) | ||||
|   if (previewedImage) { | ||||
|     onDestroy( | ||||
|       previewedImage.addCallbackAndRun((previewedImage) => { | ||||
|         showBigPreview.set( | ||||
|           previewedImage !== undefined && | ||||
|             (previewedImage?.id ?? previewedImage?.url) === (image.id ?? image.url) | ||||
|           (previewedImage?.id ?? previewedImage?.url) === (image.id ?? image.url), | ||||
|         ) | ||||
|       }) | ||||
|       }), | ||||
|     ) | ||||
|   } | ||||
| 
 | ||||
|  | @ -98,14 +110,26 @@ | |||
|     /> | ||||
|   </div> | ||||
| </Popup> | ||||
| 
 | ||||
| {#if image.status !== undefined && image.status !== "ready" && image.status !== "hidden"} | ||||
| {#if error} | ||||
|   <div class="h-80 w-60 interactive flex flex-col justify-center items-center p-4 text-center"> | ||||
|     <div class="alert flex items-center"> | ||||
|       <TriangleOutline class="shrink-0 h-8 w-8" /> | ||||
|       <Tr t={Translations.t.image.loadingFailed}/> | ||||
|     </div> | ||||
|     {#if image.provider.name.toLowerCase() === "mapillary" && $isInStrictMode} | ||||
|       <Tr t={Translations.t.image.mapillaryTrackingProtection}/> | ||||
|     {:else if $isInStrictMode} | ||||
|       <Tr t={Translations.t.image.strictProtectionDetected}/> | ||||
|       <div class="subtle text-sm mt-8">{image.url}</div> | ||||
|     {/if} | ||||
|   </div> | ||||
| {:else if image.status !== undefined && image.status !== "ready" && image.status !== "hidden"} | ||||
|   <div class="flex h-full flex-col justify-center p-4"> | ||||
|     <Loading> | ||||
|       <Tr t={Translations.t.image.processing} /> | ||||
|     </Loading> | ||||
|   </div> | ||||
| {:else if image.status !== "hidden"} | ||||
| {:else if image.status !== "hidden" || ignoreHidden} | ||||
|   <div class="relative shrink-0"> | ||||
|     <div | ||||
|       class={"relative w-fit"} | ||||
|  | @ -136,9 +160,7 @@ | |||
|           previewedImage?.set(image) | ||||
|         }} | ||||
|         on:error={() => { | ||||
|           if (fallbackImage) { | ||||
|             imgEl.src = fallbackImage | ||||
|           } | ||||
|           onError() | ||||
|         }} | ||||
|         src={image.url} | ||||
|       /> | ||||
|  | @ -169,5 +191,11 @@ | |||
|     </div> | ||||
|   </div> | ||||
| {:else if image.status === "hidden"} | ||||
|   <div class="subtle">This image has been reported</div> | ||||
|   <div class="h-80 w-60 flex flex-col justify-center items-center break-words p-4 text-center"> | ||||
|     <TriangleOutline class="w-8 h-8 subtle" /> | ||||
|     <Tr t={Translations.t.image.reported} /> | ||||
|     <button class="text-sm" on:click={() => ignoreHidden = true}> | ||||
|       <Tr t={Translations.t.image.showAnyway} /> | ||||
|     </button> | ||||
|   </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -400,7 +400,7 @@ h2.group { | |||
|     border-radius: 1em; | ||||
|     margin: 0.25em; | ||||
|     text-align: center; | ||||
|     padding: 0.15em 0.3em; | ||||
|     padding: 0.15em 0.7em; | ||||
|     border: 2px dotted #ff9143; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue