UX: finetune 'go to your geolocation' interaction on theme introduction panel, fix #1583

This commit is contained in:
Pieter Vander Vennet 2023-09-24 22:12:07 +02:00
parent c8df0170cc
commit 17b85195a2
4 changed files with 154 additions and 91 deletions

View file

@ -344,6 +344,8 @@
}, },
"useSearch": "Use the search above to see presets", "useSearch": "Use the search above to see presets",
"useSearchForMore": "Use the search function to search within {total} more values…", "useSearchForMore": "Use the search function to search within {total} more values…",
"waitingForGeopermission": "Waiting for your permission to use the geolocation...",
"waitingForLocation": "Searching your current location...",
"weekdays": { "weekdays": {
"abbreviations": { "abbreviations": {
"friday": "Fri", "friday": "Fri",

View file

@ -858,6 +858,10 @@ video {
margin-right: 3rem; margin-right: 3rem;
} }
.mb-4 {
margin-bottom: 1rem;
}
.mr-2 { .mr-2 {
margin-right: 0.5rem; margin-right: 0.5rem;
} }
@ -886,10 +890,6 @@ video {
margin-right: 0.25rem; margin-right: 0.25rem;
} }
.mb-4 {
margin-bottom: 1rem;
}
.ml-1 { .ml-1 {
margin-left: 0.25rem; margin-left: 0.25rem;
} }
@ -2662,6 +2662,46 @@ a.link-underline {
opacity: 1; opacity: 1;
} }
@media (prefers-reduced-motion: no-preference) {
@-webkit-keyframes spin {
to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes spin {
to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.motion-safe\:animate-spin {
-webkit-animation: spin 1s linear infinite;
animation: spin 1s linear infinite;
}
}
@media (prefers-reduced-motion: reduce) {
@-webkit-keyframes spin {
to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes spin {
to {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.motion-reduce\:animate-spin {
-webkit-animation: spin 1s linear infinite;
animation: spin 1s linear infinite;
}
}
@media (max-width: 480px) { @media (max-width: 480px) {
.max-\[480px\]\:w-full { .max-\[480px\]\:w-full {
width: 100%; width: 100%;

View file

@ -1,13 +1,13 @@
import { UIEventSource } from "../UIEventSource" import { UIEventSource } from "../UIEventSource";
import { LocalStorageSource } from "../Web/LocalStorageSource" import { LocalStorageSource } from "../Web/LocalStorageSource";
import { QueryParameters } from "../Web/QueryParameters" import { QueryParameters } from "../Web/QueryParameters";
export type GeolocationPermissionState = "prompt" | "requested" | "granted" | "denied" export type GeolocationPermissionState = "prompt" | "requested" | "granted" | "denied"
export interface GeoLocationPointProperties extends GeolocationCoordinates { export interface GeoLocationPointProperties extends GeolocationCoordinates {
id: "gps" id: "gps";
"user:location": "yes" "user:location": "yes";
date: string date: string;
} }
/** /**
@ -23,22 +23,22 @@ export class GeoLocationState {
*/ */
public readonly permission: UIEventSource<GeolocationPermissionState> = new UIEventSource( public readonly permission: UIEventSource<GeolocationPermissionState> = new UIEventSource(
"prompt" "prompt"
) );
/** /**
* Important to determine e.g. if we move automatically on fix or not * Important to determine e.g. if we move automatically on fix or not
*/ */
public readonly requestMoment: UIEventSource<Date | undefined> = new UIEventSource(undefined) public readonly requestMoment: UIEventSource<Date | undefined> = new UIEventSource(undefined);
/** /**
* If true: the map will center (and re-center) to this location * If true: the map will center (and re-center) to this location
*/ */
public readonly allowMoving: UIEventSource<boolean> = new UIEventSource<boolean>(true) public readonly allowMoving: UIEventSource<boolean> = new UIEventSource<boolean>(true);
/** /**
* The latest GeoLocationCoordinates, as given by the WebAPI * The latest GeoLocationCoordinates, as given by the WebAPI
*/ */
public readonly currentGPSLocation: UIEventSource<GeolocationCoordinates | undefined> = public readonly currentGPSLocation: UIEventSource<GeolocationCoordinates | undefined> =
new UIEventSource<GeolocationCoordinates | undefined>(undefined) new UIEventSource<GeolocationCoordinates | undefined>(undefined);
/** /**
* A small flag on localstorage. If the user previously granted the geolocation, it will be set. * A small flag on localstorage. If the user previously granted the geolocation, it will be set.
@ -50,69 +50,50 @@ export class GeoLocationState {
*/ */
private readonly _previousLocationGrant: UIEventSource<"true" | "false"> = <any>( private readonly _previousLocationGrant: UIEventSource<"true" | "false"> = <any>(
LocalStorageSource.Get("geolocation-permissions") LocalStorageSource.Get("geolocation-permissions")
) );
/** /**
* Used to detect a permission retraction * Used to detect a permission retraction
*/ */
private readonly _grantedThisSession: UIEventSource<boolean> = new UIEventSource<boolean>(false) private readonly _grantedThisSession: UIEventSource<boolean> = new UIEventSource<boolean>(false);
constructor() { constructor() {
const self = this const self = this;
this.permission.addCallbackAndRunD(async (state) => { this.permission.addCallbackAndRunD(async (state) => {
console.trace("GEOPERMISSION", state)
if (state === "granted") { if (state === "granted") {
self._previousLocationGrant.setData("true") self._previousLocationGrant.setData("true");
self._grantedThisSession.setData(true) self._grantedThisSession.setData(true);
} }
if (state === "prompt" && self._grantedThisSession.data) { if (state === "prompt" && self._grantedThisSession.data) {
// This is _really_ weird: we had a grant earlier, but it's 'prompt' now? // This is _really_ weird: we had a grant earlier, but it's 'prompt' now?
// This means that the rights have been revoked again! // This means that the rights have been revoked again!
// self.permission.setData("denied") self._previousLocationGrant.setData("false");
self._previousLocationGrant.setData("false") self.permission.setData("denied");
self.permission.setData("denied") self.currentGPSLocation.setData(undefined);
self.currentGPSLocation.setData(undefined) console.warn("Detected a downgrade in permissions!");
console.warn("Detected a downgrade in permissions!")
} }
if (state === "denied") { if (state === "denied") {
self._previousLocationGrant.setData("false") self._previousLocationGrant.setData("false");
} }
}) });
console.log("Previous location grant:", this._previousLocationGrant.data) console.log("Previous location grant:", this._previousLocationGrant.data);
if (this._previousLocationGrant.data === "true") { if (this._previousLocationGrant.data === "true") {
// A previous visit successfully granted permission. Chance is high that we are allowed to use it again! // A previous visit successfully granted permission. Chance is high that we are allowed to use it again!
// We set the flag to false again. If the user only wanted to share their location once, we are not gonna keep bothering them // We set the flag to false again. If the user only wanted to share their location once, we are not gonna keep bothering them
this._previousLocationGrant.setData("false") this._previousLocationGrant.setData("false");
console.log("Requesting access to GPS as this was previously granted") console.log("Requesting access to GPS as this was previously granted");
const latLonGivenViaUrl = const latLonGivenViaUrl =
QueryParameters.wasInitialized("lat") || QueryParameters.wasInitialized("lon") QueryParameters.wasInitialized("lat") || QueryParameters.wasInitialized("lon");
if (!latLonGivenViaUrl) { if (!latLonGivenViaUrl) {
this.requestMoment.setData(new Date()) this.requestMoment.setData(new Date());
} }
this.requestPermission() this.requestPermission();
} }
} }
/**
* Installs the listener for updates
* @private
*/
private async startWatching() {
const self = this
navigator.geolocation.watchPosition(
function (position) {
self.currentGPSLocation.setData(position.coords)
self._previousLocationGrant.setData("true")
},
function () {
console.warn("Could not get location with navigator.geolocation")
},
{
enableHighAccuracy: true,
}
)
}
/** /**
* Requests the user to allow access to their position. * Requests the user to allow access to their position.
* When granted, will be written to the 'geolocationState'. * When granted, will be written to the 'geolocationState'.
@ -121,33 +102,57 @@ export class GeoLocationState {
public requestPermission() { public requestPermission() {
if (typeof navigator === "undefined") { if (typeof navigator === "undefined") {
// Not compatible with this browser // Not compatible with this browser
this.permission.setData("denied") this.permission.setData("denied");
return return;
} }
if (this.permission.data !== "prompt" && this.permission.data !== "requested") { if (this.permission.data !== "prompt" && this.permission.data !== "requested") {
// If the user denies the first prompt, revokes the deny and then tries again, we have to run the flow as well // If the user denies the first prompt, revokes the deny and then tries again, we have to run the flow as well
// Hence that we continue the flow if it is "requested" // Hence that we continue the flow if it is "requested"
return return;
} }
this.permission.setData("requested") this.permission.setData("requested");
try { try {
navigator?.permissions navigator?.permissions
?.query({ name: "geolocation" }) ?.query({ name: "geolocation" })
.then((status) => { .then((status) => {
console.log("Status update: received geolocation permission is ", status.state) const self = this;
this.permission.setData(status.state) if(status.state === "granted" || status.state === "denied"){
const self = this
status.onchange = function () {
self.permission.setData(status.state) self.permission.setData(status.state)
return
} }
status.addEventListener("change", (e) => {
self.permission.setData(status.state);
});
// The code above might have reset it to 'prompt', but we _did_ request permission!
this.permission.setData("requested") this.permission.setData("requested")
// We _must_ call 'startWatching', as that is the actual trigger for the popup... // We _must_ call 'startWatching', as that is the actual trigger for the popup...
self.startWatching() self.startWatching();
}) })
.catch((e) => console.error("Could not get geopermission", e)) .catch((e) => console.error("Could not get geopermission", e));
} catch (e) { } catch (e) {
console.error("Could not get permission:", e) console.error("Could not get permission:", e);
} }
} }
/**
* Installs the listener for updates
* @private
*/
private async startWatching() {
const self = this;
navigator.geolocation.watchPosition(
function(position) {
self.currentGPSLocation.setData(position.coords);
self._previousLocationGrant.setData("true");
},
function() {
console.warn("Could not get location with navigator.geolocation");
},
{
enableHighAccuracy: true
}
);
}
} }

View file

@ -1,40 +1,44 @@
<script lang="ts"> <script lang="ts">
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations";
import Svg from "../../Svg" import Svg from "../../Svg";
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte";
import NextButton from "../Base/NextButton.svelte" import NextButton from "../Base/NextButton.svelte";
import Geosearch from "./Geosearch.svelte" import Geosearch from "./Geosearch.svelte";
import IfNot from "../Base/IfNot.svelte" import ToSvelte from "../Base/ToSvelte.svelte";
import ToSvelte from "../Base/ToSvelte.svelte" import ThemeViewState from "../../Models/ThemeViewState";
import ThemeViewState from "../../Models/ThemeViewState" import { Store, UIEventSource } from "../../Logic/UIEventSource";
import If from "../Base/If.svelte" import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid";
import { UIEventSource } from "../../Logic/UIEventSource" import { twJoin } from "tailwind-merge";
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid" import { Utils } from "../../Utils";
import { twJoin } from "tailwind-merge" import type { GeolocationPermissionState } from "../../Logic/State/GeoLocationState";
import { Utils } from "../../Utils"
/** /**
* The theme introduction panel * The theme introduction panel
*/ */
export let state: ThemeViewState export let state: ThemeViewState;
let layout = state.layout let layout = state.layout;
let selectedElement = state.selectedElement let selectedElement = state.selectedElement;
let selectedLayer = state.selectedLayer let selectedLayer = state.selectedLayer;
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined) let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined);
let searchEnabled = false let searchEnabled = false;
let geopermission: Store<GeolocationPermissionState> = state.geolocation.geolocationState.permission;
let currentGPSLocation = state.geolocation.geolocationState.currentGPSLocation;
geopermission.addCallback(perm => console.log(">>>> Permission", perm));
function jumpToCurrentLocation() { function jumpToCurrentLocation() {
const glstate = state.geolocation.geolocationState const glstate = state.geolocation.geolocationState;
if (glstate.currentGPSLocation.data !== undefined) { if (glstate.currentGPSLocation.data !== undefined) {
const c: GeolocationCoordinates = glstate.currentGPSLocation.data const c: GeolocationCoordinates = glstate.currentGPSLocation.data;
state.guistate.themeIsOpened.setData(false) state.guistate.themeIsOpened.setData(false);
const coor = { lon: c.longitude, lat: c.latitude } const coor = { lon: c.longitude, lat: c.latitude };
state.mapProperties.location.setData(coor) state.mapProperties.location.setData(coor);
} }
if (glstate.permission.data !== "granted") { if (glstate.permission.data !== "granted") {
glstate.requestPermission() glstate.requestPermission();
return return;
} }
} }
</script> </script>
@ -58,12 +62,24 @@
</NextButton> </NextButton>
<div class="flex w-full flex-wrap sm:flex-nowrap"> <div class="flex w-full flex-wrap sm:flex-nowrap">
<IfNot condition={state.geolocation.geolocationState.permission.map((p) => p === "denied")}> {#if $currentGPSLocation !== undefined || $geopermission === "prompt"}
<button class="flex w-full items-center gap-x-2" on:click={jumpToCurrentLocation}> <button class="flex w-full items-center gap-x-2" on:click={jumpToCurrentLocation}>
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8")} /> <ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8")} />
<Tr t={Translations.t.general.openTheMapAtGeolocation} /> <Tr t={Translations.t.general.openTheMapAtGeolocation} />
</button> </button>
</IfNot> <!-- No geolocation granted - we don't show the button -->
{:else if $geopermission === "requested"}
<button class="flex w-full items-center gap-x-2 disabled" on:click={jumpToCurrentLocation}>
<!-- Even though disabled, when clicking we request the location again in case the contributor dismissed the location popup -->
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8").SetClass("animate-spin")} />
<Tr t={Translations.t.general.waitingForGeopermission} />
</button>
{:else if $geopermission !== "denied"}
<button class="flex w-full items-center gap-x-2 disabled">
<ToSvelte construct={Svg.crosshair_svg().SetClass("w-8 h-8").SetClass("motion-safe:animate-spin")} />
<Tr t={Translations.t.general.waitingForLocation} />
</button>
{/if}
<div class=".button low-interaction m-1 flex w-full items-center gap-x-2 rounded border p-2"> <div class=".button low-interaction m-1 flex w-full items-center gap-x-2 rounded border p-2">
<div class="w-full"> <div class="w-full">